From 687e974046cc2e0167c8456eca4120a056075fab Mon Sep 17 00:00:00 2001 From: elainegan-openai <168589666+elainegan-openai@users.noreply.github.com> Date: Tue, 24 Mar 2026 22:17:20 -0700 Subject: [PATCH 01/40] fix: handle cancelled single function tools as tool failures (#2762) --- src/agents/agent.py | 20 +++ src/agents/run_internal/tool_execution.py | 15 +- tests/test_agent_as_tool.py | 178 ++++++++++++++++++++++ tests/test_agent_runner.py | 39 +++++ tests/test_agent_runner_streamed.py | 40 +++++ tests/test_run_step_execution.py | 23 +++ 6 files changed, 308 insertions(+), 7 deletions(-) diff --git a/src/agents/agent.py b/src/agents/agent.py index 67247afd05..5d700ebaa3 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -850,6 +850,26 @@ async def dispatch_stream_events() -> None: if custom_output_extractor: return await custom_output_extractor(run_result) + if run_result.final_output is not None and ( + not isinstance(run_result.final_output, str) or run_result.final_output != "" + ): + return run_result.final_output + + from .items import ItemHelpers, MessageOutputItem, ToolCallOutputItem + + for item in reversed(run_result.new_items): + if isinstance(item, MessageOutputItem): + text_output = ItemHelpers.text_message_output(item) + if text_output: + return text_output + + if ( + isinstance(item, ToolCallOutputItem) + and isinstance(item.output, str) + and item.output + ): + return item.output + return run_result.final_output run_agent_tool = _build_wrapped_function_tool( diff --git a/src/agents/run_internal/tool_execution.py b/src/agents/run_internal/tool_execution.py index 349066ba48..f2a807020f 100644 --- a/src/agents/run_internal/tool_execution.py +++ b/src/agents/run_internal/tool_execution.py @@ -1400,9 +1400,10 @@ async def _drain_cancelled_tasks( self, tasks: set[asyncio.Task[Any]], ) -> tuple[_FunctionToolFailure | None, set[asyncio.Task[Any]]]: - late_failure_sources: dict[asyncio.Task[Any], _FunctionToolFailureSource] = { - task: "cancelled_teardown" for task in tasks - } + late_failure_sources: dict[asyncio.Task[Any], _FunctionToolFailureSource] = dict.fromkeys( + tasks, + "cancelled_teardown", + ) return await _drain_cancelled_function_tool_tasks( pending_tasks=tasks, task_states=self.task_states, @@ -1415,9 +1416,9 @@ async def _wait_post_invoke_tasks( self, tasks: set[asyncio.Task[Any]], ) -> tuple[_FunctionToolFailure | None, set[asyncio.Task[Any]]]: - post_invoke_failure_sources: dict[asyncio.Task[Any], _FunctionToolFailureSource] = { - task: "post_invoke" for task in tasks - } + post_invoke_failure_sources: dict[asyncio.Task[Any], _FunctionToolFailureSource] = ( + dict.fromkeys(tasks, "post_invoke") + ) return await _wait_pending_function_tool_tasks_for_timeout( pending_tasks=tasks, task_states=self.task_states, @@ -1638,7 +1639,7 @@ async def _invoke_tool_and_run_post_invoke( arguments=tool_call.arguments, ) except asyncio.CancelledError as e: - if not self.isolate_parallel_failures or outer_task in self.teardown_cancelled_tasks: + if outer_task in self.teardown_cancelled_tasks: raise result = await maybe_invoke_function_tool_failure_error_function( diff --git a/tests/test_agent_as_tool.py b/tests/test_agent_as_tool.py index f54e16197d..c5cc123034 100644 --- a/tests/test_agent_as_tool.py +++ b/tests/test_agent_as_tool.py @@ -30,6 +30,7 @@ Session, SessionSettings, ToolApprovalItem, + ToolCallOutputItem, TResponseInputItem, Usage, tool_namespace, @@ -407,6 +408,183 @@ async def extractor(result) -> str: assert output == "custom output" +@pytest.mark.asyncio +async def test_agent_as_tool_fallback_uses_current_run_items_only( + monkeypatch: pytest.MonkeyPatch, +) -> None: + agent = Agent(name="summarizer") + + message = ResponseOutputMessage( + id="msg_current", + role="assistant", + status="completed", + type="message", + content=[ + ResponseOutputText( + annotations=[], + text="Current run summary", + type="output_text", + logprobs=[], + ) + ], + ) + + class DummyResult: + def __init__(self) -> None: + self.final_output = "" + self.new_items = [ + ToolCallOutputItem( + agent=agent, + raw_item={ + "call_id": "call_current", + "output": "Current tool output", + "type": "function_call_output", + }, + output="Current tool output", + ), + MessageOutputItem(agent=agent, raw_item=message), + ] + + def to_input_list(self) -> list[dict[str, Any]]: + return [ + { + "call_id": "call_old", + "output": "Old output from prior history", + "type": "function_call_output", + } + ] + + run_result = DummyResult() + + async def fake_run( + cls, + starting_agent, + input, + *, + context, + max_turns, + hooks, + run_config, + previous_response_id, + conversation_id, + session, + ): + del ( + cls, + starting_agent, + input, + context, + max_turns, + hooks, + run_config, + previous_response_id, + conversation_id, + session, + ) + return run_result + + monkeypatch.setattr(Runner, "run", classmethod(fake_run)) + + tool = agent.as_tool( + tool_name="summary_tool", + tool_description="Summarize current run output", + ) + tool_context = ToolContext( + context=None, + tool_name="summary_tool", + tool_call_id="call_1", + tool_arguments='{"input": "hello"}', + ) + + output = await tool.on_invoke_tool(tool_context, '{"input": "hello"}') + + assert output == "Current run summary" + + +@pytest.mark.asyncio +async def test_agent_as_tool_fallback_returns_most_recent_current_run_output( + monkeypatch: pytest.MonkeyPatch, +) -> None: + agent = Agent(name="summarizer") + + older_message = ResponseOutputMessage( + id="msg_older", + role="assistant", + status="completed", + type="message", + content=[ + ResponseOutputText( + annotations=[], + text="Older message output", + type="output_text", + logprobs=[], + ) + ], + ) + + class DummyResult: + def __init__(self) -> None: + self.final_output = "" + self.new_items = [ + MessageOutputItem(agent=agent, raw_item=older_message), + ToolCallOutputItem( + agent=agent, + raw_item={ + "call_id": "call_current", + "output": "Newest tool output", + "type": "function_call_output", + }, + output="Newest tool output", + ), + ] + + run_result = DummyResult() + + async def fake_run( + cls, + starting_agent, + input, + *, + context, + max_turns, + hooks, + run_config, + previous_response_id, + conversation_id, + session, + ): + del ( + cls, + starting_agent, + input, + context, + max_turns, + hooks, + run_config, + previous_response_id, + conversation_id, + session, + ) + return run_result + + monkeypatch.setattr(Runner, "run", classmethod(fake_run)) + + tool = agent.as_tool( + tool_name="summary_tool", + tool_description="Summarize current run output", + ) + tool_context = ToolContext( + context=None, + tool_name="summary_tool", + tool_call_id="call_1", + tool_arguments='{"input": "hello"}', + ) + + output = await tool.on_invoke_tool(tool_context, '{"input": "hello"}') + + assert output == "Newest tool output" + + @pytest.mark.asyncio async def test_agent_as_tool_extractor_can_access_agent_tool_invocation( monkeypatch: pytest.MonkeyPatch, diff --git a/tests/test_agent_runner.py b/tests/test_agent_runner.py index 8b07297167..ae688fe4e9 100644 --- a/tests/test_agent_runner.py +++ b/tests/test_agent_runner.py @@ -641,6 +641,45 @@ async def _cancel_tool() -> str: ] +@pytest.mark.asyncio +async def test_single_tool_call_with_cancelled_tool_reaches_final_output() -> None: + async def _cancel_tool() -> str: + raise asyncio.CancelledError("tool-cancelled") + + model = FakeModel() + agent = Agent( + name="test", + model=model, + tools=[function_tool(_cancel_tool, name_override="cancel_tool")], + ) + + model.add_multiple_turn_outputs( + [ + [get_function_tool_call("cancel_tool", "{}", call_id="call_cancel")], + [get_text_message("final answer")], + ] + ) + + result = await Runner.run(agent, input="user_message") + + assert result.final_output == "final answer" + assert len(result.raw_responses) == 2 + + second_turn_input = cast(list[dict[str, Any]], model.last_turn_args["input"]) + tool_outputs = [ + item for item in second_turn_input if item.get("type") == "function_call_output" + ] + assert tool_outputs == [ + { + "call_id": "call_cancel", + "output": ( + "An error occurred while running the tool. Please try again. Error: tool-cancelled" + ), + "type": "function_call_output", + }, + ] + + @pytest.mark.asyncio async def test_reasoning_item_id_policy_omits_follow_up_reasoning_ids() -> None: model = FakeModel() diff --git a/tests/test_agent_runner_streamed.py b/tests/test_agent_runner_streamed.py index b38bc82020..1c28fafbc2 100644 --- a/tests/test_agent_runner_streamed.py +++ b/tests/test_agent_runner_streamed.py @@ -496,6 +496,46 @@ async def _cancel_tool() -> str: ] +@pytest.mark.asyncio +async def test_streamed_single_tool_call_with_cancelled_tool_reaches_final_output() -> None: + async def _cancel_tool() -> str: + raise asyncio.CancelledError("tool-cancelled") + + model = FakeModel() + agent = Agent( + name="test", + model=model, + tools=[function_tool(_cancel_tool, name_override="cancel_tool")], + ) + + model.add_multiple_turn_outputs( + [ + [get_function_tool_call("cancel_tool", "{}", call_id="call_cancel")], + [get_text_message("final answer")], + ] + ) + + result = Runner.run_streamed(agent, input="user_message") + await consume_stream(result) + + assert result.final_output == "final answer" + assert len(result.raw_responses) == 2 + + second_turn_input = cast(list[dict[str, Any]], model.last_turn_args["input"]) + tool_outputs = [ + item for item in second_turn_input if item.get("type") == "function_call_output" + ] + assert tool_outputs == [ + { + "call_id": "call_cancel", + "output": ( + "An error occurred while running the tool. Please try again. Error: tool-cancelled" + ), + "type": "function_call_output", + }, + ] + + @pytest.mark.asyncio async def test_streamed_reasoning_item_id_policy_omits_follow_up_reasoning_ids() -> None: model = FakeModel() diff --git a/tests/test_run_step_execution.py b/tests/test_run_step_execution.py index c1cb3fe742..5b0bb6313f 100644 --- a/tests/test_run_step_execution.py +++ b/tests/test_run_step_execution.py @@ -740,6 +740,29 @@ async def _manual_on_invoke_tool(_ctx: ToolContext[Any], _args: str) -> str: ) +@pytest.mark.asyncio +async def test_single_tool_call_uses_default_failure_error_function_for_cancelled_tool(): + async def _cancel_tool() -> str: + raise asyncio.CancelledError("tool-cancelled") + + cancel_tool = function_tool(_cancel_tool, name_override="cancel_tool") + agent = Agent(name="test", tools=[cancel_tool]) + response = ModelResponse( + output=[get_function_tool_call("cancel_tool", "{}", call_id="1")], + usage=Usage(), + response_id=None, + ) + + result = await get_execute_result(agent, response) + + assert len(result.generated_items) == 2 + assert isinstance(result.next_step, NextStepRunAgain) + assert_item_is_function_tool_call_output( + result.generated_items[1], + "An error occurred while running the tool. Please try again. Error: tool-cancelled", + ) + + @pytest.mark.asyncio async def test_multiple_tool_calls_surface_hook_failure_over_sibling_cancellation(): hook_started = asyncio.Event() From 90884968b3d573b39c1e03cd2af930a6bcfdacb4 Mon Sep 17 00:00:00 2001 From: elainegan-openai <168589666+elainegan-openai@users.noreply.github.com> Date: Tue, 24 Mar 2026 22:35:54 -0700 Subject: [PATCH 02/40] fix: optionize initialized notification tolerance (#2765) --- src/agents/mcp/server.py | 124 ++++++++++- .../test_streamable_http_client_factory.py | 193 ++++++++++++++++++ 2 files changed, 313 insertions(+), 4 deletions(-) diff --git a/src/agents/mcp/server.py b/src/agents/mcp/server.py index 363d6995b4..abbb5e087f 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -4,12 +4,13 @@ import asyncio import inspect import sys -from collections.abc import Awaitable +from collections.abc import AsyncGenerator, Awaitable from contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager from datetime import timedelta from pathlib import Path from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar, Union, cast +import anyio import httpx if sys.version_info < (3, 11): @@ -19,7 +20,11 @@ from mcp import ClientSession, StdioServerParameters, Tool as MCPTool, stdio_client from mcp.client.session import MessageHandlerFnT from mcp.client.sse import sse_client -from mcp.client.streamable_http import GetSessionIdCallback, streamablehttp_client +from mcp.client.streamable_http import ( + GetSessionIdCallback, + StreamableHTTPTransport, + streamablehttp_client, +) from mcp.shared.exceptions import McpError from mcp.shared.message import SessionMessage from mcp.types import ( @@ -71,6 +76,101 @@ class RequireApprovalObject(TypedDict, total=False): T = TypeVar("T") +def _create_default_streamable_http_client( + headers: dict[str, str] | None = None, + timeout: httpx.Timeout | None = None, + auth: httpx.Auth | None = None, +) -> httpx.AsyncClient: + kwargs: dict[str, Any] = {"follow_redirects": True} + if timeout is not None: + kwargs["timeout"] = timeout + if headers is not None: + kwargs["headers"] = headers + if auth is not None: + kwargs["auth"] = auth + return httpx.AsyncClient(**kwargs) + + +class _InitializedNotificationTolerantStreamableHTTPTransport(StreamableHTTPTransport): + async def _handle_post_request(self, ctx: Any) -> None: + message = ctx.session_message.message + if not self._is_initialized_notification(message): + await super()._handle_post_request(ctx) + return + + try: + await super()._handle_post_request(ctx) + except httpx.HTTPError: + logger.warning( + "Ignoring initialized notification HTTP failure", + exc_info=True, + ) + return + + +@asynccontextmanager +async def _streamablehttp_client_with_transport( + url: str, + *, + headers: dict[str, str] | None = None, + timeout: float | timedelta = 30, + sse_read_timeout: float | timedelta = 60 * 5, + terminate_on_close: bool = True, + httpx_client_factory: HttpClientFactory = _create_default_streamable_http_client, + auth: httpx.Auth | None = None, + transport_factory: Callable[[str], StreamableHTTPTransport] = StreamableHTTPTransport, +) -> AsyncGenerator[MCPStreamTransport, None]: + timeout_seconds = timeout.total_seconds() if isinstance(timeout, timedelta) else timeout + sse_read_timeout_seconds = ( + sse_read_timeout.total_seconds() + if isinstance(sse_read_timeout, timedelta) + else sse_read_timeout + ) + + client = httpx_client_factory( + headers=headers, + timeout=httpx.Timeout(timeout_seconds, read=sse_read_timeout_seconds), + auth=auth, + ) + transport = transport_factory(url) + read_stream_writer, read_stream = anyio.create_memory_object_stream[SessionMessage | Exception]( + 0 + ) + write_stream, write_stream_reader = anyio.create_memory_object_stream[SessionMessage](0) + + async with client: + async with anyio.create_task_group() as tg: + try: + logger.debug(f"Connecting to StreamableHTTP endpoint: {url}") + + def start_get_stream() -> None: + tg.start_soon(transport.handle_get_stream, client, read_stream_writer) + + tg.start_soon( + transport.post_writer, + client, + write_stream_reader, + read_stream_writer, + write_stream, + start_get_stream, + tg, + ) + + try: + yield ( + read_stream, + write_stream, + transport.get_session_id, + ) + finally: + if transport.session_id and terminate_on_close: + await transport.terminate_session(client) + tg.cancel_scope.cancel() + finally: + await read_stream_writer.aclose() + await write_stream.aclose() + + class _SharedSessionRequestNeedsIsolation(Exception): """Raised when a shared-session request should be retried on an isolated session.""" @@ -1160,6 +1260,14 @@ class MCPServerStreamableHttpParams(TypedDict): transport. """ + ignore_initialized_notification_failure: NotRequired[bool] + """Whether to ignore failures when sending the best-effort + ``notifications/initialized`` POST. + + Defaults to ``False``. When set to ``True``, initialized-notification failures are + logged and ignored so subsequent requests on the same transport can continue. + """ + class MCPServerStreamableHttp(_MCPServerWithClientSession): """MCP server implementation that uses the Streamable HTTP transport. See the [spec] @@ -1250,8 +1358,16 @@ def create_streams( "sse_read_timeout": self.params.get("sse_read_timeout", 60 * 5), "terminate_on_close": self.params.get("terminate_on_close", True), } - if "httpx_client_factory" in self.params: - kwargs["httpx_client_factory"] = self.params["httpx_client_factory"] + httpx_client_factory = self.params.get("httpx_client_factory") + if self.params.get("ignore_initialized_notification_failure", False): + return _streamablehttp_client_with_transport( + **kwargs, + httpx_client_factory=httpx_client_factory or _create_default_streamable_http_client, + auth=self.params.get("auth"), + transport_factory=_InitializedNotificationTolerantStreamableHTTPTransport, + ) + if httpx_client_factory is not None: + kwargs["httpx_client_factory"] = httpx_client_factory if "auth" in self.params: kwargs["auth"] = self.params["auth"] return streamablehttp_client(**kwargs) diff --git a/tests/mcp/test_streamable_http_client_factory.py b/tests/mcp/test_streamable_http_client_factory.py index cf931a3011..068407a2fd 100644 --- a/tests/mcp/test_streamable_http_client_factory.py +++ b/tests/mcp/test_streamable_http_client_factory.py @@ -2,12 +2,21 @@ from __future__ import annotations +import base64 from unittest.mock import MagicMock, patch import httpx import pytest +from anyio import create_memory_object_stream +from mcp.shared.message import SessionMessage +from mcp.types import JSONRPCMessage, JSONRPCNotification, JSONRPCRequest from agents.mcp import MCPServerStreamableHttp +from agents.mcp.server import ( + _create_default_streamable_http_client, + _InitializedNotificationTolerantStreamableHTTPTransport, + _streamablehttp_client_with_transport, +) class TestMCPServerStreamableHttpClientFactory: @@ -247,3 +256,187 @@ def comprehensive_factory( terminate_on_close=False, httpx_client_factory=comprehensive_factory, ) + + +@pytest.mark.asyncio +async def test_initialized_notification_failure_returns_synthetic_success(): + async def handler(request: httpx.Request) -> httpx.Response: + return httpx.Response(503, request=request) + + transport = _InitializedNotificationTolerantStreamableHTTPTransport("https://example.test/mcp") + read_stream_writer, _ = create_memory_object_stream[SessionMessage | Exception](0) + client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + try: + ctx = MagicMock() + ctx.client = client + ctx.read_stream_writer = read_stream_writer + ctx.session_message = SessionMessage( + JSONRPCMessage( + JSONRPCNotification( + jsonrpc="2.0", + method="notifications/initialized", + params={}, + ) + ) + ) + + await transport._handle_post_request(ctx) + finally: + await client.aclose() + await read_stream_writer.aclose() + + +@pytest.mark.asyncio +async def test_initialized_notification_transport_exception_returns_synthetic_success(): + async def handler(request: httpx.Request) -> httpx.Response: + raise httpx.ConnectError("boom", request=request) + + transport = _InitializedNotificationTolerantStreamableHTTPTransport("https://example.test/mcp") + read_stream_writer, _ = create_memory_object_stream[SessionMessage | Exception](0) + client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + try: + ctx = MagicMock() + ctx.client = client + ctx.read_stream_writer = read_stream_writer + ctx.session_message = SessionMessage( + JSONRPCMessage( + JSONRPCNotification( + jsonrpc="2.0", + method="notifications/initialized", + params={}, + ) + ) + ) + + await transport._handle_post_request(ctx) + finally: + await client.aclose() + await read_stream_writer.aclose() + + +@pytest.mark.asyncio +async def test_streamable_http_server_passes_ignore_initialized_notification_failure(): + with patch("agents.mcp.server._streamablehttp_client_with_transport") as mock_client: + mock_client.return_value = MagicMock() + + server = MCPServerStreamableHttp( + params={ + "url": "http://localhost:8000/mcp", + "ignore_initialized_notification_failure": True, + } + ) + + server.create_streams() + + kwargs = mock_client.call_args.kwargs + assert kwargs["url"] == "http://localhost:8000/mcp" + assert kwargs["headers"] is None + assert kwargs["timeout"] == 5 + assert kwargs["sse_read_timeout"] == 300 + assert kwargs["terminate_on_close"] is True + assert ( + kwargs["transport_factory"] is _InitializedNotificationTolerantStreamableHTTPTransport + ) + + +@pytest.mark.asyncio +async def test_transport_preserves_non_initialized_failures(): + async def handler(request: httpx.Request) -> httpx.Response: + raise httpx.ConnectError("boom", request=request) + + transport = _InitializedNotificationTolerantStreamableHTTPTransport("https://example.test/mcp") + read_stream_writer, _ = create_memory_object_stream[SessionMessage | Exception](0) + client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + try: + ctx = MagicMock() + ctx.client = client + ctx.read_stream_writer = read_stream_writer + ctx.session_message = SessionMessage( + JSONRPCMessage( + JSONRPCRequest( + jsonrpc="2.0", + id=1, + method="tools/list", + params={}, + ) + ) + ) + + with pytest.raises(httpx.ConnectError): + await transport._handle_post_request(ctx) + finally: + await client.aclose() + await read_stream_writer.aclose() + + +@pytest.mark.asyncio +async def test_stream_client_preserves_custom_factory_headers_timeout_and_auth(): + seen: dict[str, object] = {} + + class RecordingAuth(httpx.Auth): + def auth_flow(self, request: httpx.Request): + request.headers["Authorization"] = f"Basic {base64.b64encode(b'user:pass').decode()}" + yield request + + async def handler(request: httpx.Request) -> httpx.Response: + seen["request_headers"] = dict(request.headers) + return httpx.Response(200, request=request) + + def base_factory( + headers: dict[str, str] | None = None, + timeout: httpx.Timeout | None = None, + auth: httpx.Auth | None = None, + ) -> httpx.AsyncClient: + seen["factory_headers"] = headers + seen["factory_timeout"] = timeout + seen["factory_auth"] = auth + return httpx.AsyncClient( + headers=headers, + timeout=timeout, + auth=auth, + transport=httpx.MockTransport(handler), + ) + + timeout = httpx.Timeout(12.0) + auth = RecordingAuth() + async with _streamablehttp_client_with_transport( + "https://example.test/mcp", + headers={"X-Test": "value"}, + timeout=12.0, + sse_read_timeout=30.0, + httpx_client_factory=base_factory, + auth=auth, + transport_factory=_InitializedNotificationTolerantStreamableHTTPTransport, + ): + pass + + assert seen["factory_headers"] == {"X-Test": "value"} + seen_timeout = seen["factory_timeout"] + assert isinstance(seen_timeout, httpx.Timeout) + assert seen_timeout.connect == timeout.connect + assert seen_timeout.read == 30.0 + assert seen_timeout.write == timeout.write + assert seen_timeout.pool == timeout.pool + assert seen["factory_auth"] is auth + + +@pytest.mark.asyncio +async def test_default_streamable_http_client_matches_expected_defaults(): + timeout = httpx.Timeout(12.0) + auth = httpx.BasicAuth("user", "pass") + + client = _create_default_streamable_http_client( + headers={"X-Test": "value"}, + timeout=timeout, + auth=auth, + ) + try: + assert client.headers["X-Test"] == "value" + assert client.timeout.connect == timeout.connect + assert client.timeout.read == timeout.read + assert client.timeout.write == timeout.write + assert client.timeout.pool == timeout.pool + assert client.auth is auth + assert client.follow_redirects is True + finally: + await client.aclose() From 881f402aa2c42eac85fdcd0a2a3a7ffe72463cce Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Wed, 25 Mar 2026 16:31:44 +0900 Subject: [PATCH 03/40] fix: harden example auto-runs against PATH and port conflicts (#2770) --- examples/mcp/sse_example/main.py | 31 +++++++++-- examples/mcp/sse_example/server.py | 6 +- examples/run_examples.py | 88 ++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 6 deletions(-) diff --git a/examples/mcp/sse_example/main.py b/examples/mcp/sse_example/main.py index 7c1137d2cf..8180914cd3 100644 --- a/examples/mcp/sse_example/main.py +++ b/examples/mcp/sse_example/main.py @@ -1,14 +1,32 @@ import asyncio import os import shutil +import socket import subprocess import time -from typing import Any +from typing import Any, cast from agents import Agent, Runner, gen_trace_id, trace from agents.mcp import MCPServer, MCPServerSse from agents.model_settings import ModelSettings +SSE_HOST = os.getenv("SSE_HOST", "127.0.0.1") + + +def _choose_port() -> int: + env_port = os.getenv("SSE_PORT") + if env_port: + return int(env_port) + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind((SSE_HOST, 0)) + address = cast(tuple[str, int], s.getsockname()) + return address[1] + + +SSE_PORT = _choose_port() +os.environ.setdefault("SSE_PORT", str(SSE_PORT)) +SSE_URL = f"http://{SSE_HOST}:{SSE_PORT}/sse" + async def run(mcp_server: MCPServer): agent = Agent( @@ -41,7 +59,7 @@ async def main(): async with MCPServerSse( name="SSE Python Server", params={ - "url": "http://localhost:8000/sse", + "url": SSE_URL, }, ) as server: trace_id = gen_trace_id() @@ -58,16 +76,19 @@ async def main(): ) # We'll run the SSE server in a subprocess. Usually this would be a remote server, but for this - # demo, we'll run it locally at http://localhost:8000/sse + # demo, we'll run it locally at SSE_URL. process: subprocess.Popen[Any] | None = None try: this_dir = os.path.dirname(os.path.abspath(__file__)) server_file = os.path.join(this_dir, "server.py") - print("Starting SSE server at http://localhost:8000/sse ...") + print(f"Starting SSE server at {SSE_URL} ...") # Run `uv run server.py` to start the SSE server - process = subprocess.Popen(["uv", "run", server_file]) + env = os.environ.copy() + env.setdefault("SSE_HOST", SSE_HOST) + env.setdefault("SSE_PORT", str(SSE_PORT)) + process = subprocess.Popen(["uv", "run", server_file], env=env) # Give it 3 seconds to start time.sleep(3) diff --git a/examples/mcp/sse_example/server.py b/examples/mcp/sse_example/server.py index 709f8cb810..075137fe03 100644 --- a/examples/mcp/sse_example/server.py +++ b/examples/mcp/sse_example/server.py @@ -1,9 +1,13 @@ +import os import random from mcp.server.fastmcp import FastMCP +SSE_HOST = os.getenv("SSE_HOST", "127.0.0.1") +SSE_PORT = int(os.getenv("SSE_PORT", "8000")) + # Create server -mcp = FastMCP("Echo Server") +mcp = FastMCP("Echo Server", host=SSE_HOST, port=SSE_PORT) @mcp.tool() diff --git a/examples/run_examples.py b/examples/run_examples.py index a3a8174464..79f76f921b 100644 --- a/examples/run_examples.py +++ b/examples/run_examples.py @@ -13,6 +13,7 @@ import argparse import datetime +import functools import os import re import shlex @@ -32,6 +33,18 @@ RERUN_FILE_DEFAULT = ROOT_DIR / ".tmp" / "examples-rerun.txt" DEFAULT_MAIN_LOG = LOG_DIR_DEFAULT / f"main_{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}.log" +COMMON_PATH_HINTS = ( + Path.home() / ".local" / "bin", + Path("/opt/homebrew/bin"), + Path("/opt/homebrew/sbin"), + Path("/usr/local/bin"), + Path("/usr/local/sbin"), +) + +DISCOVERY_EXCLUDE = { + "examples/run_examples.py", +} + # Examples that are noisy, require extra credentials, or hang in auto runs. DEFAULT_AUTO_SKIP = { "examples/agent_patterns/llm_as_a_judge.py", @@ -39,6 +52,13 @@ "examples/customer_service/main.py", "examples/hosted_mcp/connectors.py", "examples/mcp/git_example/main.py", + # These are helper daemons or multi-process components exercised by sibling examples. + "examples/mcp/manager_example/app.py", + "examples/mcp/manager_example/mcp_server.py", + "examples/mcp/prompt_server/server.py", + "examples/mcp/sse_example/server.py", + "examples/mcp/streamablehttp_custom_client_example/server.py", + "examples/mcp/streamablehttp_example/server.py", "examples/model_providers/custom_example_agent.py", "examples/model_providers/custom_example_global.py", "examples/model_providers/custom_example_provider.py", @@ -84,6 +104,63 @@ def normalize_relpath(relpath: str) -> str: return str(PurePosixPath(normalized)) +def split_path_entries(path_value: str) -> list[str]: + return [entry for entry in path_value.split(os.pathsep) if entry] + + +def dedupe_existing_paths(paths: Iterable[str]) -> list[str]: + deduped: list[str] = [] + seen: set[str] = set() + for entry in paths: + expanded = os.path.expanduser(entry) + if not expanded or expanded in seen: + continue + if not Path(expanded).exists(): + continue + deduped.append(expanded) + seen.add(expanded) + return deduped + + +@functools.lru_cache(maxsize=1) +def interactive_shell_path() -> str | None: + shell = os.environ.get("SHELL") + if not shell: + return None + + shell_name = Path(shell).name + if shell_name not in {"bash", "zsh"}: + return None + + try: + result = subprocess.run( + [shell, "-lic", 'printf "%s" "$PATH"'], + capture_output=True, + check=True, + cwd=ROOT_DIR, + text=True, + ) + except (OSError, subprocess.SubprocessError): + return None + + path_value = result.stdout.strip() + return path_value or None + + +def build_command_path(base_path: str | None = None) -> str: + candidates: list[str] = [] + if base_path is None: + base_path = os.environ.get("PATH", "") + candidates.extend(split_path_entries(base_path)) + + shell_path = interactive_shell_path() + if shell_path: + candidates.extend(split_path_entries(shell_path)) + + candidates.extend(str(path) for path in COMMON_PATH_HINTS) + return os.pathsep.join(dedupe_existing_paths(candidates)) + + def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Run example scripts sequentially.") parser.add_argument( @@ -221,6 +298,10 @@ def discover_examples(filters: Iterable[str]) -> list[ExampleScript]: if not MAIN_PATTERN.search(source): continue + relpath = normalize_relpath(str(path.relative_to(ROOT_DIR))) + if relpath in DISCOVERY_EXCLUDE: + continue + if filters_lower and not any( f in str(path.relative_to(ROOT_DIR)).lower() for f in filters_lower ): @@ -351,6 +432,11 @@ def run_examples(examples: Sequence[ExampleScript], args: argparse.Namespace) -> buffer_output = not args.no_buffer_output and os.environ.get( "EXAMPLES_BUFFER_OUTPUT", "1" ).lower() not in {"0", "false", "no", "off"} + command_path = build_command_path() + path_augmented = command_path != os.environ.get("PATH", "") + + if path_augmented: + print("Augmented subprocess PATH using interactive shell/common tool directories.") def safe_write_main(line: str) -> None: with main_log_lock: @@ -363,6 +449,7 @@ def run_single(example: ExampleScript) -> ExampleResult: ensure_dirs(log_path, is_file=True) env = os.environ.copy() + env["PATH"] = command_path if auto_mode: env["EXAMPLES_INTERACTIVE_MODE"] = "auto" env["APPLY_PATCH_AUTO_APPROVE"] = "1" @@ -441,6 +528,7 @@ def run_single(example: ExampleScript) -> ExampleResult: safe_write_main(f"# logs_dir: {logs_dir}") safe_write_main(f"# jobs: {jobs}") safe_write_main(f"# buffer_output: {buffer_output}") + safe_write_main(f"# path_augmented: {path_augmented}") run_list: list[ExampleScript] = [] From 0a5b8c9ce56ed7735bd034df0f684b30b18526bd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:37:26 +0900 Subject: [PATCH 04/40] Release 0.13.1 (#2768) --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e22a8dd4ac..fe08cae8d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openai-agents" -version = "0.13.0" +version = "0.13.1" description = "OpenAI Agents SDK" readme = "README.md" requires-python = ">=3.10" diff --git a/uv.lock b/uv.lock index 8bbb2f9304..f4e4b0e36f 100644 --- a/uv.lock +++ b/uv.lock @@ -1905,7 +1905,7 @@ wheels = [ [[package]] name = "openai-agents" -version = "0.13.0" +version = "0.13.1" source = { editable = "." } dependencies = [ { name = "griffe" }, From 13f4a7405e491050ebed1072860839f17b516365 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Wed, 25 Mar 2026 16:46:21 +0900 Subject: [PATCH 05/40] docs: update pages to add any-llm adapter (#2715) --- docs/examples.md | 4 ++-- docs/llms-full.txt | 4 ++-- docs/llms.txt | 4 ++-- docs/models/index.md | 32 +++++++++++++++++++++----------- docs/models/litellm.md | 4 ++-- docs/ref/extensions/litellm.md | 8 +++++++- docs/tracing.md | 8 ++++---- docs/usage.md | 19 +++++-------------- mkdocs.yml | 6 +++++- 9 files changed, 50 insertions(+), 39 deletions(-) diff --git a/docs/examples.md b/docs/examples.md index 8a291192a0..8c353aa3de 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -29,7 +29,7 @@ Check out a variety of sample implementations of the SDK in the examples section - File handling (local and remote, images and PDFs) - Usage tracking - Runner-managed retry settings (`examples/basic/retry.py`) - - Runner-managed retries with LiteLLM (`examples/basic/retry_litellm.py`) + - Runner-managed retries through a third-party adapter (`examples/basic/retry_litellm.py`) - Non-strict output types - Previous response ID usage @@ -68,7 +68,7 @@ Check out a variety of sample implementations of the SDK in the examples section - Stateless Responses compaction with `ModelSettings(store=False)` (`examples/memory/compaction_session_stateless_example.py`) - **[model_providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers):** - Explore how to use non-OpenAI models with the SDK, including custom providers and LiteLLM integration. + Explore how to use non-OpenAI models with the SDK, including custom providers and third-party adapters. - **[realtime](https://github.com/openai/openai-agents-python/tree/main/examples/realtime):** Examples showing how to build real-time experiences using the SDK, including: diff --git a/docs/llms-full.txt b/docs/llms-full.txt index dddad2545d..f700844061 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -38,7 +38,7 @@ The Agents SDK delivers a focused set of Python primitives—agents, tools, guar - [Realtime guide](https://openai.github.io/openai-agents-python/realtime/guide/): Deep dive into realtime session lifecycle, structured input, approvals, interruptions, and low-level transport control. ## Models and Provider Integrations -- [Model catalog](https://openai.github.io/openai-agents-python/models/): Covers OpenAI model selection, non-OpenAI provider patterns, websocket transport, and the SDK's best-effort LiteLLM guidance in one place. +- [Model catalog](https://openai.github.io/openai-agents-python/models/): Covers OpenAI model selection, non-OpenAI provider patterns, websocket transport, and third-party adapter guidance in one place. ## API Reference – Agents SDK Core - [API index](https://openai.github.io/openai-agents-python/ref/index/): Directory of all documented modules, classes, and functions in the SDK. @@ -103,7 +103,7 @@ The Agents SDK delivers a focused set of Python primitives—agents, tools, guar ## API Reference – Extensions - [Handoff filters extension](https://openai.github.io/openai-agents-python/ref/extensions/handoff_filters/): Build filters that decide whether to trigger a handoff. - [Handoff prompt extension](https://openai.github.io/openai-agents-python/ref/extensions/handoff_prompt/): Customize prompt templates used when transferring control. -- [LiteLLM extension](https://openai.github.io/openai-agents-python/ref/extensions/litellm/): Adapter for using LiteLLM-managed providers inside the SDK. +- [Third-party adapters API reference](https://openai.github.io/openai-agents-python/ref/extensions/): API reference entry point for Any-LLM and LiteLLM model adapters and providers. - [SQLAlchemy session memory](https://openai.github.io/openai-agents-python/ref/extensions/memory/sqlalchemy_session/): Persist agent session history to SQL databases. ## Optional diff --git a/docs/llms.txt b/docs/llms.txt index a96401c0c0..1665255e9d 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -49,10 +49,10 @@ The SDK focuses on a concise set of primitives so you can orchestrate multi-agen - [Tracing APIs](https://openai.github.io/openai-agents-python/ref/tracing/index/): Programmatic interfaces for creating traces, spans, and integrating custom processors. - [Realtime APIs](https://openai.github.io/openai-agents-python/ref/realtime/agent/): Classes for realtime agents, runners, sessions, and event payloads. - [Voice APIs](https://openai.github.io/openai-agents-python/ref/voice/pipeline/): Configure voice pipelines, inputs, events, and model adapters. -- [Extensions](https://openai.github.io/openai-agents-python/ref/extensions/handoff_filters/): Extend the SDK with custom handoff filters, prompts, LiteLLM integration, and SQLAlchemy session memory. +- [Extensions](https://openai.github.io/openai-agents-python/ref/extensions/handoff_filters/): Extend the SDK with custom handoff filters, prompts, third-party adapters, and SQLAlchemy session memory. ## Models and Providers -- [Model catalog](https://openai.github.io/openai-agents-python/models/): Overview of OpenAI models, non-OpenAI provider patterns, websocket transport, and the SDK's best-effort LiteLLM guidance. +- [Model catalog](https://openai.github.io/openai-agents-python/models/): Overview of OpenAI models, non-OpenAI provider patterns, websocket transport, and third-party adapter guidance. ## Optional - [Release notes](https://openai.github.io/openai-agents-python/release/): Track SDK changes, migration notes, and deprecations. diff --git a/docs/models/index.md b/docs/models/index.md index 3ec1b573c8..fa818611d1 100644 --- a/docs/models/index.md +++ b/docs/models/index.md @@ -16,7 +16,7 @@ Start with the simplest path that fits your setup: | Use one non-OpenAI provider | Start with the built-in provider integration points | [Non-OpenAI models](#non-openai-models) | | Mix models or providers across agents | Select providers per run or per agent and review feature differences | [Mixing models in one workflow](#mixing-models-in-one-workflow) and [Mixing models across providers](#mixing-models-across-providers) | | Tune advanced OpenAI Responses request settings | Use `ModelSettings` on the OpenAI Responses path | [Advanced OpenAI Responses settings](#advanced-openai-responses-settings) | -| Use LiteLLM for non-OpenAI Chat Completions providers | Treat LiteLLM as a beta fallback | [LiteLLM](#litellm) | +| Use a third-party adapter for non-OpenAI or mixed-provider routing | Compare the supported beta adapters and validate the provider path you plan to ship | [Third-party adapters](#third-party-adapters) | ## OpenAI models @@ -135,7 +135,7 @@ result = await Runner.run( #### Advanced routing with `MultiProvider` -If you need prefix-based model routing (for example mixing `openai/...` and `litellm/...` model names in one run), use [`MultiProvider`][agents.MultiProvider] and set `openai_use_responses_websocket=True` there instead. +If you need prefix-based model routing (for example mixing `openai/...` and `any-llm/...` model names in one run), use [`MultiProvider`][agents.MultiProvider] and set `openai_use_responses_websocket=True` there instead. `MultiProvider` keeps two historical defaults: @@ -180,7 +180,7 @@ If you use a custom OpenAI-compatible endpoint or proxy, websocket transport als ## Non-OpenAI models -If you need a non-OpenAI provider, start with the SDK's built-in provider integration points. In many setups, this is enough without adding LiteLLM. Examples for each pattern live in [examples/model_providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/). +If you need a non-OpenAI provider, start with the SDK's built-in provider integration points. In many setups, this is enough without adding a third-party adapter. Examples for each pattern live in [examples/model_providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/). ### Ways to integrate non-OpenAI providers @@ -189,7 +189,7 @@ If you need a non-OpenAI provider, start with the SDK's built-in provider integr | [`set_default_openai_client`][agents.set_default_openai_client] | One OpenAI-compatible endpoint should be the default for most or all agents | Global default | | [`ModelProvider`][agents.models.interface.ModelProvider] | One custom provider should apply to a single run | Per run | | [`Agent.model`][agents.agent.Agent.model] | Different agents need different providers or concrete model objects | Per agent | -| LiteLLM (beta) | You need LiteLLM-specific provider coverage or routing | See [LiteLLM](#litellm) | +| Third-party adapter | You need adapter-managed provider coverage or routing that the built-in paths do not provide | See [Third-party adapters](#third-party-adapters) | You can integrate other LLM providers with these built-in paths: @@ -404,7 +404,7 @@ Stateful follow-up requests using `previous_response_id` or `conversation_id` ar - An agent can override only part of `retry.backoff` and keep sibling backoff fields from the runner. - `policy` is runtime-only, so serialized `ModelSettings` keep `max_retries` and `backoff` but omit the callback itself. -For fuller examples, see [`examples/basic/retry.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry.py) and [`examples/basic/retry_litellm.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry_litellm.py). +For fuller examples, see [`examples/basic/retry.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry.py) and the [adapter-backed retry example](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry_litellm.py). ## Troubleshooting non-OpenAI providers @@ -443,14 +443,24 @@ You need to be aware of feature differences between model providers, or you may - Filter out multimodal inputs before calling models that are text-only - Be aware that providers that don't support structured JSON outputs will occasionally produce invalid JSON. -## LiteLLM +## Third-party adapters -LiteLLM support is included on a best-effort, beta basis for cases where you need to bring non-OpenAI providers into an Agents SDK workflow. +Reach for a third-party adapter only when the SDK's built-in provider integration points are not enough. If you are using OpenAI models only with this SDK, prefer the built-in [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] path instead of Any-LLM or LiteLLM. Third-party adapters are for cases where you need to combine OpenAI models with non-OpenAI providers, or need adapter-managed provider coverage or routing that the built-in paths do not provide. Adapters add another compatibility layer between the SDK and the upstream model provider, so feature support and request semantics can vary by provider. The SDK currently includes Any-LLM and LiteLLM as best-effort, beta adapter integrations. -If you are using OpenAI models with this SDK, we recommend the built-in [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] path instead of LiteLLM. +### Any-LLM -If you need to combine OpenAI models with non-OpenAI providers, especially through Chat Completions-compatible APIs, LiteLLM is available as a beta option, but it may not be the optimal choice for every setup. +Any-LLM support is included on a best-effort, beta basis for cases where you need Any-LLM-managed provider coverage or routing. -If you need LiteLLM for a non-OpenAI provider, install `openai-agents[litellm]`, then start from [`examples/model_providers/litellm_auto.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/litellm_auto.py) or [`examples/model_providers/litellm_provider.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/litellm_provider.py). You can either use `litellm/...` model names or instantiate [`LitellmModel`][agents.extensions.models.litellm_model.LitellmModel] directly. +Depending on the upstream provider path, Any-LLM may use the Responses API, Chat Completions-compatible APIs, or provider-specific compatibility layers. -If you want LiteLLM responses to populate the SDK's usage metrics, pass `ModelSettings(include_usage=True)`. +If you need Any-LLM, install `openai-agents[any-llm]`, then start from [`examples/model_providers/any_llm_auto.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/any_llm_auto.py) or [`examples/model_providers/any_llm_provider.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/any_llm_provider.py). You can use `any-llm/...` model names with [`MultiProvider`][agents.MultiProvider], instantiate `AnyLLMModel` directly, or use `AnyLLMProvider` at run scope. If you need to pin the model surface explicitly, pass `api="responses"` or `api="chat_completions"` when constructing `AnyLLMModel`. + +Any-LLM remains a third-party adapter layer, so provider dependencies and capability gaps are defined upstream by Any-LLM rather than by the SDK. Usage metrics are propagated automatically when the upstream provider returns them, but streamed Chat Completions backends may require `ModelSettings(include_usage=True)` before they emit usage chunks. Validate the exact provider backend you plan to deploy if you depend on structured outputs, tool calling, usage reporting, or Responses-specific behavior. + +### LiteLLM + +LiteLLM support is included on a best-effort, beta basis for cases where you need LiteLLM-specific provider coverage or routing. + +If you need LiteLLM, install `openai-agents[litellm]`, then start from [`examples/model_providers/litellm_auto.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/litellm_auto.py) or [`examples/model_providers/litellm_provider.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/litellm_provider.py). You can use `litellm/...` model names or instantiate [`LitellmModel`][agents.extensions.models.litellm_model.LitellmModel] directly. + +Some LiteLLM-backed providers do not populate SDK usage metrics by default. If you need usage reporting, pass `ModelSettings(include_usage=True)` and validate the exact provider backend you plan to deploy if you depend on structured outputs, tool calling, usage reporting, or adapter-specific routing behavior. diff --git a/docs/models/litellm.md b/docs/models/litellm.md index cf6e971c3a..e4863dd6ce 100644 --- a/docs/models/litellm.md +++ b/docs/models/litellm.md @@ -1,9 +1,9 @@ # LiteLLM -This page moved to the [LiteLLM section in Models](index.md#litellm). +This page moved to the [Third-party adapters section in Models](index.md#third-party-adapters). If you are not redirected automatically, use the link above. diff --git a/docs/ref/extensions/litellm.md b/docs/ref/extensions/litellm.md index 7bd67fde4f..bb550bac8e 100644 --- a/docs/ref/extensions/litellm.md +++ b/docs/ref/extensions/litellm.md @@ -1,3 +1,9 @@ # `LiteLLM Models` -::: agents.extensions.models.litellm_model + + +This page moved to the [Third-party adapters API reference](third_party_adapters.md). + +If you are not redirected automatically, use the link above. diff --git a/docs/tracing.md b/docs/tracing.md index 9dc289aeff..15cdfd6c31 100644 --- a/docs/tracing.md +++ b/docs/tracing.md @@ -103,18 +103,18 @@ To customize this default setup, to send traces to alternative or additional bac ## Tracing with non-OpenAI models -You can use an OpenAI API key with non-OpenAI Models to enable free tracing in the OpenAI Traces dashboard without needing to disable tracing. +You can use an OpenAI API key with non-OpenAI models to enable free tracing in the OpenAI Traces dashboard without needing to disable tracing. See the [Third-party adapters](models/index.md#third-party-adapters) section in the Models guide for adapter selection and setup caveats. ```python import os from agents import set_tracing_export_api_key, Agent, Runner -from agents.extensions.models.litellm_model import LitellmModel +from agents.extensions.models.any_llm_model import AnyLLMModel tracing_api_key = os.environ["OPENAI_API_KEY"] set_tracing_export_api_key(tracing_api_key) -model = LitellmModel( - model="your-model-name", +model = AnyLLMModel( + model="your-provider/your-model-name", api_key="your-api-key", ) diff --git a/docs/usage.md b/docs/usage.md index 938f2467c7..71dcc7aa98 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -29,23 +29,14 @@ print("Total tokens:", usage.total_tokens) Usage is aggregated across all model calls during the run (including tool calls and handoffs). -### Enabling usage with LiteLLM models +### Enabling usage with third-party adapters -LiteLLM providers do not report usage metrics by default. When you are using [`LitellmModel`][agents.extensions.models.litellm_model.LitellmModel], pass `ModelSettings(include_usage=True)` to your agent so that LiteLLM responses populate `result.context_wrapper.usage`. See the [LiteLLM note](models/index.md#litellm) in the Models guide for setup guidance and examples. +Usage reporting varies across third-party adapters and provider backends. If you rely on adapter-backed models and need accurate `result.context_wrapper.usage` values: -```python -from agents import Agent, ModelSettings, Runner -from agents.extensions.models.litellm_model import LitellmModel - -agent = Agent( - name="Assistant", - model=LitellmModel(model="your/model", api_key="..."), - model_settings=ModelSettings(include_usage=True), -) +- With `AnyLLMModel`, usage is propagated automatically when the upstream provider returns it. For streamed Chat Completions backends, you may need `ModelSettings(include_usage=True)` before usage chunks are emitted. +- With `LitellmModel`, some provider backends do not report usage by default, so `ModelSettings(include_usage=True)` is often required. -result = await Runner.run(agent, "What's the weather in Tokyo?") -print(result.context_wrapper.usage.total_tokens) -``` +Review the adapter-specific notes in the [Third-party adapters](models/index.md#third-party-adapters) section of the Models guide and validate the exact provider backend you plan to deploy. ## Per-request usage tracking diff --git a/mkdocs.yml b/mkdocs.yml index 3ef42e5609..057472b830 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -156,7 +156,11 @@ plugins: - Extensions: - ref/extensions/handoff_filters.md - ref/extensions/handoff_prompt.md - - ref/extensions/litellm.md + - Third-party adapters: + - Any-LLM model: ref/extensions/models/any_llm_model.md + - Any-LLM provider: ref/extensions/models/any_llm_provider.md + - LiteLLM model: ref/extensions/models/litellm_model.md + - LiteLLM provider: ref/extensions/models/litellm_provider.md - ref/extensions/tool_output_trimmer.md - ref/extensions/memory/sqlalchemy_session.md - ref/extensions/memory/async_sqlite_session.md From 7dc7fa2be1e8faba64f727698c22a753ac79d91b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:55:39 +0900 Subject: [PATCH 06/40] docs: update translated document pages (#2771) --- docs/ja/examples.md | 54 ++++---- docs/ja/models/index.md | 268 ++++++++++++++++++++------------------ docs/ja/models/litellm.md | 4 +- docs/ja/tracing.md | 84 ++++++------ docs/ja/usage.md | 45 +++---- docs/ko/examples.md | 78 +++++------ docs/ko/models/index.md | 208 +++++++++++++++-------------- docs/ko/models/litellm.md | 6 +- docs/ko/tracing.md | 80 ++++++------ docs/ko/usage.md | 41 +++--- docs/zh/examples.md | 46 +++---- docs/zh/models/index.md | 220 ++++++++++++++++--------------- docs/zh/models/litellm.md | 4 +- docs/zh/tracing.md | 92 ++++++------- docs/zh/usage.md | 53 ++++---- 15 files changed, 643 insertions(+), 640 deletions(-) diff --git a/docs/ja/examples.md b/docs/ja/examples.md index 4d6bdb4ac5..dbe84067a5 100644 --- a/docs/ja/examples.md +++ b/docs/ja/examples.md @@ -4,99 +4,99 @@ search: --- # コード例 -[repo](https://github.com/openai/openai-agents-python/tree/main/examples) の examples セクションで、 SDK のさまざまなサンプル実装をご確認ください。examples は、異なるパターンと機能を示す複数のカテゴリーに整理されています。 +[repo](https://github.com/openai/openai-agents-python/tree/main/examples) の examples セクションで、 SDK のさまざまなサンプル実装を確認できます。examples は、異なるパターンと機能を示す複数のカテゴリーに整理されています。 ## カテゴリー - **[agent_patterns](https://github.com/openai/openai-agents-python/tree/main/examples/agent_patterns):** - このカテゴリーのコード例では、一般的なエージェント設計パターンを示しています。たとえば次のとおりです。 + このカテゴリーのコード例では、一般的なエージェント設計パターンを示しています。例: - 決定論的ワークフロー - Agents as tools - エージェントの並列実行 - 条件付きツール使用 - - 入出力ガードレール + - 入力/出力ガードレール - 審判としての LLM - ルーティング - ストリーミングガードレール - 承認フロー向けのカスタム拒否メッセージ (`examples/agent_patterns/human_in_the_loop_custom_rejection.py`) - **[basic](https://github.com/openai/openai-agents-python/tree/main/examples/basic):** - これらのコード例では、 SDK の基本的な機能を紹介しています。たとえば次のとおりです。 + これらのコード例では、 SDK の基礎的な機能を紹介しています。例: - - Hello World のコード例 (デフォルトモデル、 GPT-5、オープンウェイトモデル) + - Hello world のコード例 (デフォルトモデル、 GPT-5 、 open-weight モデル) - エージェントライフサイクル管理 - 動的システムプロンプト - ストリーミング出力 (テキスト、項目、関数呼び出し引数) - - ターン間で共有セッションヘルパーを使用する Responses websocket transport (`examples/basic/stream_ws.py`) + - ターン間で共有セッションヘルパーを使う Responses websocket トランスポート (`examples/basic/stream_ws.py`) - プロンプトテンプレート - - ファイル処理 (ローカルおよびリモート、画像および PDF) + - ファイル処理 (ローカル/リモート、画像/PDF) - 使用状況トラッキング - - Runner 管理の再試行設定 (`examples/basic/retry.py`) - - LiteLLM を使用した Runner 管理の再試行 (`examples/basic/retry_litellm.py`) + - Runner 管理のリトライ設定 (`examples/basic/retry.py`) + - サードパーティアダプターを介した Runner 管理のリトライ (`examples/basic/retry_litellm.py`) - 非 strict な出力型 - - 以前のレスポンス ID の使用 + - 前回レスポンス ID の使用 - **[customer_service](https://github.com/openai/openai-agents-python/tree/main/examples/customer_service):** 航空会社向けのカスタマーサービスシステムのコード例です。 - **[financial_research_agent](https://github.com/openai/openai-agents-python/tree/main/examples/financial_research_agent):** - 金融データ分析向けに、エージェントとツールを使った構造化リサーチワークフローを示す金融リサーチエージェントです。 + 金融データ分析のためのエージェントとツールを使った構造化リサーチワークフローを示す、金融リサーチエージェントです。 - **[handoffs](https://github.com/openai/openai-agents-python/tree/main/examples/handoffs):** - メッセージフィルタリングを使ったエージェントハンドオフの実践的なコード例をご覧ください。 + メッセージフィルタリングを伴うエージェントハンドオフの実践的なコード例をご覧ください。 - **[hosted_mcp](https://github.com/openai/openai-agents-python/tree/main/examples/hosted_mcp):** - ホストされた MCP (Model context protocol) コネクタと承認の使い方を示すコード例です。 + hosted MCP (Model Context Protocol) コネクターと承認の使い方を示すコード例です。 - **[mcp](https://github.com/openai/openai-agents-python/tree/main/examples/mcp):** - MCP (Model context protocol) を使ってエージェントを構築する方法を学べます。内容は次のとおりです。 + MCP (Model Context Protocol) を使ってエージェントを構築する方法を学べます。内容: - ファイルシステムのコード例 - Git のコード例 - MCP プロンプトサーバーのコード例 - SSE (Server-Sent Events) のコード例 - - ストリーム可能な HTTP のコード例 + - ストリーミング可能な HTTP のコード例 - **[memory](https://github.com/openai/openai-agents-python/tree/main/examples/memory):** - エージェント向けのさまざまなメモリ実装のコード例です。内容は次のとおりです。 + エージェント向けのさまざまなメモリ実装のコード例です。内容: - SQLite セッションストレージ - 高度な SQLite セッションストレージ - Redis セッションストレージ - SQLAlchemy セッションストレージ - - Dapr state store セッションストレージ + - Dapr ステートストアセッションストレージ - 暗号化セッションストレージ - OpenAI Conversations セッションストレージ - Responses compaction セッションストレージ - `ModelSettings(store=False)` を使ったステートレスな Responses compaction (`examples/memory/compaction_session_stateless_example.py`) - **[model_providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers):** - カスタムプロバイダーや LiteLLM 統合を含め、 SDK で非 OpenAI モデルを使う方法を確認できます。 + カスタムプロバイダーやサードパーティアダプターを含め、 SDK で非 OpenAI モデルを使う方法を確認できます。 - **[realtime](https://github.com/openai/openai-agents-python/tree/main/examples/realtime):** - SDK を使用してリアルタイム体験を構築する方法を示すコード例です。内容は次のとおりです。 + SDK を使ってリアルタイム体験を構築する方法を示すコード例です。内容: - - 構造化テキストと画像メッセージを使う Web アプリケーションパターン + - 構造化テキストおよび画像メッセージを使った Web アプリケーションパターン - コマンドライン音声ループと再生処理 - WebSocket 経由の Twilio Media Streams 統合 - - Realtime Calls API のアタッチフローを使う Twilio SIP 統合 + - Realtime Calls API attach フローを使用した Twilio SIP 統合 - **[reasoning_content](https://github.com/openai/openai-agents-python/tree/main/examples/reasoning_content):** reasoning content と structured outputs の扱い方を示すコード例です。 - **[research_bot](https://github.com/openai/openai-agents-python/tree/main/examples/research_bot):** - 複雑なマルチエージェントのリサーチワークフローを示す、シンプルなディープリサーチクローンです。 + 複雑なマルチエージェントのディープリサーチワークフローを示す、シンプルなディープリサーチクローンです。 - **[tools](https://github.com/openai/openai-agents-python/tree/main/examples/tools):** - 次のような OpenAI がホストするツールと実験的な Codex ツール機能の実装方法を学べます。 + OpenAI がホストするツールと、次のような実験的な Codex ツール機能の実装方法を学べます。 - Web 検索 とフィルター付き Web 検索 - ファイル検索 - Code Interpreter - - インラインスキル付きホストコンテナーシェル (`examples/tools/container_shell_inline_skill.py`) - - スキル参照付きホストコンテナーシェル (`examples/tools/container_shell_skill_reference.py`) - - ローカルスキル付きローカルシェル (`examples/tools/local_shell_skill.py`) + - インラインスキル付き hosted container shell (`examples/tools/container_shell_inline_skill.py`) + - スキル参照付き hosted container shell (`examples/tools/container_shell_skill_reference.py`) + - ローカルスキル付きローカル shell (`examples/tools/local_shell_skill.py`) - 名前空間と遅延ツールを使ったツール検索 (`examples/tools/tool_search.py`) - コンピュータ操作 - 画像生成 @@ -104,4 +104,4 @@ search: - 実験的な Codex 同一スレッドワークフロー (`examples/tools/codex_same_thread.py`) - **[voice](https://github.com/openai/openai-agents-python/tree/main/examples/voice):** - ストリーミング音声のコード例を含む、 TTS および STT モデルを使用した音声エージェントのコード例をご覧ください。 \ No newline at end of file + ストリーミング音声のコード例を含む、 TTS / STT モデルを使用した音声エージェントのコード例をご覧ください。 \ No newline at end of file diff --git a/docs/ja/models/index.md b/docs/ja/models/index.md index f221dbd95c..4e8a239deb 100644 --- a/docs/ja/models/index.md +++ b/docs/ja/models/index.md @@ -4,42 +4,42 @@ search: --- # モデル -Agents SDK には、OpenAI モデルをすぐに使える形で 2 つの方式でサポートしています。 +Agents SDK には、 OpenAI モデル向けの即時利用可能なサポートが 2 つの形で用意されています。 -- **推奨**: [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel]。新しい [Responses API](https://platform.openai.com/docs/api-reference/responses) を使って OpenAI API を呼び出します。 -- [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel]。 [Chat Completions API](https://platform.openai.com/docs/api-reference/chat) を使って OpenAI API を呼び出します。 +- **推奨**: [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel]。新しい [Responses API](https://platform.openai.com/docs/api-reference/responses) を使用して OpenAI API を呼び出します。 +- [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel]。 [Chat Completions API](https://platform.openai.com/docs/api-reference/chat) を使用して OpenAI API を呼び出します。 ## モデル設定の選択 -ご利用の構成に合う最もシンプルな経路から始めてください。 +まずは、構成に合う最もシンプルな方法から始めてください。 -| 目的 | 推奨経路 | 詳細 | +| 次のことをしたい場合 | 推奨パス | 詳細 | | --- | --- | --- | -| OpenAI モデルのみを使う | デフォルトの OpenAI provider と Responses モデル経路を使う | [OpenAI モデル](#openai-models) | -| websocket 転送で OpenAI Responses API を使う | Responses モデル経路を維持し、websocket 転送を有効化する | [Responses WebSocket 転送](#responses-websocket-transport) | -| 1 つの non-OpenAI provider を使う | 組み込みの provider 統合ポイントから始める | [non-OpenAI モデル](#non-openai-models) | -| エージェント間でモデルや provider を混在させる | 実行単位またはエージェント単位で provider を選び、機能差を確認する | [1 つのワークフロー内でのモデル混在](#mixing-models-in-one-workflow) および [provider 間でのモデル混在](#mixing-models-across-providers) | -| OpenAI Responses の高度なリクエスト設定を調整する | OpenAI Responses 経路で `ModelSettings` を使う | [高度な OpenAI Responses 設定](#advanced-openai-responses-settings) | -| non-OpenAI Chat Completions provider に LiteLLM を使う | LiteLLM を beta のフォールバックとして扱う | [LiteLLM](#litellm) | +| OpenAI モデルのみを使用する | 既定の OpenAI プロバイダーと Responses モデルパスを使用する | [OpenAI モデル](#openai-models) | +| websocket トランスポートで OpenAI Responses API を使用する | Responses モデルパスを維持し、 websocket トランスポートを有効化する | [Responses WebSocket トランスポート](#responses-websocket-transport) | +| 1 つの非 OpenAI プロバイダーを使用する | 組み込みのプロバイダー統合ポイントから始める | [非 OpenAI モデル](#non-openai-models) | +| エージェント間でモデルまたはプロバイダーを混在させる | 実行ごとまたはエージェントごとにプロバイダーを選択し、機能差を確認する | [1 つのワークフローでのモデル混在](#mixing-models-in-one-workflow) と [プロバイダー間でのモデル混在](#mixing-models-across-providers) | +| OpenAI Responses の高度なリクエスト設定を調整する | OpenAI Responses パスで `ModelSettings` を使用する | [高度な OpenAI Responses 設定](#advanced-openai-responses-settings) | +| 非 OpenAI または混在プロバイダーのルーティングにサードパーティーアダプターを使う | サポートされているベータアダプターを比較し、出荷予定のプロバイダーパスを検証する | [サードパーティーアダプター](#third-party-adapters) | ## OpenAI モデル -ほとんどの OpenAI 専用アプリでは、デフォルトの OpenAI provider と文字列のモデル名を使い、Responses モデル経路を維持する方法を推奨します。 +ほとんどの OpenAI 専用アプリでは、既定の OpenAI プロバイダーで文字列のモデル名を使い、 Responses モデルパスを使い続けるのが推奨です。 -`Agent` 初期化時にモデルを指定しない場合は、デフォルトモデルが使われます。現在のデフォルトは互換性と低遅延のため [`gpt-4.1`](https://developers.openai.com/api/docs/models/gpt-4.1) です。利用可能であれば、明示的な `model_settings` を維持しつつ、より高品質な [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4) をエージェントに設定することを推奨します。 +`Agent` 初期化時にモデルを指定しない場合は、既定モデルが使われます。現在の既定は互換性と低レイテンシのため [`gpt-4.1`](https://developers.openai.com/api/docs/models/gpt-4.1) です。利用可能な場合は、明示的な `model_settings` を維持したまま、より高品質な [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4) にエージェントを設定することを推奨します。 -[`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4) のような他モデルに切り替えるには、エージェントを設定する方法が 2 つあります。 +[`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4) のような他モデルへ切り替えるには、エージェントの設定方法が 2 つあります。 -### デフォルトモデル +### 既定モデル -まず、カスタムモデルを設定しないすべてのエージェントで特定モデルを一貫して使いたい場合は、エージェント実行前に `OPENAI_DEFAULT_MODEL` 環境変数を設定します。 +1 つ目として、カスタムモデルを設定しないすべてのエージェントで特定モデルを一貫して使いたい場合は、エージェント実行前に環境変数 `OPENAI_DEFAULT_MODEL` を設定します。 ```bash export OPENAI_DEFAULT_MODEL=gpt-5.4 python3 my_awesome_agent.py ``` -次に、`RunConfig` で実行ごとのデフォルトモデルを設定できます。エージェントにモデルを設定しなければ、この実行のモデルが使われます。 +2 つ目として、 `RunConfig` で実行単位の既定モデルを設定できます。エージェントにモデルを設定しなければ、この実行のモデルが使われます。 ```python from agents import Agent, RunConfig, Runner @@ -58,7 +58,7 @@ result = await Runner.run( #### GPT-5 モデル -この方法で [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4) のような GPT-5 モデルを使う場合、SDK はデフォルトの `ModelSettings` を適用します。多くのユースケースで最適に動く設定が使われます。デフォルトモデルの推論 effort を調整するには、独自の `ModelSettings` を渡します。 +この方法で [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4) などの GPT-5 モデルを使う場合、 SDK は既定の `ModelSettings` を適用します。これは多くのユースケースで最適に動く設定です。既定モデルの推論 effort を調整するには、独自の `ModelSettings` を渡してください。 ```python from openai.types.shared import Reasoning @@ -74,21 +74,21 @@ my_agent = Agent( ) ``` -低遅延のためには、`gpt-5.4` で `reasoning.effort="none"` を使うことを推奨します。gpt-4.1 ファミリー( mini / nano を含む)も、対話型エージェントアプリ構築において有力な選択肢です。 +低レイテンシには、 `gpt-5.4` で `reasoning.effort="none"` を使うことを推奨します。 gpt-4.1 ファミリー( mini と nano を含む)も、対話型エージェントアプリ構築において引き続き有力な選択肢です。 #### ComputerTool モデル選択 -エージェントに [`ComputerTool`][agents.tool.ComputerTool] が含まれる場合、実際の Responses リクエストで有効なモデルによって、SDK が送信する computer-tool ペイロードが決まります。明示的な `gpt-5.4` リクエストでは GA の組み込み `computer` ツールを使い、明示的な `computer-use-preview` リクエストでは従来の `computer_use_preview` ペイロードを維持します。 +エージェントに [`ComputerTool`][agents.tool.ComputerTool] が含まれる場合、実際の Responses リクエストで有効なモデルによって、 SDK が送信するコンピュータツール payload が決まります。明示的な `gpt-5.4` リクエストでは GA の組み込み `computer` ツールを使い、明示的な `computer-use-preview` リクエストでは従来の `computer_use_preview` payload を維持します。 -主な例外は prompt 管理型呼び出しです。prompt テンプレートがモデルを所有し、SDK がリクエストから `model` を省略する場合、SDK は prompt がどのモデルに固定されているかを推測しないため、preview 互換の computer ペイロードをデフォルトで使います。このフローで GA 経路を維持するには、リクエストで `model="gpt-5.4"` を明示するか、`ModelSettings(tool_choice="computer")` または `ModelSettings(tool_choice="computer_use")` で GA セレクターを強制してください。 +主な例外はプロンプト管理呼び出しです。プロンプトテンプレートがモデルを保持し、 SDK がリクエストから `model` を省略する場合、 SDK はプロンプトが固定するモデルを推測しないため、 preview 互換のコンピュータ payload を既定で使います。このフローで GA パスを維持するには、リクエストで `model="gpt-5.4"` を明示するか、 `ModelSettings(tool_choice="computer")` または `ModelSettings(tool_choice="computer_use")` で GA セレクターを強制してください。 -[`ComputerTool`][agents.tool.ComputerTool] が登録されている場合、`tool_choice="computer"`、`"computer_use"`、`"computer_use_preview"` は、有効なリクエストモデルに一致する組み込みセレクターに正規化されます。`ComputerTool` が登録されていない場合、これらの文字列は通常の関数名として振る舞い続けます。 +[`ComputerTool`][agents.tool.ComputerTool] が登録されている場合、 `tool_choice="computer"` 、 `"computer_use"` 、 `"computer_use_preview"` は、有効なリクエストモデルに一致する組み込みセレクターに正規化されます。 `ComputerTool` が登録されていない場合、これらの文字列は通常の関数名として動作し続けます。 -preview 互換リクエストでは `environment` と表示寸法を先にシリアライズする必要があるため、[`ComputerProvider`][agents.tool.ComputerProvider] ファクトリーを使う prompt 管理フローでは、具体的な `Computer` または `AsyncComputer` インスタンスを渡すか、リクエスト送信前に GA セレクターを強制する必要があります。移行の詳細は [Tools](../tools.md#computertool-and-the-responses-computer-tool) を参照してください。 +preview 互換リクエストでは、 `environment` と表示寸法を先にシリアライズする必要があるため、 [`ComputerProvider`][agents.tool.ComputerProvider] ファクトリーを使うプロンプト管理フローでは、具体的な `Computer` または `AsyncComputer` インスタンスを渡すか、リクエスト送信前に GA セレクターを強制する必要があります。移行の詳細は [Tools](../tools.md#computertool-and-the-responses-computer-tool) を参照してください。 -#### non-GPT-5 モデル +#### 非 GPT-5 モデル -カスタム `model_settings` なしで non–GPT-5 モデル名を渡すと、SDK は任意モデル互換の汎用 `ModelSettings` に戻ります。 +カスタム `model_settings` なしで非 GPT-5 モデル名を渡すと、 SDK は任意モデル互換の汎用 `ModelSettings` に戻ります。 ### Responses 専用ツール検索機能 @@ -96,13 +96,13 @@ preview 互換リクエストでは `environment` と表示寸法を先にシリ - [`ToolSearchTool`][agents.tool.ToolSearchTool] - [`tool_namespace()`][agents.tool.tool_namespace] -- `@function_tool(defer_loading=True)` と、その他の遅延読み込み Responses ツール面 +- `@function_tool(defer_loading=True)` と、その他の遅延読み込み Responses ツールサーフェス -これらの機能は Chat Completions モデルおよび non-Responses バックエンドでは拒否されます。遅延読み込みツールを使う場合は、エージェントに `ToolSearchTool()` を追加し、素の namespace 名や遅延専用関数名を強制する代わりに、`auto` または `required` の tool choice でモデルにツールを読み込ませてください。設定詳細と現時点の制約は [Tools](../tools.md#hosted-tool-search) を参照してください。 +これらの機能は Chat Completions モデルおよび非 Responses バックエンドでは拒否されます。遅延読み込みツールを使う場合は、エージェントに `ToolSearchTool()` を追加し、 namespace 名や遅延専用関数名を強制する代わりに、 `auto` または `required` の tool choice でモデルにツールを読み込ませてください。設定詳細と現在の制約は [Tools](../tools.md#hosted-tool-search) を参照してください。 -### Responses WebSocket 転送 +### Responses WebSocket トランスポート -デフォルトでは、OpenAI Responses API リクエストは HTTP 転送を使います。OpenAI バックエンドのモデル使用時には websocket 転送を有効化できます。 +既定では、 OpenAI Responses API リクエストは HTTP トランスポートを使用します。 OpenAI バックエンドモデル使用時は websocket トランスポートを有効化できます。 #### 基本設定 @@ -112,13 +112,13 @@ from agents import set_default_openai_responses_transport set_default_openai_responses_transport("websocket") ``` -これは、デフォルト OpenAI provider で解決される OpenAI Responses モデル( `"gpt-5.4"` のような文字列モデル名を含む)に影響します。 +これは、既定の OpenAI プロバイダーで解決される OpenAI Responses モデル( `"gpt-5.4"` のような文字列モデル名を含む)に影響します。 -転送方式の選択は、SDK がモデル名をモデルインスタンスへ解決するときに行われます。具体的な [`Model`][agents.models.interface.Model] オブジェクトを渡した場合、その転送方式はすでに固定されています。[`OpenAIResponsesWSModel`][agents.models.openai_responses.OpenAIResponsesWSModel] は websocket、[`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] は HTTP、[`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] は Chat Completions のままです。`RunConfig(model_provider=...)` を渡す場合は、グローバルデフォルトではなくその provider が転送選択を制御します。 +トランスポート選択は、 SDK がモデル名をモデルインスタンスへ解決する際に行われます。具体的な [`Model`][agents.models.interface.Model] オブジェクトを渡す場合、そのトランスポートは既に固定されています。 [`OpenAIResponsesWSModel`][agents.models.openai_responses.OpenAIResponsesWSModel] は websocket、 [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] は HTTP、 [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] は Chat Completions のままです。 `RunConfig(model_provider=...)` を渡した場合、グローバル既定ではなくそのプロバイダーがトランスポート選択を制御します。 -#### provider / 実行レベル設定 +#### プロバイダーまたは実行レベル設定 -websocket 転送は provider 単位または実行単位でも設定できます。 +websocket トランスポートはプロバイダー単位または実行単位でも設定できます。 ```python from agents import Agent, OpenAIProvider, RunConfig, Runner @@ -137,16 +137,16 @@ result = await Runner.run( ) ``` -#### `MultiProvider` による高度なルーティング +#### `MultiProvider` を使った高度なルーティング -接頭辞ベースのモデルルーティングが必要な場合(例: 1 回の実行で `openai/...` と `litellm/...` のモデル名を混在させる)、[`MultiProvider`][agents.MultiProvider] を使い、そこで `openai_use_responses_websocket=True` を設定してください。 +プレフィックスベースのモデルルーティングが必要な場合(例: 1 回の実行で `openai/...` と `any-llm/...` を混在)、 [`MultiProvider`][agents.MultiProvider] を使い、そこで `openai_use_responses_websocket=True` を設定します。 -`MultiProvider` は 2 つの従来デフォルトを維持しています。 +`MultiProvider` は 2 つの従来既定を維持します。 -- `openai/...` は OpenAI provider のエイリアスとして扱われるため、`openai/gpt-4.1` はモデル `gpt-4.1` としてルーティングされます。 -- 未知の接頭辞はそのまま渡されず、`UserError` を発生させます。 +- `openai/...` は OpenAI プロバイダーのエイリアスとして扱われるため、 `openai/gpt-4.1` はモデル `gpt-4.1` としてルーティングされます。 +- 不明なプレフィックスはそのまま渡されず、 `UserError` を発生させます。 -OpenAI 互換エンドポイントで、名前空間付きモデル ID の文字列をそのまま期待する場合は、明示的に pass-through 動作を有効化してください。websocket 有効構成では、`MultiProvider` 側でも `openai_use_responses_websocket=True` を維持してください。 +OpenAI 互換エンドポイントが名前空間付きモデル ID の文字列そのものを期待する場合は、明示的にパススルー動作を有効化してください。 websocket 有効構成では、 `MultiProvider` 側でも `openai_use_responses_websocket=True` を維持してください。 ```python from agents import Agent, MultiProvider, RunConfig, Runner @@ -172,52 +172,52 @@ result = await Runner.run( ) ``` -バックエンドが `openai/...` の文字列リテラルを期待する場合は `openai_prefix_mode="model_id"` を使います。`openrouter/openai/gpt-4.1-mini` のような他の名前空間付きモデル ID を期待する場合は `unknown_prefix_mode="model_id"` を使います。これらのオプションは websocket 転送外の `MultiProvider` でも動作します。この例で websocket を有効化しているのは、このセクションで説明している転送設定の一部だからです。同じオプションは [`responses_websocket_session()`][agents.responses_websocket_session] でも利用可能です。 +バックエンドが文字列 `openai/...` をそのまま期待する場合は `openai_prefix_mode="model_id"` を使います。バックエンドが `openrouter/openai/gpt-4.1-mini` のような他の名前空間付きモデル ID を期待する場合は `unknown_prefix_mode="model_id"` を使います。これらのオプションは websocket 以外の `MultiProvider` でも利用可能です。この例では本セクションのトランスポート設定の一部として websocket を有効のままにしています。同じオプションは [`responses_websocket_session()`][agents.responses_websocket_session] でも利用できます。 -カスタムの OpenAI 互換エンドポイントや proxy を使う場合、websocket 転送には互換 websocket `/responses` エンドポイントも必要です。このような構成では `websocket_base_url` の明示設定が必要になることがあります。 +カスタム OpenAI 互換エンドポイントまたはプロキシを使う場合、 websocket トランスポートには互換 websocket `/responses` エンドポイントも必要です。その構成では `websocket_base_url` を明示設定する必要がある場合があります。 -#### 注記 +#### 注意事項 -- これは websocket 転送上の Responses API であり、[Realtime API](../realtime/guide.md) ではありません。Chat Completions や、Responses websocket `/responses` エンドポイントをサポートしない non-OpenAI provider には適用されません。 +- これは websocket トランスポート上の Responses API であり、 [Realtime API](../realtime/guide.md) ではありません。 Chat Completions や、 Responses websocket `/responses` エンドポイントをサポートしない非 OpenAI プロバイダーには適用されません。 - 環境で未導入の場合は `websockets` パッケージをインストールしてください。 -- websocket 転送を有効化後、[`Runner.run_streamed()`][agents.run.Runner.run_streamed] を直接使えます。複数ターンのワークフローで同じ websocket 接続をターン間(ネストした agent-as-tool 呼び出しを含む)で再利用したい場合は、[`responses_websocket_session()`][agents.responses_websocket_session] ヘルパーを推奨します。[Running agents](../running_agents.md) ガイドと [`examples/basic/stream_ws.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/stream_ws.py) を参照してください。 +- websocket トランスポート有効化後は [`Runner.run_streamed()`][agents.run.Runner.run_streamed] を直接使用できます。同じ websocket 接続をターン間(ネストした agent-as-tool 呼び出しを含む)で再利用したいマルチターンワークフローでは、 [`responses_websocket_session()`][agents.responses_websocket_session] ヘルパーを推奨します。[Running agents](../running_agents.md) ガイドおよび [`examples/basic/stream_ws.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/stream_ws.py) を参照してください。 -## non-OpenAI モデル +## 非 OpenAI モデル -non-OpenAI provider が必要な場合は、まず SDK の組み込み provider 統合ポイントから始めてください。多くの構成では LiteLLM 追加なしで十分です。各パターンの例は [examples/model_providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/) にあります。 +非 OpenAI プロバイダーが必要な場合は、 SDK の組み込みプロバイダー統合ポイントから始めてください。多くの構成では、サードパーティーアダプターを追加せずに十分対応できます。各パターンの例は [examples/model_providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/) にあります。 -### non-OpenAI provider 統合方法 +### 非 OpenAI プロバイダー統合方法 | アプローチ | 使用する場面 | スコープ | | --- | --- | --- | -| [`set_default_openai_client`][agents.set_default_openai_client] | 1 つの OpenAI 互換エンドポイントを、ほとんどまたはすべてのエージェントのデフォルトにしたい | グローバルデフォルト | -| [`ModelProvider`][agents.models.interface.ModelProvider] | 1 つのカスタム provider を単一実行に適用したい | 実行単位 | -| [`Agent.model`][agents.agent.Agent.model] | エージェントごとに異なる provider または具体的モデルオブジェクトが必要 | エージェント単位 | -| LiteLLM (beta) | LiteLLM 固有の provider カバレッジやルーティングが必要 | [LiteLLM](#litellm) を参照 | +| [`set_default_openai_client`][agents.set_default_openai_client] | 1 つの OpenAI 互換エンドポイントをほとんどまたはすべてのエージェントの既定にしたい | グローバル既定 | +| [`ModelProvider`][agents.models.interface.ModelProvider] | 1 つのカスタムプロバイダーを単一実行に適用したい | 実行単位 | +| [`Agent.model`][agents.agent.Agent.model] | 異なるエージェントに異なるプロバイダーまたは具体的モデルオブジェクトが必要 | エージェント単位 | +| サードパーティーアダプター | 組み込みパスで提供されない、アダプター管理のプロバイダー対応またはルーティングが必要 | [サードパーティーアダプター](#third-party-adapters) を参照 | -これらの組み込み経路で他の LLM provider を統合できます。 +これらの組み込みパスで他の LLM プロバイダーを統合できます。 -1. [`set_default_openai_client`][agents.set_default_openai_client] は、`AsyncOpenAI` インスタンスを LLM クライアントとしてグローバルに使いたい場合に有用です。LLM provider が OpenAI 互換 API エンドポイントを持ち、`base_url` と `api_key` を設定できる場合に使います。設定可能な例は [examples/model_providers/custom_example_global.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_global.py) を参照してください。 -2. [`ModelProvider`][agents.models.interface.ModelProvider] は `Runner.run` レベルです。これにより「この実行の全エージェントでカスタム model provider を使う」と指定できます。設定可能な例は [examples/model_providers/custom_example_provider.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_provider.py) を参照してください。 -3. [`Agent.model`][agents.agent.Agent.model] では特定 Agent インスタンスでモデルを指定できます。これによりエージェントごとに異なる provider を組み合わせられます。設定可能な例は [examples/model_providers/custom_example_agent.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_agent.py) を参照してください。 +1. [`set_default_openai_client`][agents.set_default_openai_client] は、 `AsyncOpenAI` インスタンスを LLM クライアントとしてグローバルに使用したい場合に有用です。これは、 LLM プロバイダーが OpenAI 互換 API エンドポイントを持ち、 `base_url` と `api_key` を設定できるケース向けです。設定可能な例は [examples/model_providers/custom_example_global.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_global.py) を参照してください。 +2. [`ModelProvider`][agents.models.interface.ModelProvider] は `Runner.run` レベルです。これにより「この実行のすべてのエージェントでカスタムモデルプロバイダーを使用する」と指定できます。設定可能な例は [examples/model_providers/custom_example_provider.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_provider.py) を参照してください。 +3. [`Agent.model`][agents.agent.Agent.model] は特定の Agent インスタンスでモデルを指定できます。これにより、異なるエージェントに対して異なるプロバイダーを組み合わせられます。設定可能な例は [examples/model_providers/custom_example_agent.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_agent.py) を参照してください。 -`platform.openai.com` の API key がない場合は、`set_tracing_disabled()` でトレーシングを無効化するか、[別のトレーシングプロセッサー](../tracing.md) を設定することを推奨します。 +`platform.openai.com` の API キーがない場合は、 `set_tracing_disabled()` でトレーシングを無効化するか、 [別のトレーシングプロセッサー](../tracing.md) の設定を推奨します。 !!! note - これらの例では、Chat Completions API / model を使っています。多くの LLM provider がまだ Responses API をサポートしていないためです。LLM provider が対応している場合は Responses の利用を推奨します。 + これらの例では、多くの LLM プロバイダーがまだ Responses API をサポートしていないため、 Chat Completions API / モデルを使用しています。お使いの LLM プロバイダーが対応している場合は、 Responses の使用を推奨します。 -## 1 つのワークフロー内でのモデル混在 +## 1 つのワークフローでのモデル混在 -単一ワークフロー内で、エージェントごとに異なるモデルを使いたい場合があります。たとえば、トリアージには小さく高速なモデルを使い、複雑なタスクには大きく高性能なモデルを使う、といった構成です。[`Agent`][agents.Agent] を設定する際、次のいずれかで特定モデルを選択できます。 +1 つのワークフロー内で、エージェントごとに異なるモデルを使いたい場合があります。たとえば、トリアージには小さく高速なモデルを使い、複雑なタスクにはより大規模で高機能なモデルを使えます。[`Agent`][agents.Agent] を設定する際、特定モデルは次のいずれかで選択できます。 1. モデル名を渡す。 -2. 任意のモデル名 + その名前を Model インスタンスにマップできる [`ModelProvider`][agents.models.interface.ModelProvider] を渡す。 -3. [`Model`][agents.models.interface.Model] 実装を直接渡す。 +2. 任意のモデル名 + その名前を Model インスタンスにマップ可能な [`ModelProvider`][agents.models.interface.ModelProvider] を渡す。 +3. [`Model`][agents.models.interface.Model] 実装を直接提供する。 !!! note - SDK は [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] と [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] の両方をサポートしていますが、2 つは対応機能・ツール集合が異なるため、ワークフローごとに単一のモデル形状を使うことを推奨します。モデル形状を混在させる必要がある場合は、利用する機能が両方で使えることを確認してください。 + SDK は [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] と [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] の両方の形をサポートしますが、 2 つは対応機能とツールが異なるため、ワークフローごとに 1 つのモデル形に統一することを推奨します。ワークフローでモデル形の混在が必要な場合は、使用するすべての機能が両方で利用可能であることを確認してください。 ```python from agents import Agent, Runner, AsyncOpenAI, OpenAIChatCompletionsModel @@ -253,7 +253,7 @@ async def main(): 1. OpenAI モデル名を直接設定します。 2. [`Model`][agents.models.interface.Model] 実装を提供します。 -エージェントで使うモデルをさらに設定したい場合は、temperature などの任意モデル設定パラメーターを提供する [`ModelSettings`][agents.models.interface.ModelSettings] を渡せます。 +エージェントで使うモデルをさらに設定したい場合は、 temperature などの任意パラメーターを提供する [`ModelSettings`][agents.models.interface.ModelSettings] を渡せます。 ```python from agents import Agent, ModelSettings @@ -268,19 +268,19 @@ english_agent = Agent( ## 高度な OpenAI Responses 設定 -OpenAI Responses 経路でより細かな制御が必要な場合は、`ModelSettings` から始めてください。 +OpenAI Responses パスでより細かな制御が必要な場合は、まず `ModelSettings` を使用してください。 ### 一般的な高度 `ModelSettings` オプション -OpenAI Responses API 利用時は、いくつかのリクエストフィールドに直接対応する `ModelSettings` フィールドがすでにあるため、それらに `extra_args` は不要です。 +OpenAI Responses API 使用時は、いくつかのリクエストフィールドに対応する `ModelSettings` フィールドが既にあるため、それらには `extra_args` は不要です。 -- `parallel_tool_calls`: 同一ターンでの複数 tool call を許可 / 禁止します。 -- `truncation`: `"auto"` を設定すると、コンテキスト超過時に失敗せず、Responses API が最も古い会話項目を削除します。 -- `store`: 生成レスポンスを後続取得のためサーバー側に保存するかを制御します。レスポンス ID に依存するフォローアップワークフローや、`store=False` 時にローカル入力へフォールバックが必要なセッション圧縮フローで重要です。 -- `prompt_cache_retention`: たとえば `"24h"` のように、キャッシュされた prompt 接頭辞をより長く保持します。 -- `response_include`: `web_search_call.action.sources`、`file_search_call.results`、`reasoning.encrypted_content` など、より豊富なレスポンスペイロードを要求します。 -- `top_logprobs`: 出力テキストの上位 token logprobs を要求します。SDK は `message.output_text.logprobs` も自動追加します。 -- `retry`: モデル呼び出しに対する runner 管理 retry 設定を有効化します。[Runner 管理リトライ](#runner-managed-retries) を参照してください。 +- `parallel_tool_calls`: 同一ターンでの複数ツール呼び出しを許可または禁止します。 +- `truncation`: コンテキストあふれ時に失敗する代わりに、 Responses API に最古の会話項目を削除させるには `"auto"` を設定します。 +- `store`: 生成された応答を後で取得できるようサーバー側に保存するかを制御します。これは、 response ID に依存するフォローアップワークフローや、 `store=False` 時にローカル入力へフォールバックが必要なセッション圧縮フローで重要です。 +- `prompt_cache_retention`: たとえば `"24h"` のように、キャッシュされたプロンプト接頭辞をより長く保持します。 +- `response_include`: `web_search_call.action.sources` 、 `file_search_call.results` 、 `reasoning.encrypted_content` など、より豊富な応答 payload を要求します。 +- `top_logprobs`: 出力テキストの上位トークン logprobs を要求します。 SDK は `message.output_text.logprobs` も自動追加します。 +- `retry`: モデル呼び出しにランナー管理リトライ設定を有効化します。[ランナー管理リトライ](#runner-managed-retries) を参照してください。 ```python from agents import Agent, ModelSettings @@ -299,13 +299,13 @@ research_agent = Agent( ) ``` -`store=False` を設定すると、Responses API はそのレスポンスを後続のサーバー側取得に利用できる状態で保持しません。これは stateless または zero-data-retention 風フローで有用ですが、通常レスポンス ID を再利用する機能は、代わりにローカル管理状態へ依存する必要があります。たとえば [`OpenAIResponsesCompactionSession`][agents.memory.openai_responses_compaction_session.OpenAIResponsesCompactionSession] は、最後のレスポンスが保存されていない場合、デフォルト `"auto"` 圧縮経路を入力ベース圧縮へ切り替えます。[Sessions ガイド](../sessions/index.md#openai-responses-compaction-sessions) を参照してください。 +`store=False` を設定すると、 Responses API はその応答を後でサーバー側取得可能な状態に保持しません。これはステートレスまたはゼロデータ保持スタイルのフローで有用ですが、通常 response ID を再利用する機能は、代わりにローカル管理状態に依存する必要があります。たとえば、 [`OpenAIResponsesCompactionSession`][agents.memory.openai_responses_compaction_session.OpenAIResponsesCompactionSession] は、最後の応答が保存されていない場合、既定の `"auto"` 圧縮パスを入力ベース圧縮に切り替えます。[Sessions ガイド](../sessions/index.md#openai-responses-compaction-sessions) を参照してください。 ### `extra_args` の受け渡し -SDK がまだトップレベルで直接公開していない provider 固有または新しいリクエストフィールドが必要な場合は `extra_args` を使います。 +SDK がまだトップレベルで直接公開していない、プロバイダー固有または新しいリクエストフィールドが必要な場合は `extra_args` を使います。 -また OpenAI の Responses API を使う場合、[他にもいくつかの任意パラメーター](https://platform.openai.com/docs/api-reference/responses/create)(例: `user`、`service_tier` など)があります。トップレベルにない場合は、`extra_args` で渡せます。 +また OpenAI の Responses API 使用時には、[その他の任意パラメーター](https://platform.openai.com/docs/api-reference/responses/create)(例: `user` 、 `service_tier` など)があります。トップレベルにない場合、これらも `extra_args` で渡せます。 ```python from agents import Agent, ModelSettings @@ -321,9 +321,9 @@ english_agent = Agent( ) ``` -## Runner 管理リトライ +## ランナー管理リトライ -リトライは実行時限定で、明示的な opt-in です。`ModelSettings(retry=...)` を設定し、かつ retry policy が再試行を選択しない限り、SDK は一般的なモデルリクエストをリトライしません。 +リトライは実行時専用で、明示的に有効化する必要があります。 SDK は `ModelSettings(retry=...)` を設定し、かつリトライポリシーがリトライを選択した場合を除き、一般的なモデルリクエストをリトライしません。 ```python from agents import Agent, ModelRetrySettings, ModelSettings, retry_policies @@ -357,79 +357,79 @@ agent = Agent( | フィールド | 型 | 注記 | | --- | --- | --- | -| `max_retries` | `int | None` | 初回リクエスト後に許可される再試行回数です。 | -| `backoff` | `ModelRetryBackoffSettings | dict | None` | policy が明示的 delay を返さずに再試行するときのデフォルト遅延戦略です。 | -| `policy` | `RetryPolicy | None` | 再試行するかを決めるコールバックです。このフィールドは実行時限定でシリアライズされません。 | +| `max_retries` | `int | None` | 初回リクエスト後に許可されるリトライ試行回数。 | +| `backoff` | `ModelRetryBackoffSettings | dict | None` | ポリシーが明示遅延を返さずにリトライする場合の既定遅延戦略。 | +| `policy` | `RetryPolicy | None` | リトライ可否を判断するコールバック。このフィールドは実行時専用でシリアライズされません。 | -retry policy は [`RetryPolicyContext`][agents.retry.RetryPolicyContext] を受け取ります。内容は以下です。 +リトライポリシーは、 [`RetryPolicyContext`][agents.retry.RetryPolicyContext] を受け取ります。内容は次のとおりです。 -- `attempt` と `max_retries`(試行回数に応じた判断に使用)。 -- `stream`(streamed / non-streamed で分岐可能)。 -- `error`(raw 検査用)。 -- `status_code`、`retry_after`、`error_code`、`is_network_error`、`is_timeout`、`is_abort` などの `normalized` 情報。 -- 下位モデルアダプターが retry ガイダンスを提供できる場合の `provider_advice`。 +- `attempt` と `max_retries` (試行回数を考慮した判断用) +- `stream` (ストリーミング / 非ストリーミング分岐用) +- `error` (raw 検査用) +- `normalized` 情報( `status_code` 、 `retry_after` 、 `error_code` 、 `is_network_error` 、 `is_timeout` 、 `is_abort` など) +- 基盤モデルアダプターがリトライ指針を提供できる場合の `provider_advice` -policy は次のいずれかを返せます。 +ポリシーは次のいずれかを返せます。 -- 単純な再試行判定としての `True` / `False`。 -- delay 上書きや診断理由付与を行いたい場合の [`RetryDecision`][agents.retry.RetryDecision]。 +- 単純なリトライ判断として `True` / `False` +- 遅延上書きや診断理由付与をしたい場合の [`RetryDecision`][agents.retry.RetryDecision] SDK は `retry_policies` に既製ヘルパーを提供しています。 -| ヘルパー | 振る舞い | +| ヘルパー | 動作 | | --- | --- | -| `retry_policies.never()` | 常に opt-out します。 | -| `retry_policies.provider_suggested()` | 利用可能な場合、provider の retry 推奨に従います。 | -| `retry_policies.network_error()` | 一時的な転送 / timeout 障害に一致します。 | -| `retry_policies.http_status([...])` | 選択した HTTP status code に一致します。 | -| `retry_policies.retry_after()` | retry-after ヒントがある場合のみ、その delay で再試行します。 | -| `retry_policies.any(...)` | ネスト policy のいずれかが opt-in したとき再試行します。 | -| `retry_policies.all(...)` | ネスト policy のすべてが opt-in したときのみ再試行します。 | +| `retry_policies.never()` | 常に無効化します。 | +| `retry_policies.provider_suggested()` | 利用可能な場合、プロバイダーのリトライ助言に従います。 | +| `retry_policies.network_error()` | 一時的なトランスポート / タイムアウト失敗に一致します。 | +| `retry_policies.http_status([...])` | 指定した HTTP ステータスコードに一致します。 | +| `retry_policies.retry_after()` | retry-after ヒントがある場合のみ、その遅延でリトライします。 | +| `retry_policies.any(...)` | ネストしたポリシーのいずれかが有効化したらリトライします。 | +| `retry_policies.all(...)` | ネストしたポリシーのすべてが有効化した場合のみリトライします。 | -policy を組み合わせる場合、`provider_suggested()` は最も安全な最初の構成要素です。provider が判別可能な場合、provider veto と replay-safety 承認を保持できるためです。 +ポリシーを組み合わせる場合、 `provider_suggested()` は最も安全な最初の構成要素です。プロバイダーが判別可能な場合、プロバイダー側の拒否や再実行安全性承認を維持できるためです。 ##### 安全境界 -次の障害は自動再試行されません。 +一部の失敗は自動リトライされません。 -- Abort エラー。 -- provider アドバイスが replay unsafe と判定したリクエスト。 -- 出力がすでに始まっており replay が unsafe になる streamed 実行。 +- 中断エラー。 +- プロバイダー助言で再実行が unsafe とされたリクエスト。 +- 出力開始後で再実行が unsafe になるストリーミング実行。 -`previous_response_id` または `conversation_id` を使う状態付きフォローアップリクエストも、より保守的に扱われます。これらのリクエストでは `network_error()` や `http_status([500])` のような非 provider 判定だけでは不十分です。retry policy には通常 `retry_policies.provider_suggested()` を通じた provider の replay-safe 承認を含める必要があります。 +`previous_response_id` または `conversation_id` を使う状態保持フォローアップリクエストも、より保守的に扱われます。これらでは `network_error()` や `http_status([500])` のような非プロバイダー述語だけでは不十分です。リトライポリシーには通常 `retry_policies.provider_suggested()` を通じた、プロバイダー由来の replay-safe 承認を含める必要があります。 -##### Runner とエージェントのマージ挙動 +##### ランナーとエージェントのマージ動作 -`retry` は runner レベルとエージェントレベルの `ModelSettings` 間で deep-merge されます。 +`retry` はランナーレベルとエージェントレベルの `ModelSettings` 間でディープマージされます。 -- エージェントは `retry.max_retries` のみを上書きしつつ、runner の `policy` を継承できます。 -- エージェントは `retry.backoff` の一部のみを上書きし、他の backoff フィールドは runner から維持できます。 -- `policy` は実行時限定のため、シリアライズされた `ModelSettings` は `max_retries` と `backoff` を保持し、コールバック自体は省略します。 +- エージェントは `retry.max_retries` のみ上書きし、ランナーの `policy` を継承できます。 +- エージェントは `retry.backoff` の一部のみ上書きし、兄弟 backoff フィールドをランナーから維持できます。 +- `policy` は実行時専用のため、シリアライズされた `ModelSettings` では `max_retries` と `backoff` は保持されますが、コールバック自体は除外されます。 -より完全な例は [`examples/basic/retry.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry.py) と [`examples/basic/retry_litellm.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry_litellm.py) を参照してください。 +より完全な例は [`examples/basic/retry.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry.py) と [adapter-backed retry example](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry_litellm.py) を参照してください。 -## non-OpenAI provider のトラブルシューティング +## 非 OpenAI プロバイダーのトラブルシューティング ### トレーシングクライアントエラー 401 -トレーシング関連エラーが出る場合、トレースが OpenAI サーバーへアップロードされる一方で OpenAI API key がないことが原因です。解決方法は 3 つあります。 +トレーシング関連エラーが出る場合、トレースは OpenAI サーバーへアップロードされる一方で OpenAI API キーがないことが原因です。解決策は 3 つあります。 1. トレーシングを完全に無効化する: [`set_tracing_disabled(True)`][agents.set_tracing_disabled]。 -2. トレーシング用 OpenAI key を設定する: [`set_tracing_export_api_key(...)`][agents.set_tracing_export_api_key]。この API key はトレースアップロード専用で、[platform.openai.com](https://platform.openai.com/) 由来である必要があります。 -3. non-OpenAI トレースプロセッサーを使う。[tracing docs](../tracing.md#custom-tracing-processors) を参照してください。 +2. トレーシング用 OpenAI キーを設定する: [`set_tracing_export_api_key(...)`][agents.set_tracing_export_api_key]。この API キーはトレースアップロード専用で、 [platform.openai.com](https://platform.openai.com/) 発行である必要があります。 +3. 非 OpenAI のトレースプロセッサーを使う。[tracing docs](../tracing.md#custom-tracing-processors) を参照してください。 ### Responses API サポート -SDK はデフォルトで Responses API を使いますが、多くの他 LLM provider はまだ対応していません。その結果 404 などの問題が発生することがあります。解決方法は 2 つあります。 +SDK は既定で Responses API を使いますが、他の多くの LLM プロバイダーはまだ対応していません。その結果として 404 などの問題が発生する場合があります。解決策は 2 つあります。 -1. [`set_default_openai_api("chat_completions")`][agents.set_default_openai_api] を呼び出します。これは環境変数で `OPENAI_API_KEY` と `OPENAI_BASE_URL` を設定している場合に機能します。 -2. [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] を使います。例は [こちら](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/) にあります。 +1. [`set_default_openai_api("chat_completions")`][agents.set_default_openai_api] を呼び出す。これは環境変数で `OPENAI_API_KEY` と `OPENAI_BASE_URL` を設定している場合に機能します。 +2. [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] を使う。例は [こちら](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/) にあります。 ### structured outputs サポート -一部の model provider は [structured outputs](https://platform.openai.com/docs/guides/structured-outputs) をサポートしていません。これにより、次のようなエラーが出る場合があります。 +一部のモデルプロバイダーは [structured outputs](https://platform.openai.com/docs/guides/structured-outputs) をサポートしていません。この場合、次のようなエラーになることがあります。 ``` @@ -437,24 +437,34 @@ BadRequestError: Error code: 400 - {'error': {'message': "'response_format.type' ``` -これは一部 model provider 側の制約です。JSON 出力はサポートしていても、出力に使う `json_schema` の指定を許可しません。この問題の修正に取り組んでいますが、JSON schema 出力をサポートする provider の利用を推奨します。そうでない場合、アプリは不正な JSON によって頻繁に壊れる可能性があります。 +これは一部モデルプロバイダーの制約です。 JSON 出力はサポートしていても、出力に使用する `json_schema` の指定を許可していません。現在修正に取り組んでいますが、 JSON schema 出力をサポートするプロバイダーの利用を推奨します。そうでない場合、不正な JSON によりアプリが頻繁に壊れる可能性があります。 -## provider 間でのモデル混在 +## プロバイダー間でのモデル混在 -model provider 間の機能差を把握していないとエラーになる可能性があります。たとえば OpenAI は structured outputs、マルチモーダル入力、ホスト型ファイル検索と Web 検索をサポートしますが、多くの他 provider はこれらをサポートしません。次の制約に注意してください。 +モデルプロバイダー間の機能差を把握しておく必要があります。そうしないとエラーになる可能性があります。たとえば OpenAI は structured outputs、マルチモーダル入力、ホスト型ファイル検索と Web 検索をサポートしますが、多くの他プロバイダーはこれらをサポートしません。次の制約に注意してください。 -- 未対応 provider に、未対応の `tools` を送らない -- テキスト専用モデル呼び出し前に、マルチモーダル入力を除外する -- structured JSON 出力非対応 provider は、ときどき不正な JSON を生成する点に注意する +- 非対応プロバイダーへ、対応していない `tools` を送らない +- テキスト専用モデル呼び出し前にマルチモーダル入力を除外する +- structured JSON 出力非対応プロバイダーは無効 JSON を時々生成する可能性があることを理解する -## LiteLLM +## サードパーティーアダプター -LiteLLM サポートは、non-OpenAI provider を Agents SDK ワークフローへ取り込む必要があるケース向けに、best-effort の beta として提供されています。 +SDK の組み込みプロバイダー統合ポイントで不足する場合のみ、サードパーティーアダプターを選択してください。この SDK で OpenAI モデルのみを使用する場合、 Any-LLM や LiteLLM ではなく組み込みの [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] パスを優先してください。サードパーティーアダプターは、 OpenAI モデルと非 OpenAI プロバイダーを組み合わせる必要がある場合、または組み込みパスで提供されないアダプター管理のプロバイダー対応 / ルーティングが必要な場合向けです。アダプターは SDK と上流モデルプロバイダーの間に別の互換レイヤーを追加するため、機能サポートやリクエスト意味論はプロバイダーごとに異なります。 SDK には現在、 Any-LLM と LiteLLM がベストエフォートのベータ統合として含まれています。 -この SDK で OpenAI モデルを使う場合は、LiteLLM ではなく組み込みの [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] 経路を推奨します。 +### Any-LLM -OpenAI モデルと non-OpenAI provider を組み合わせる必要があり、とくに Chat Completions 互換 API 経由で使う場合、LiteLLM は beta オプションとして利用できますが、すべての構成で最適とは限りません。 +Any-LLM サポートは、 Any-LLM 管理のプロバイダー対応またはルーティングが必要な場合向けに、ベストエフォートのベータとして含まれています。 -non-OpenAI provider で LiteLLM が必要な場合は `openai-agents[litellm]` をインストールし、[`examples/model_providers/litellm_auto.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/litellm_auto.py) または [`examples/model_providers/litellm_provider.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/litellm_provider.py) から始めてください。`litellm/...` モデル名を使うか、[`LitellmModel`][agents.extensions.models.litellm_model.LitellmModel] を直接インスタンス化できます。 +上流プロバイダーパスに応じて、 Any-LLM は Responses API、 Chat Completions 互換 API、またはプロバイダー固有互換レイヤーを使う場合があります。 -LiteLLM のレスポンスで SDK の usage metrics を埋めたい場合は、`ModelSettings(include_usage=True)` を渡してください。 \ No newline at end of file +Any-LLM が必要な場合は `openai-agents[any-llm]` をインストールし、 [`examples/model_providers/any_llm_auto.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/any_llm_auto.py) または [`examples/model_providers/any_llm_provider.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/any_llm_provider.py) から始めてください。[`MultiProvider`][agents.MultiProvider] で `any-llm/...` モデル名を使うか、 `AnyLLMModel` を直接インスタンス化するか、実行スコープで `AnyLLMProvider` を使えます。モデルサーフェスを明示固定したい場合は、 `AnyLLMModel` 構築時に `api="responses"` または `api="chat_completions"` を渡してください。 + +Any-LLM はサードパーティーアダプターレイヤーであるため、プロバイダー依存関係と機能差分は SDK ではなく Any-LLM 側で定義されます。使用量メトリクスは上流プロバイダーが返す場合に自動伝播されますが、ストリーミング Chat Completions バックエンドでは usage チャンク出力前に `ModelSettings(include_usage=True)` が必要な場合があります。structured outputs、ツール呼び出し、 usage レポート、 Responses 固有動作に依存する場合は、デプロイ予定の正確なプロバイダーバックエンドを検証してください。 + +### LiteLLM + +LiteLLM サポートは、 LiteLLM 固有のプロバイダー対応またはルーティングが必要な場合向けに、ベストエフォートのベータとして含まれています。 + +LiteLLM が必要な場合は `openai-agents[litellm]` をインストールし、 [`examples/model_providers/litellm_auto.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/litellm_auto.py) または [`examples/model_providers/litellm_provider.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/litellm_provider.py) から始めてください。 `litellm/...` モデル名を使うか、 [`LitellmModel`][agents.extensions.models.litellm_model.LitellmModel] を直接インスタンス化できます。 + +一部の LiteLLM バックエンドプロバイダーは既定で SDK usage メトリクスを埋めません。 usage レポートが必要な場合は `ModelSettings(include_usage=True)` を渡し、 structured outputs、ツール呼び出し、 usage レポート、またはアダプター固有ルーティング動作に依存する場合は、デプロイ予定の正確なプロバイダーバックエンドを検証してください。 \ No newline at end of file diff --git a/docs/ja/models/litellm.md b/docs/ja/models/litellm.md index ff56dcc7ae..c1437b4455 100644 --- a/docs/ja/models/litellm.md +++ b/docs/ja/models/litellm.md @@ -5,9 +5,9 @@ search: # LiteLLM -このページは [Models の LiteLLM セクション](index.md#litellm)に移動しました。 +このページは [Models の Third-party adapters セクション](index.md#third-party-adapters)に移動しました。 自動的にリダイレクトされない場合は、上記のリンクを使用してください。 \ No newline at end of file diff --git a/docs/ja/tracing.md b/docs/ja/tracing.md index f5bdbe3968..84930611a5 100644 --- a/docs/ja/tracing.md +++ b/docs/ja/tracing.md @@ -4,31 +4,31 @@ search: --- # トレーシング -Agents SDK には組み込みのトレーシングが含まれており、エージェント実行中のイベント( LLM 生成、ツール呼び出し、ハンドオフ、ガードレール、さらには発生したカスタムイベント)を包括的に記録します。[Traces ダッシュボード](https://platform.openai.com/traces) を使用すると、開発中および本番環境でワークフローをデバッグ、可視化、監視できます。 +Agents SDK には組み込みのトレーシングが含まれており、エージェント実行中のイベント( LLM 生成、ツール呼び出し、ハンドオフ、ガードレール、さらに発生したカスタムイベント)を包括的に記録します。[Traces ダッシュボード](https://platform.openai.com/traces) を使用すると、開発中および本番環境でワークフローをデバッグ、可視化、監視できます。 !!!note - トレーシングはデフォルトで有効です。一般的な方法として、次の 3 つで無効化できます。 + トレーシングはデフォルトで有効です。一般的には次の 3 つの方法で無効化できます。 1. 環境変数 `OPENAI_AGENTS_DISABLE_TRACING=1` を設定して、グローバルにトレーシングを無効化できます - 2. [`set_tracing_disabled(True)`][agents.set_tracing_disabled] を使って、コード内でグローバルにトレーシングを無効化できます + 2. [`set_tracing_disabled(True)`][agents.set_tracing_disabled] を使って、コード上でグローバルにトレーシングを無効化できます 3. [`agents.run.RunConfig.tracing_disabled`][] を `True` に設定して、単一の実行でトレーシングを無効化できます ***OpenAI の API を使用し、Zero Data Retention ( ZDR ) ポリシーの下で運用している組織では、トレーシングは利用できません。*** -## トレースとスパン +## Traces と spans -- **トレース** は「ワークフロー」の単一のエンドツーエンド操作を表します。スパンで構成されます。トレースには次のプロパティがあります。 - - `workflow_name`: 論理的なワークフローまたはアプリです。たとえば「Code generation」や「Customer service」です。 +- **Traces** は 1 つの「ワークフロー」のエンドツーエンドの単一操作を表します。これは Span で構成されます。Traces には次のプロパティがあります。 + - `workflow_name`: 論理的なワークフローまたはアプリです。例: 「Code generation」や「Customer service」。 - `trace_id`: トレースの一意な ID です。指定しない場合は自動生成されます。形式は `trace_<32_alphanumeric>` である必要があります。 - - `group_id`: 同じ会話からの複数のトレースを関連付けるための任意のグループ ID です。たとえば、チャットスレッド ID を使用できます。 - - `disabled`: True の場合、トレースは記録されません。 + - `group_id`: 任意のグループ ID で、同じ会話内の複数のトレースを関連付けます。たとえば、チャットスレッド ID を使用できます。 + - `disabled`: True の場合、そのトレースは記録されません。 - `metadata`: トレースの任意のメタデータです。 -- **スパン** は開始時刻と終了時刻を持つ操作を表します。スパンには次があります。 +- **Spans** は開始時刻と終了時刻を持つ操作を表します。Spans には次が含まれます。 - `started_at` と `ended_at` のタイムスタンプ。 - - `trace_id`: 属するトレースを表します - - `parent_id`: このスパンの親スパン(存在する場合)を指します - - `span_data`: スパンに関する情報です。たとえば `AgentSpanData` にはエージェントの情報、`GenerationSpanData` には LLM 生成の情報などが含まれます。 + - `trace_id`(所属するトレースを表します) + - `parent_id`(この Span の親 Span を指します。存在する場合) + - `span_data`( Span に関する情報)。たとえば、`AgentSpanData` は Agent の情報、`GenerationSpanData` は LLM 生成の情報を含みます。 ## デフォルトトレーシング @@ -42,15 +42,15 @@ Agents SDK には組み込みのトレーシングが含まれており、エー - ハンドオフは `handoff_span()` でラップされます - 音声入力( speech-to-text )は `transcription_span()` でラップされます - 音声出力( text-to-speech )は `speech_span()` でラップされます -- 関連する音声スパンは `speech_group_span()` の子になる場合があります +- 関連する音声 Span は `speech_group_span()` の配下になる場合があります -デフォルトで、トレース名は「Agent workflow」です。`trace` を使用する場合はこの名前を設定できます。また、[`RunConfig`][agents.run.RunConfig] で名前や他のプロパティを設定することもできます。 +デフォルトで、トレース名は「Agent workflow」です。`trace` を使用する場合はこの名前を設定できます。また、[`RunConfig`][agents.run.RunConfig] で名前やその他のプロパティを設定することもできます。 -さらに、[カスタムトレースプロセッサー](#custom-tracing-processors) を設定して、トレースを他の送信先へプッシュできます(置き換えまたは二次送信先として)。 +さらに、[カスタムトレースプロセッサー](#custom-tracing-processors) を設定して、トレースを他の送信先にプッシュできます(置き換えまたは副次的な送信先として)。 ## 上位レベルトレース -場合によっては、`run()` への複数回の呼び出しを 1 つのトレースに含めたいことがあります。これを行うには、コード全体を `trace()` でラップします。 +場合によっては、`run()` の複数回呼び出しを 1 つのトレースの一部にしたいことがあります。これは、コード全体を `trace()` でラップすることで実現できます。 ```python from agents import Agent, Runner, trace @@ -65,60 +65,60 @@ async def main(): print(f"Rating: {second_result.final_output}") ``` -1. `Runner.run` への 2 回の呼び出しが `with trace()` でラップされているため、個々の実行は 2 つのトレースを作成するのではなく、全体のトレースの一部になります。 +1. `Runner.run` の 2 回の呼び出しは `with trace()` でラップされているため、2 つのトレースを作成するのではなく、個々の実行が全体トレースの一部になります。 -## トレースの作成 +## トレース作成 [`trace()`][agents.tracing.trace] 関数を使用してトレースを作成できます。トレースは開始と終了が必要です。方法は 2 つあります。 -1. **推奨**: コンテキストマネージャーとしてトレースを使用します。つまり `with trace(...) as my_trace` です。これにより、適切なタイミングでトレースが自動的に開始・終了されます。 +1. **推奨**: トレースをコンテキストマネージャーとして使います(例: `with trace(...) as my_trace`)。これにより、適切なタイミングでトレースが自動的に開始・終了されます。 2. [`trace.start()`][agents.tracing.Trace.start] と [`trace.finish()`][agents.tracing.Trace.finish] を手動で呼び出すこともできます。 -現在のトレースは Python の [`contextvar`](https://docs.python.org/3/library/contextvars.html) を介して追跡されます。これは並行処理でも自動的に機能することを意味します。トレースを手動で開始 / 終了する場合は、現在のトレースを更新するために `start()` / `finish()` に `mark_as_current` と `reset_current` を渡す必要があります。 +現在のトレースは Python の [`contextvar`](https://docs.python.org/3/library/contextvars.html) を通じて追跡されます。これは並行処理でも自動的に機能することを意味します。トレースを手動で開始・終了する場合は、現在のトレースを更新するために `start()`/`finish()` に `mark_as_current` と `reset_current` を渡す必要があります。 -## スパンの作成 +## Span 作成 -さまざまな [`*_span()`][agents.tracing.create] メソッドを使ってスパンを作成できます。一般に、スパンを手動で作成する必要はありません。カスタムスパン情報を追跡するために [`custom_span()`][agents.tracing.custom_span] 関数が利用できます。 +さまざまな [`*_span()`][agents.tracing.create] メソッドを使って Span を作成できます。一般に、Span を手動で作成する必要はありません。カスタム Span 情報を追跡するための [`custom_span()`][agents.tracing.custom_span] 関数も利用できます。 -スパンは自動的に現在のトレースの一部となり、最も近い現在のスパンの下にネストされます。これは Python の [`contextvar`](https://docs.python.org/3/library/contextvars.html) で追跡されます。 +Span は自動的に現在のトレースの一部となり、最も近い現在の Span の配下にネストされます。これは Python の [`contextvar`](https://docs.python.org/3/library/contextvars.html) で追跡されます。 -## 機密データ +## 機微データ -一部のスパンは、機密性のある可能性があるデータを取得する場合があります。 +一部の Span では、機微データが含まれる可能性があります。 -`generation_span()` は LLM 生成の入力 / 出力を保存し、`function_span()` は関数呼び出しの入力 / 出力を保存します。これらには機密データが含まれる可能性があるため、[`RunConfig.trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data] でそのデータの取得を無効化できます。 +`generation_span()` は LLM 生成の入出力を保存し、`function_span()` は関数呼び出しの入出力を保存します。これらには機微データが含まれる可能性があるため、[`RunConfig.trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data] でそのデータの収集を無効化できます。 -同様に、音声スパンにはデフォルトで入力 / 出力音声の base64 エンコードされた PCM データが含まれます。[`VoicePipelineConfig.trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data] を設定することで、この音声データの取得を無効化できます。 +同様に、Audio Span にはデフォルトで入力・出力音声の base64 エンコードされた PCM データが含まれます。[`VoicePipelineConfig.trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data] を設定することで、この音声データの収集を無効化できます。 -デフォルトで `trace_include_sensitive_data` は `True` です。コードを変更せずにデフォルトを設定するには、アプリ実行前に環境変数 `OPENAI_AGENTS_TRACE_INCLUDE_SENSITIVE_DATA` を `true/1` または `false/0` として export します。 +デフォルトでは、`trace_include_sensitive_data` は `True` です。アプリ実行前に環境変数 `OPENAI_AGENTS_TRACE_INCLUDE_SENSITIVE_DATA` を `true/1` または `false/0` に設定することで、コードなしでデフォルト値を設定できます。 ## カスタムトレーシングプロセッサー トレーシングの高レベルアーキテクチャは次のとおりです。 -- 初期化時に、トレース作成を担当するグローバルな [`TraceProvider`][agents.tracing.setup.TraceProvider] を作成します。 -- `TraceProvider` に [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor] を設定し、トレース / スパンをバッチで [`BackendSpanExporter`][agents.tracing.processors.BackendSpanExporter] に送信します。これはスパンとトレースをバッチで OpenAI バックエンドへエクスポートします。 +- 初期化時に、トレース作成を担うグローバル [`TraceProvider`][agents.tracing.setup.TraceProvider] を作成します。 +- `TraceProvider` に [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor] を設定します。これはトレース / Span をバッチで [`BackendSpanExporter`][agents.tracing.processors.BackendSpanExporter] に送信し、さらに Span とトレースを OpenAI バックエンドへバッチでエクスポートします。 -このデフォルト設定をカスタマイズし、代替または追加バックエンドへトレースを送信したり、エクスポーターの動作を変更したりするには、次の 2 つの方法があります。 +このデフォルト設定をカスタマイズして、別のまたは追加のバックエンドにトレースを送信したり、エクスポーターの動作を変更したりするには、次の 2 つの方法があります。 -1. [`add_trace_processor()`][agents.tracing.add_trace_processor] は、準備完了時にトレースとスパンを受け取る**追加**のトレースプロセッサーを追加できます。これにより、OpenAI バックエンドへの送信に加えて独自処理を実行できます。 -2. [`set_trace_processors()`][agents.tracing.set_trace_processors] は、デフォルトプロセッサーを独自のトレースプロセッサーで**置き換え**できます。これは、そうする `TracingProcessor` を含めない限り、トレースが OpenAI バックエンドへ送信されないことを意味します。 +1. [`add_trace_processor()`][agents.tracing.add_trace_processor] を使うと、準備できたトレースと Span を受け取る**追加の**トレースプロセッサーを追加できます。これにより、OpenAI バックエンドへの送信に加えて独自の処理を行えます。 +2. [`set_trace_processors()`][agents.tracing.set_trace_processors] を使うと、デフォルトプロセッサーを独自のトレースプロセッサーで**置き換え**できます。これは、`TracingProcessor` を含めない限り、トレースが OpenAI バックエンドへ送信されないことを意味します。 ## 非 OpenAI モデルでのトレーシング -OpenAI API キーを非 OpenAI モデルと共に使用して、トレーシングを無効化することなく OpenAI Traces ダッシュボードで無料トレーシングを有効化できます。 +トレーシングを無効化しなくても、OpenAI 以外のモデルで OpenAI API キーを使用して OpenAI Traces ダッシュボードで無料トレーシングを有効化できます。アダプター選択とセットアップ時の注意点については、Models ガイドの [Third-party adapters](models/index.md#third-party-adapters) セクションを参照してください。 ```python import os from agents import set_tracing_export_api_key, Agent, Runner -from agents.extensions.models.litellm_model import LitellmModel +from agents.extensions.models.any_llm_model import AnyLLMModel tracing_api_key = os.environ["OPENAI_API_KEY"] set_tracing_export_api_key(tracing_api_key) -model = LitellmModel( - model="your-model-name", +model = AnyLLMModel( + model="your-provider/your-model-name", api_key="your-api-key", ) @@ -128,7 +128,7 @@ agent = Agent( ) ``` -単一の実行でのみ別のトレーシングキーが必要な場合は、グローバルエクスポーターを変更する代わりに `RunConfig` 経由で渡してください。 +単一の実行にのみ別のトレーシングキーが必要な場合は、グローバルエクスポーターを変更する代わりに `RunConfig` 経由で渡してください。 ```python from agents import Runner, RunConfig @@ -140,13 +140,13 @@ await Runner.run( ) ``` -## 追加の注意事項 -- Openai Traces ダッシュボードで無料トレースを確認します。 +## 追加メモ +- Openai Traces ダッシュボードで無料トレースを表示します。 -## エコシステム連携 +## エコシステム統合 -次のコミュニティおよびベンダー連携は、OpenAI Agents SDK のトレーシングサーフェスをサポートしています。 +以下のコミュニティおよびベンダーの統合は、OpenAI Agents SDK のトレーシング機能をサポートしています。 ### 外部トレーシングプロセッサー一覧 diff --git a/docs/ja/usage.md b/docs/ja/usage.md index 1c1e1d5328..28cccd5512 100644 --- a/docs/ja/usage.md +++ b/docs/ja/usage.md @@ -4,22 +4,22 @@ search: --- # 使用方法 -Agents SDK は、実行ごとのトークン使用量を自動的に追跡します。実行コンテキストからアクセスでき、コスト監視、制限の適用、分析記録に利用できます。 +Agents SDK は、すべての実行についてトークン使用量を自動的に追跡します。実行コンテキストからこれにアクセスし、コストの監視、制限の適用、または分析の記録に使用できます。 ## 追跡対象 - **requests**: 実行された LLM API 呼び出し回数 -- **input_tokens**: 送信された入力トークン総数 -- **output_tokens**: 受信した出力トークン総数 +- **input_tokens**: 送信された入力トークンの合計 +- **output_tokens**: 受信した出力トークンの合計 - **total_tokens**: 入力 + 出力 - **request_usage_entries**: リクエストごとの使用量内訳の一覧 - **details**: - `input_tokens_details.cached_tokens` - `output_tokens_details.reasoning_tokens` -## 実行からの使用量へのアクセス +## 実行からの使用量アクセス -`Runner.run(...)` の後、`result.context_wrapper.usage` で使用量にアクセスします。 +`Runner.run(...)` の後、`result.context_wrapper.usage` 経由で使用量にアクセスします。 ```python result = await Runner.run(agent, "What's the weather in Tokyo?") @@ -31,29 +31,20 @@ print("Output tokens:", usage.output_tokens) print("Total tokens:", usage.total_tokens) ``` -使用量は、実行中のすべてのモデル呼び出し(ツール呼び出しとハンドオフを含む)で集計されます。 +使用量は、実行中のすべてのモデル呼び出し(ツール呼び出しとハンドオフを含む)にわたって集計されます。 -### LiteLLM モデルでの使用量の有効化 +### サードパーティアダプターでの使用量有効化 -LiteLLM プロバイダーは、デフォルトでは使用量メトリクスを報告しません。[`LitellmModel`][agents.extensions.models.litellm_model.LitellmModel] を使用している場合は、LiteLLM のレスポンスが `result.context_wrapper.usage` を埋めるよう、エージェントに `ModelSettings(include_usage=True)` を渡してください。設定手順とコード例については、Models ガイドの [LiteLLM note](models/index.md#litellm) を参照してください。 +使用量レポートは、サードパーティアダプターおよびプロバイダーバックエンドによって異なります。アダプター経由のモデルに依存し、正確な `result.context_wrapper.usage` の値が必要な場合: -```python -from agents import Agent, ModelSettings, Runner -from agents.extensions.models.litellm_model import LitellmModel - -agent = Agent( - name="Assistant", - model=LitellmModel(model="your/model", api_key="..."), - model_settings=ModelSettings(include_usage=True), -) +- `AnyLLMModel` では、上流プロバイダーが使用量を返すと自動的に伝播されます。ストリーミング Chat Completions バックエンドでは、使用量チャンクが出力される前に `ModelSettings(include_usage=True)` が必要な場合があります。 +- `LitellmModel` では、一部のプロバイダーバックエンドは既定で使用量をレポートしないため、`ModelSettings(include_usage=True)` が必要になることがよくあります。 -result = await Runner.run(agent, "What's the weather in Tokyo?") -print(result.context_wrapper.usage.total_tokens) -``` +Models ガイドの [Third-party adapters](models/index.md#third-party-adapters) セクションにあるアダプター固有の注意事項を確認し、デプロイ予定の正確なプロバイダーバックエンドを検証してください。 ## リクエストごとの使用量追跡 -SDK は、`request_usage_entries` 内の API リクエストごとの使用量を自動追跡します。これは詳細なコスト計算やコンテキストウィンドウ消費量の監視に有用です。 +SDK は、各 API リクエストの使用量を `request_usage_entries` で自動追跡します。これは、詳細なコスト計算やコンテキストウィンドウ消費の監視に役立ちます。 ```python result = await Runner.run(agent, "What's the weather in Tokyo?") @@ -62,9 +53,9 @@ for i, request in enumerate(result.context_wrapper.usage.request_usage_entries): print(f"Request {i + 1}: {request.input_tokens} in, {request.output_tokens} out") ``` -## セッションでの使用量へのアクセス +## セッションでの使用量アクセス -`Session`(例: `SQLiteSession`)を使用する場合、`Runner.run(...)` の各呼び出しはその特定の実行に対する使用量を返します。セッションは文脈のために会話履歴を維持しますが、各実行の使用量は独立しています。 +`Session`(例: `SQLiteSession`)を使用する場合、`Runner.run(...)` の各呼び出しは、その特定の実行の使用量を返します。セッションはコンテキスト用に会話履歴を維持しますが、各実行の使用量は独立しています。 ```python session = SQLiteSession("my_conversation") @@ -76,11 +67,11 @@ second = await Runner.run(agent, "Can you elaborate?", session=session) print(second.context_wrapper.usage.total_tokens) # Usage for second run ``` -セッションは実行間で会話コンテキストを保持しますが、各 `Runner.run()` 呼び出しで返される使用量メトリクスは、その特定の実行のみを表します。セッションでは、前のメッセージが各実行の入力として再投入される場合があり、その結果、後続ターンの入力トークン数に影響します。 +セッションは実行間で会話コンテキストを保持しますが、各 `Runner.run()` 呼び出しで返される使用量メトリクスは、その特定の実行のみを表す点に注意してください。セッションでは、前のメッセージが各実行の入力として再投入される場合があり、これが後続ターンの入力トークン数に影響します。 -## フックでの使用量の利用 +## フックでの使用量活用 -`RunHooks` を使用している場合、各フックに渡される `context` オブジェクトには `usage` が含まれます。これにより、ライフサイクルの重要なタイミングで使用量を記録できます。 +`RunHooks` を使用している場合、各フックに渡される `context` オブジェクトには `usage` が含まれます。これにより、ライフサイクルの重要なタイミングで使用量をログ記録できます。 ```python class MyHooks(RunHooks): @@ -91,7 +82,7 @@ class MyHooks(RunHooks): ## API リファレンス -詳細な API ドキュメントは次を参照してください。 +詳細な API ドキュメントは以下を参照してください。 - [`Usage`][agents.usage.Usage] - 使用量追跡データ構造 - [`RequestUsage`][agents.usage.RequestUsage] - リクエストごとの使用量詳細 diff --git a/docs/ko/examples.md b/docs/ko/examples.md index 00fcb001d5..9d3d59cee4 100644 --- a/docs/ko/examples.md +++ b/docs/ko/examples.md @@ -2,38 +2,38 @@ search: exclude: true --- -# 예제 +# 코드 예제 -[repo](https://github.com/openai/openai-agents-python/tree/main/examples)의 examples 섹션에서 SDK의 다양한 샘플 구현을 확인해 보세요. examples는 서로 다른 패턴과 기능을 보여 주는 여러 카테고리로 구성되어 있습니다 +[repo](https://github.com/openai/openai-agents-python/tree/main/examples)의 examples 섹션에서 SDK의 다양한 샘플 구현을 확인해 보세요. examples는 다양한 패턴과 기능을 보여주는 여러 카테고리로 구성되어 있습니다. ## 카테고리 - **[agent_patterns](https://github.com/openai/openai-agents-python/tree/main/examples/agent_patterns):** - 이 카테고리의 예제는 다음과 같은 일반적인 에이전트 설계 패턴을 보여 줍니다 + 이 카테고리의 예제는 다음과 같은 일반적인 에이전트 설계 패턴을 보여줍니다 - 결정론적 워크플로 - Agents as tools - 병렬 에이전트 실행 - 조건부 도구 사용 - 입력/출력 가드레일 - - 심판으로서의 LLM + - 심사자로서의 LLM - 라우팅 - 스트리밍 가드레일 - - 승인 흐름을 위한 사용자 지정 거부 메시지 (`examples/agent_patterns/human_in_the_loop_custom_rejection.py`) + - 승인 흐름을 위한 사용자 지정 거절 메시지 (`examples/agent_patterns/human_in_the_loop_custom_rejection.py`) - **[basic](https://github.com/openai/openai-agents-python/tree/main/examples/basic):** - 이 예제들은 다음과 같은 SDK의 핵심 기능을 보여 줍니다 + 이 예제들은 다음과 같은 SDK의 기본 기능을 보여줍니다 - - Hello world 예제(Default model, GPT-5, open-weight model) - - 에이전트 수명 주기 관리 + - Hello World 예제 (기본 모델, GPT-5, 오픈 가중치 모델) + - 에이전트 라이프사이클 관리 - 동적 시스템 프롬프트 - - 스트리밍 출력(텍스트, 항목, 함수 호출 인수) + - 스트리밍 출력 (텍스트, 항목, 함수 호출 인수) - 턴 간 공유 세션 헬퍼를 사용하는 Responses websocket 전송 (`examples/basic/stream_ws.py`) - 프롬프트 템플릿 - - 파일 처리(로컬 및 원격, 이미지 및 PDF) + - 파일 처리 (로컬 및 원격, 이미지 및 PDF) - 사용량 추적 - Runner 관리 재시도 설정 (`examples/basic/retry.py`) - - LiteLLM을 사용한 Runner 관리 재시도 (`examples/basic/retry_litellm.py`) + - 서드파티 어댑터를 통한 Runner 관리 재시도 (`examples/basic/retry_litellm.py`) - 비엄격 출력 타입 - 이전 응답 ID 사용 @@ -41,41 +41,41 @@ search: 항공사를 위한 고객 서비스 시스템 예제입니다 - **[financial_research_agent](https://github.com/openai/openai-agents-python/tree/main/examples/financial_research_agent):** - 금융 데이터 분석을 위한 에이전트와 도구를 사용해 구조화된 리서치 워크플로를 보여 주는 금융 리서치 에이전트입니다 + 금융 데이터 분석을 위해 에이전트와 도구를 활용한 구조화된 리서치 워크플로를 보여주는 금융 리서치 에이전트입니다 - **[handoffs](https://github.com/openai/openai-agents-python/tree/main/examples/handoffs):** - 메시지 필터링이 포함된 에이전트 핸드오프의 실용적인 예제를 확인해 보세요 + 메시지 필터링을 사용하는 에이전트 핸드오프의 실용적인 예제를 확인하세요 - **[hosted_mcp](https://github.com/openai/openai-agents-python/tree/main/examples/hosted_mcp):** - 호스티드 MCP(Model Context Protocol) 커넥터와 승인 사용 방법을 보여 주는 예제입니다 + 호스티드 MCP (Model context protocol) 커넥터와 승인 사용 방법을 보여주는 예제입니다 - **[mcp](https://github.com/openai/openai-agents-python/tree/main/examples/mcp):** - 다음을 포함해 MCP(Model Context Protocol)로 에이전트를 구축하는 방법을 알아보세요 + 다음을 포함해 MCP (Model context protocol)로 에이전트를 구축하는 방법을 알아보세요 - - 파일시스템 예제 + - 파일 시스템 예제 - Git 예제 - MCP 프롬프트 서버 예제 - - SSE(Server-Sent Events) 예제 + - SSE (Server-Sent Events) 예제 - 스트리밍 가능한 HTTP 예제 - **[memory](https://github.com/openai/openai-agents-python/tree/main/examples/memory):** - 다음을 포함한 에이전트용 다양한 메모리 구현 예제입니다 - - - SQLite 세션 스토리지 - - 고급 SQLite 세션 스토리지 - - Redis 세션 스토리지 - - SQLAlchemy 세션 스토리지 - - Dapr 상태 저장소 세션 스토리지 - - 암호화된 세션 스토리지 - - OpenAI Conversations 세션 스토리지 - - Responses 압축 세션 스토리지 - - `ModelSettings(store=False)`를 사용하는 무상태 Responses 압축 (`examples/memory/compaction_session_stateless_example.py`) + 다음을 포함한 에이전트를 위한 다양한 메모리 구현 예제입니다 + + - SQLite 세션 저장소 + - 고급 SQLite 세션 저장소 + - Redis 세션 저장소 + - SQLAlchemy 세션 저장소 + - Dapr 상태 저장소 세션 저장소 + - 암호화된 세션 저장소 + - OpenAI Conversations 세션 저장소 + - Responses 컴팩션 세션 저장소 + - `ModelSettings(store=False)`를 사용한 상태 비저장 Responses 컴팩션 (`examples/memory/compaction_session_stateless_example.py`) - **[model_providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers):** - 사용자 지정 provider와 LiteLLM 통합을 포함해 SDK에서 OpenAI 이외 모델을 사용하는 방법을 살펴보세요 + 사용자 지정 제공자와 서드파티 어댑터를 포함해 SDK에서 OpenAI가 아닌 모델을 사용하는 방법을 살펴보세요 - **[realtime](https://github.com/openai/openai-agents-python/tree/main/examples/realtime):** - 다음을 포함해 SDK를 사용해 실시간 경험을 구축하는 방법을 보여 주는 예제입니다 + 다음을 포함해 SDK를 사용해 실시간 경험을 구축하는 방법을 보여주는 예제입니다 - 구조화된 텍스트 및 이미지 메시지를 사용하는 웹 애플리케이션 패턴 - 명령줄 오디오 루프 및 재생 처리 @@ -83,25 +83,25 @@ search: - Realtime Calls API attach 흐름을 사용하는 Twilio SIP 통합 - **[reasoning_content](https://github.com/openai/openai-agents-python/tree/main/examples/reasoning_content):** - 추론 콘텐츠 및 structured outputs를 다루는 방법을 보여 주는 예제입니다 + reasoning content 및 structured outputs를 다루는 방법을 보여주는 예제입니다 - **[research_bot](https://github.com/openai/openai-agents-python/tree/main/examples/research_bot):** - 복잡한 멀티 에이전트 리서치 워크플로를 보여 주는 간단한 딥 리서치 클론입니다 + 복잡한 멀티 에이전트 리서치 워크플로를 보여주는 간단한 딥 리서치 클론입니다 - **[tools](https://github.com/openai/openai-agents-python/tree/main/examples/tools):** - 다음과 같은 OpenAI 호스트하는 도구와 실험적 Codex 툴링을 구현하는 방법을 알아보세요 + 다음과 같은 OpenAI 호스트하는 도구 및 실험적 Codex 도구 기능을 구현하는 방법을 알아보세요 - - 웹 검색 및 필터를 사용한 웹 검색 + - 웹 검색 및 필터가 있는 웹 검색 - 파일 검색 - 코드 인터프리터 - - 인라인 스킬이 있는 호스티드 컨테이너 셸 (`examples/tools/container_shell_inline_skill.py`) - - 스킬 참조가 있는 호스티드 컨테이너 셸 (`examples/tools/container_shell_skill_reference.py`) - - 로컬 스킬이 있는 로컬 셸 (`examples/tools/local_shell_skill.py`) - - 네임스페이스 및 지연 도구를 사용한 도구 검색 (`examples/tools/tool_search.py`) + - 인라인 스킬을 사용하는 호스티드 컨테이너 셸 (`examples/tools/container_shell_inline_skill.py`) + - 스킬 참조를 사용하는 호스티드 컨테이너 셸 (`examples/tools/container_shell_skill_reference.py`) + - 로컬 스킬을 사용하는 로컬 셸 (`examples/tools/local_shell_skill.py`) + - 네임스페이스와 지연 도구를 사용하는 도구 검색 (`examples/tools/tool_search.py`) - 컴퓨터 사용 - 이미지 생성 - 실험적 Codex 도구 워크플로 (`examples/tools/codex.py`) - 실험적 Codex 동일 스레드 워크플로 (`examples/tools/codex_same_thread.py`) - **[voice](https://github.com/openai/openai-agents-python/tree/main/examples/voice):** - 스트리밍 음성 예제를 포함해, TTS 및 STT 모델을 사용하는 음성 에이전트 예제를 확인해 보세요 \ No newline at end of file + 스트리밍 음성 예제를 포함해 TTS 및 STT 모델을 사용하는 음성 에이전트 예제를 확인하세요 \ No newline at end of file diff --git a/docs/ko/models/index.md b/docs/ko/models/index.md index 24013778b1..6e932e5bb0 100644 --- a/docs/ko/models/index.md +++ b/docs/ko/models/index.md @@ -4,42 +4,42 @@ search: --- # 모델 -Agents SDK 는 OpenAI 모델을 즉시 사용할 수 있도록 두 가지 방식으로 지원합니다: +Agents SDK 는 즉시 사용할 수 있는 OpenAI 모델 지원을 두 가지 방식으로 제공합니다: -- **권장**: 새 [Responses API](https://platform.openai.com/docs/api-reference/responses)를 사용해 OpenAI API 를 호출하는 [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] +- **권장**: 새로운 [Responses API](https://platform.openai.com/docs/api-reference/responses)를 사용해 OpenAI API 를 호출하는 [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] - [Chat Completions API](https://platform.openai.com/docs/api-reference/chat)를 사용해 OpenAI API 를 호출하는 [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] ## 모델 설정 선택 -사용 환경에 맞는 가장 단순한 경로부터 시작하세요: +사용 중인 설정에 맞는 가장 단순한 경로부터 시작하세요: -| 다음을 하려는 경우 | 권장 경로 | 자세히 보기 | +| 다음을 하려는 경우 | 권장 경로 | 더 읽기 | | --- | --- | --- | | OpenAI 모델만 사용 | 기본 OpenAI provider 와 Responses 모델 경로 사용 | [OpenAI 모델](#openai-models) | | websocket 전송으로 OpenAI Responses API 사용 | Responses 모델 경로를 유지하고 websocket 전송 활성화 | [Responses WebSocket 전송](#responses-websocket-transport) | -| OpenAI 가 아닌 provider 하나 사용 | 내장 provider 통합 지점으로 시작 | [OpenAI 가 아닌 모델](#non-openai-models) | -| 에이전트 전반에서 모델 또는 provider 혼합 | 실행별 또는 에이전트별로 provider 선택 후 기능 차이 검토 | [하나의 워크플로에서 모델 혼합](#mixing-models-in-one-workflow) 및 [provider 간 모델 혼합](#mixing-models-across-providers) | +| OpenAI 가 아닌 provider 하나 사용 | 내장된 provider 통합 지점부터 시작 | [OpenAI 가 아닌 모델](#non-openai-models) | +| 에이전트 간에 모델 또는 provider 혼합 | 실행 단위 또는 에이전트 단위로 provider 선택 후 기능 차이 검토 | [하나의 워크플로에서 모델 혼합](#mixing-models-in-one-workflow) 및 [provider 간 모델 혼합](#mixing-models-across-providers) | | 고급 OpenAI Responses 요청 설정 조정 | OpenAI Responses 경로에서 `ModelSettings` 사용 | [고급 OpenAI Responses 설정](#advanced-openai-responses-settings) | -| OpenAI 가 아닌 Chat Completions provider 에 LiteLLM 사용 | LiteLLM 을 베타 대체 옵션으로 사용 | [LiteLLM](#litellm) | +| OpenAI 가 아닌 경로 또는 혼합 provider 라우팅에 서드파티 adapter 사용 | 지원되는 베타 adapter 비교 후 배포할 provider 경로 검증 | [서드파티 adapter](#third-party-adapters) | ## OpenAI 모델 -대부분의 OpenAI 전용 앱에서는 기본 OpenAI provider 와 문자열 모델 이름을 사용하고, Responses 모델 경로를 유지하는 것을 권장합니다. +대부분의 OpenAI 전용 앱에서는 기본 OpenAI provider 와 문자열 모델 이름을 사용하고, Responses 모델 경로를 유지하는 것을 권장합니다 -`Agent` 를 초기화할 때 모델을 지정하지 않으면 기본 모델이 사용됩니다. 현재 기본값은 호환성과 낮은 지연 시간을 위해 [`gpt-4.1`](https://developers.openai.com/api/docs/models/gpt-4.1)입니다. 접근 권한이 있다면, 명시적인 `model_settings` 를 유지하면서 더 높은 품질을 위해 에이전트를 [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4)로 설정하는 것을 권장합니다. +`Agent` 를 초기화할 때 모델을 지정하지 않으면 기본 모델이 사용됩니다. 현재 기본값은 호환성과 낮은 지연 시간을 위해 [`gpt-4.1`](https://developers.openai.com/api/docs/models/gpt-4.1)입니다. 사용 권한이 있다면, 명시적인 `model_settings` 를 유지하면서 더 높은 품질을 위해 에이전트를 [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4)로 설정하는 것을 권장합니다 -[`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4) 같은 다른 모델로 전환하려면 에이전트를 구성하는 방법이 두 가지 있습니다. +[`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4) 같은 다른 모델로 전환하려면 에이전트를 구성하는 방법이 두 가지 있습니다 ### 기본 모델 -첫째, 사용자 지정 모델을 설정하지 않은 모든 에이전트에서 특정 모델을 일관되게 사용하려면, 에이전트를 실행하기 전에 `OPENAI_DEFAULT_MODEL` 환경 변수를 설정하세요. +첫째, 사용자 지정 모델을 설정하지 않는 모든 에이전트에서 일관되게 특정 모델을 사용하려면 에이전트를 실행하기 전에 `OPENAI_DEFAULT_MODEL` 환경 변수를 설정하세요 ```bash export OPENAI_DEFAULT_MODEL=gpt-5.4 python3 my_awesome_agent.py ``` -둘째, `RunConfig` 를 통해 실행 단위 기본 모델을 설정할 수 있습니다. 에이전트에 모델을 설정하지 않으면 이 실행의 모델이 사용됩니다. +둘째, `RunConfig` 를 통해 실행 단위 기본 모델을 설정할 수 있습니다. 에이전트에 모델을 설정하지 않으면 이 실행의 모델이 사용됩니다 ```python from agents import Agent, RunConfig, Runner @@ -58,7 +58,7 @@ result = await Runner.run( #### GPT-5 모델 -이 방식으로 [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4) 같은 GPT-5 모델을 사용하면 SDK 가 기본 `ModelSettings` 를 적용합니다. 대부분의 사용 사례에서 가장 잘 작동하는 값을 설정합니다. 기본 모델의 reasoning effort 를 조정하려면 자체 `ModelSettings` 를 전달하세요: +이 방식으로 [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4) 같은 GPT-5 모델을 사용하면 SDK 가 기본 `ModelSettings` 를 적용합니다. 대부분의 사용 사례에서 가장 잘 동작하는 값이 설정됩니다. 기본 모델의 reasoning effort 를 조정하려면 사용자 정의 `ModelSettings` 를 전달하세요: ```python from openai.types.shared import Reasoning @@ -74,21 +74,21 @@ my_agent = Agent( ) ``` -더 낮은 지연 시간을 위해 `gpt-5.4` 에서 `reasoning.effort="none"` 사용을 권장합니다. gpt-4.1 계열( mini 및 nano 변형 포함)도 인터랙티브 에이전트 앱 구축에 여전히 좋은 선택입니다. +더 낮은 지연 시간을 위해 `gpt-5.4` 에서 `reasoning.effort="none"` 사용을 권장합니다. gpt-4.1 계열( mini 및 nano 변형 포함)도 대화형 에이전트 앱 구축에 여전히 좋은 선택입니다 #### ComputerTool 모델 선택 -에이전트가 [`ComputerTool`][agents.tool.ComputerTool] 을 포함하면, 실제 Responses 요청에서의 유효 모델이 SDK 가 어떤 컴퓨터 도구 페이로드를 보내는지 결정합니다. 명시적 `gpt-5.4` 요청은 GA 내장 `computer` 도구를 사용하고, 명시적 `computer-use-preview` 요청은 기존 `computer_use_preview` 페이로드를 유지합니다. +에이전트에 [`ComputerTool`][agents.tool.ComputerTool] 이 포함된 경우, 실제 Responses 요청에서의 유효 모델이 SDK 가 전송하는 컴퓨터 도구 payload 를 결정합니다. 명시적인 `gpt-5.4` 요청은 GA 내장 `computer` 도구를 사용하고, 명시적인 `computer-use-preview` 요청은 이전 `computer_use_preview` payload 를 유지합니다 -주요 예외는 프롬프트 관리 호출입니다. 프롬프트 템플릿이 모델을 소유하고 SDK 가 요청에서 `model` 을 생략하면, SDK 는 프롬프트가 고정한 모델을 추측하지 않기 위해 preview 호환 컴퓨터 페이로드를 기본값으로 사용합니다. 이 흐름에서 GA 경로를 유지하려면 요청에서 `model="gpt-5.4"` 를 명시하거나, `ModelSettings(tool_choice="computer")` 또는 `ModelSettings(tool_choice="computer_use")` 로 GA 선택기를 강제하세요. +주요 예외는 프롬프트 관리 호출입니다. 프롬프트 템플릿이 모델을 소유하고 SDK 가 요청에서 `model` 을 생략하는 경우, SDK 는 프롬프트가 어떤 모델에 고정됐는지 추측하지 않기 위해 preview 호환 컴퓨터 payload 를 기본으로 사용합니다. 이 흐름에서 GA 경로를 유지하려면 요청에 `model="gpt-5.4"` 를 명시하거나 `ModelSettings(tool_choice="computer")` 또는 `ModelSettings(tool_choice="computer_use")` 로 GA 선택자를 강제하세요 -등록된 [`ComputerTool`][agents.tool.ComputerTool] 이 있으면 `tool_choice="computer"`, `"computer_use"`, `"computer_use_preview"` 는 유효 요청 모델과 일치하는 내장 선택기로 정규화됩니다. `ComputerTool` 이 등록되지 않은 경우, 이러한 문자열은 일반 함수 이름처럼 계속 동작합니다. +등록된 [`ComputerTool`][agents.tool.ComputerTool] 이 있는 경우 `tool_choice="computer"`, `"computer_use"`, `"computer_use_preview"` 는 유효 요청 모델과 일치하는 내장 선택자로 정규화됩니다. `ComputerTool` 이 등록되지 않은 경우 이 문자열은 일반 함수 이름처럼 계속 동작합니다 -preview 호환 요청은 `environment` 및 디스플레이 크기를 먼저 직렬화해야 하므로, [`ComputerProvider`][agents.tool.ComputerProvider] 팩토리를 사용하는 프롬프트 관리 흐름에서는 구체적인 `Computer` 또는 `AsyncComputer` 인스턴스를 전달하거나 요청 전 GA 선택기를 강제해야 합니다. 전체 마이그레이션 세부 사항은 [도구](../tools.md#computertool-and-the-responses-computer-tool)를 참고하세요. +preview 호환 요청은 `environment` 와 디스플레이 크기를 미리 직렬화해야 하므로 [`ComputerProvider`][agents.tool.ComputerProvider] 팩토리를 사용하는 프롬프트 관리 흐름에서는 구체적인 `Computer` 또는 `AsyncComputer` 인스턴스를 전달하거나 요청 전송 전에 GA 선택자를 강제해야 합니다. 전체 마이그레이션 세부 사항은 [Tools](../tools.md#computertool-and-the-responses-computer-tool)를 참조하세요 #### GPT-5 가 아닌 모델 -사용자 지정 `model_settings` 없이 GPT-5 가 아닌 모델 이름을 전달하면 SDK 는 모든 모델과 호환되는 일반 `ModelSettings` 로 되돌아갑니다. +사용자 지정 `model_settings` 없이 GPT-5 가 아닌 모델 이름을 전달하면 SDK 는 모든 모델과 호환되는 일반 `ModelSettings` 로 되돌아갑니다 ### Responses 전용 도구 검색 기능 @@ -98,11 +98,11 @@ preview 호환 요청은 `environment` 및 디스플레이 크기를 먼저 직 - [`tool_namespace()`][agents.tool.tool_namespace] - `@function_tool(defer_loading=True)` 및 기타 지연 로딩 Responses 도구 표면 -이 기능들은 Chat Completions 모델과 Responses 가 아닌 백엔드에서 거부됩니다. 지연 로딩 도구를 사용할 때는 에이전트에 `ToolSearchTool()` 을 추가하고, 네임스페이스 이름 또는 지연 전용 함수 이름을 강제하기보다 `auto` 또는 `required` tool choice 를 통해 모델이 도구를 로드하도록 하세요. 설정 세부 사항과 현재 제약은 [도구](../tools.md#hosted-tool-search)를 참고하세요. +이 기능들은 Chat Completions 모델 및 Responses 가 아닌 backend 에서 거부됩니다. 지연 로딩 도구를 사용할 때는 에이전트에 `ToolSearchTool()` 을 추가하고, 빈 namespace 이름이나 지연 전용 함수 이름을 강제하는 대신 모델이 `auto` 또는 `required` tool choice 를 통해 도구를 로드하도록 하세요. 설정 세부 사항과 현재 제약은 [Tools](../tools.md#hosted-tool-search)를 참조하세요 ### Responses WebSocket 전송 -기본적으로 OpenAI Responses API 요청은 HTTP 전송을 사용합니다. OpenAI 기반 모델 사용 시 websocket 전송을 활성화할 수 있습니다. +기본적으로 OpenAI Responses API 요청은 HTTP 전송을 사용합니다. OpenAI 기반 모델 사용 시 websocket 전송을 선택적으로 활성화할 수 있습니다 #### 기본 설정 @@ -112,13 +112,13 @@ from agents import set_default_openai_responses_transport set_default_openai_responses_transport("websocket") ``` -이는 기본 OpenAI provider 로 해석되는 OpenAI Responses 모델( `"gpt-5.4"` 같은 문자열 모델 이름 포함)에 영향을 줍니다. +이는 기본 OpenAI provider 로 해석되는 OpenAI Responses 모델(`"gpt-5.4"` 같은 문자열 모델 이름 포함)에 적용됩니다 -전송 선택은 SDK 가 모델 이름을 모델 인스턴스로 해석할 때 수행됩니다. 구체적인 [`Model`][agents.models.interface.Model] 객체를 전달하면 전송이 이미 고정됩니다: [`OpenAIResponsesWSModel`][agents.models.openai_responses.OpenAIResponsesWSModel] 은 websocket, [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] 은 HTTP, [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] 은 Chat Completions 를 사용합니다. `RunConfig(model_provider=...)` 를 전달하면 전역 기본값 대신 해당 provider 가 전송 선택을 제어합니다. +전송 선택은 SDK 가 모델 이름을 모델 인스턴스로 해석할 때 이루어집니다. 구체적인 [`Model`][agents.models.interface.Model] 객체를 전달하면 해당 전송은 이미 고정됩니다: [`OpenAIResponsesWSModel`][agents.models.openai_responses.OpenAIResponsesWSModel] 은 websocket, [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] 은 HTTP, [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] 은 Chat Completions 를 유지합니다. `RunConfig(model_provider=...)` 를 전달하면 전역 기본값 대신 해당 provider 가 전송 선택을 제어합니다 -#### provider 또는 실행 수준 설정 +#### provider 또는 실행 단위 설정 -provider 단위 또는 실행 단위로 websocket 전송을 구성할 수도 있습니다: +websocket 전송은 provider 단위 또는 실행 단위로도 구성할 수 있습니다: ```python from agents import Agent, OpenAIProvider, RunConfig, Runner @@ -139,14 +139,14 @@ result = await Runner.run( #### `MultiProvider` 를 사용한 고급 라우팅 -접두사 기반 모델 라우팅이 필요하다면(예: 하나의 실행에서 `openai/...` 와 `litellm/...` 모델 이름 혼합), [`MultiProvider`][agents.MultiProvider] 를 사용하고 그곳에서 `openai_use_responses_websocket=True` 를 설정하세요. +접두사 기반 모델 라우팅이 필요하다면(예: 한 번의 실행에서 `openai/...` 와 `any-llm/...` 모델 이름 혼합) [`MultiProvider`][agents.MultiProvider] 를 사용하고, 그곳에서 `openai_use_responses_websocket=True` 를 설정하세요 `MultiProvider` 는 두 가지 기존 기본값을 유지합니다: -- `openai/...` 는 OpenAI provider 의 별칭으로 처리되므로 `openai/gpt-4.1` 은 `gpt-4.1` 모델로 라우팅됩니다 +- `openai/...` 는 OpenAI provider 의 별칭으로 처리되어 `openai/gpt-4.1` 은 모델 `gpt-4.1` 로 라우팅됩니다 - 알 수 없는 접두사는 그대로 전달되지 않고 `UserError` 를 발생시킵니다 -OpenAI 호환 엔드포인트가 리터럴 네임스페이스 모델 ID 를 기대하는 경우, 명시적으로 pass-through 동작을 활성화하세요. websocket 활성화 구성에서는 `MultiProvider` 에서도 `openai_use_responses_websocket=True` 를 유지하세요: +OpenAI provider 를 리터럴 네임스페이스 모델 ID 를 기대하는 OpenAI 호환 endpoint 로 지정하는 경우, pass-through 동작을 명시적으로 활성화하세요. websocket 활성화 설정에서는 `MultiProvider` 에서도 `openai_use_responses_websocket=True` 를 유지하세요: ```python from agents import Agent, MultiProvider, RunConfig, Runner @@ -172,52 +172,52 @@ result = await Runner.run( ) ``` -백엔드가 리터럴 `openai/...` 문자열을 기대하면 `openai_prefix_mode="model_id"` 를 사용하세요. `openrouter/openai/gpt-4.1-mini` 같은 다른 네임스페이스 모델 ID 를 기대하면 `unknown_prefix_mode="model_id"` 를 사용하세요. 이 옵션들은 websocket 전송 외의 `MultiProvider` 에서도 동작합니다. 이 예제는 이 섹션에서 설명한 전송 설정의 일부이기 때문에 websocket 을 활성화한 상태를 유지합니다. 동일한 옵션은 [`responses_websocket_session()`][agents.responses_websocket_session] 에서도 사용할 수 있습니다. +backend 가 리터럴 `openai/...` 문자열을 기대하면 `openai_prefix_mode="model_id"` 를 사용하세요. backend 가 `openrouter/openai/gpt-4.1-mini` 같은 다른 네임스페이스 모델 ID 를 기대하면 `unknown_prefix_mode="model_id"` 를 사용하세요. 이 옵션들은 websocket 전송 외부의 `MultiProvider` 에서도 동작합니다. 이 예시는 이 섹션에서 설명한 전송 설정의 일부이므로 websocket 을 활성화한 상태를 유지합니다. 동일한 옵션은 [`responses_websocket_session()`][agents.responses_websocket_session] 에서도 사용할 수 있습니다 -사용자 지정 OpenAI 호환 엔드포인트나 프록시를 사용하는 경우, websocket 전송에는 호환되는 websocket `/responses` 엔드포인트도 필요합니다. 이런 구성에서는 `websocket_base_url` 을 명시적으로 설정해야 할 수 있습니다. +사용자 지정 OpenAI 호환 endpoint 또는 proxy 를 사용하는 경우 websocket 전송에도 호환되는 websocket `/responses` endpoint 가 필요합니다. 이런 설정에서는 `websocket_base_url` 을 명시적으로 설정해야 할 수 있습니다 #### 참고 사항 -- 이는 websocket 전송 위의 Responses API 이며, [Realtime API](../realtime/guide.md)가 아닙니다. Chat Completions 또는 Responses websocket `/responses` 엔드포인트를 지원하지 않는 OpenAI 가 아닌 provider 에는 적용되지 않습니다 -- 환경에 아직 없다면 `websockets` 패키지를 설치하세요 -- websocket 전송을 활성화한 뒤 [`Runner.run_streamed()`][agents.run.Runner.run_streamed] 를 직접 사용할 수 있습니다. 여러 턴 워크플로에서 같은 websocket 연결을 턴 간(중첩된 agent-as-tool 호출 포함) 재사용하려면 [`responses_websocket_session()`][agents.responses_websocket_session] 헬퍼를 권장합니다. [에이전트 실행](../running_agents.md) 가이드와 [`examples/basic/stream_ws.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/stream_ws.py)를 참고하세요 +- 이것은 websocket 전송의 Responses API 이며 [Realtime API](../realtime/guide.md)가 아닙니다. Chat Completions 또는 Responses websocket `/responses` endpoint 를 지원하지 않는 OpenAI 가 아닌 provider 에는 적용되지 않습니다 +- 환경에 `websockets` 패키지가 아직 없다면 설치하세요 +- websocket 전송을 활성화한 뒤 [`Runner.run_streamed()`][agents.run.Runner.run_streamed] 를 직접 사용할 수 있습니다. 여러 턴 워크플로에서 턴 간(중첩된 agent-as-tool 호출 포함) 동일 websocket 연결을 재사용하려면 [`responses_websocket_session()`][agents.responses_websocket_session] 헬퍼를 권장합니다. [Running agents](../running_agents.md) 가이드와 [`examples/basic/stream_ws.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/stream_ws.py)를 참조하세요 ## OpenAI 가 아닌 모델 -OpenAI 가 아닌 provider 가 필요하면 SDK 의 내장 provider 통합 지점부터 시작하세요. 많은 설정에서는 LiteLLM 을 추가하지 않아도 충분합니다. 각 패턴의 예시는 [examples/model_providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/)에 있습니다. +OpenAI 가 아닌 provider 가 필요하면 SDK 의 내장 provider 통합 지점부터 시작하세요. 많은 설정에서 서드파티 adapter 없이도 충분합니다. 각 패턴의 예시는 [examples/model_providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/)에 있습니다 ### OpenAI 가 아닌 provider 통합 방법 -| 접근 방식 | 사용 시점 | 범위 | +| 접근 방식 | 사용하는 경우 | 범위 | | --- | --- | --- | -| [`set_default_openai_client`][agents.set_default_openai_client] | 하나의 OpenAI 호환 엔드포인트를 대부분 또는 모든 에이전트의 기본값으로 써야 할 때 | 전역 기본값 | -| [`ModelProvider`][agents.models.interface.ModelProvider] | 하나의 사용자 지정 provider 를 단일 실행에 적용해야 할 때 | 실행별 | -| [`Agent.model`][agents.agent.Agent.model] | 서로 다른 에이전트에 서로 다른 provider 또는 구체적 모델 객체가 필요할 때 | 에이전트별 | -| LiteLLM (베타) | LiteLLM 고유의 provider 범위 또는 라우팅이 필요할 때 | [LiteLLM](#litellm) 참고 | +| [`set_default_openai_client`][agents.set_default_openai_client] | 하나의 OpenAI 호환 endpoint 를 대부분 또는 모든 에이전트의 기본값으로 사용해야 하는 경우 | 전역 기본값 | +| [`ModelProvider`][agents.models.interface.ModelProvider] | 하나의 사용자 지정 provider 를 단일 실행에 적용해야 하는 경우 | 실행 단위 | +| [`Agent.model`][agents.agent.Agent.model] | 서로 다른 에이전트에 서로 다른 provider 또는 구체적 모델 객체가 필요한 경우 | 에이전트 단위 | +| 서드파티 adapter | 내장 경로가 제공하지 않는 adapter 관리 provider 범위 또는 라우팅이 필요한 경우 | [서드파티 adapters](#third-party-adapters) 참조 | -다음 내장 경로로 다른 LLM provider 를 통합할 수 있습니다: +이 내장 경로들로 다른 LLM provider 를 통합할 수 있습니다: -1. [`set_default_openai_client`][agents.set_default_openai_client] 는 `AsyncOpenAI` 인스턴스를 LLM 클라이언트로 전역 사용하려는 경우에 유용합니다. LLM provider 가 OpenAI 호환 API 엔드포인트를 제공하고 `base_url` 과 `api_key` 를 설정할 수 있는 경우에 해당합니다. 구성 가능한 예시는 [examples/model_providers/custom_example_global.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_global.py)를 참고하세요 -2. [`ModelProvider`][agents.models.interface.ModelProvider] 는 `Runner.run` 수준에서 적용됩니다. 이를 통해 "이 실행의 모든 에이전트에 사용자 지정 모델 provider 를 사용"하도록 지정할 수 있습니다. 구성 가능한 예시는 [examples/model_providers/custom_example_provider.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_provider.py)를 참고하세요 -3. [`Agent.model`][agents.agent.Agent.model] 은 특정 Agent 인스턴스에 모델을 지정할 수 있게 합니다. 이를 통해 에이전트별로 서로 다른 provider 를 혼합해 사용할 수 있습니다. 구성 가능한 예시는 [examples/model_providers/custom_example_agent.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_agent.py)를 참고하세요 +1. [`set_default_openai_client`][agents.set_default_openai_client] 는 `AsyncOpenAI` 인스턴스를 LLM 클라이언트로 전역 사용하려는 경우에 유용합니다. LLM provider 가 OpenAI 호환 API endpoint 를 제공하고 `base_url` 및 `api_key` 를 설정할 수 있는 경우에 해당합니다. 구성 가능한 예시는 [examples/model_providers/custom_example_global.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_global.py)를 참조하세요 +2. [`ModelProvider`][agents.models.interface.ModelProvider] 는 `Runner.run` 수준에서 동작합니다. 이를 통해 "이 실행의 모든 에이전트에 사용자 지정 모델 provider 를 사용"할 수 있습니다. 구성 가능한 예시는 [examples/model_providers/custom_example_provider.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_provider.py)를 참조하세요 +3. [`Agent.model`][agents.agent.Agent.model] 은 특정 Agent 인스턴스에 모델을 지정할 수 있게 해줍니다. 이를 통해 서로 다른 에이전트에 서로 다른 provider 를 혼합해 사용할 수 있습니다. 구성 가능한 예시는 [examples/model_providers/custom_example_agent.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_agent.py)를 참조하세요 -`platform.openai.com` 의 API 키가 없는 경우, `set_tracing_disabled()` 로 트레이싱을 비활성화하거나 [다른 트레이싱 프로세서](../tracing.md)를 설정하는 것을 권장합니다. +`platform.openai.com` 의 API 키가 없는 경우에는 `set_tracing_disabled()` 로 트레이싱을 비활성화하거나 [다른 트레이싱 프로세서](../tracing.md)를 설정하는 것을 권장합니다 !!! note - 이 예시들에서는 Chat Completions API/모델을 사용합니다. 많은 LLM provider 가 아직 Responses API 를 지원하지 않기 때문입니다. LLM provider 가 이를 지원한다면 Responses 사용을 권장합니다 + 이 예시들에서는 많은 LLM provider 가 아직 Responses API 를 지원하지 않기 때문에 Chat Completions API/모델을 사용합니다. LLM provider 가 이를 지원한다면 Responses 사용을 권장합니다 ## 하나의 워크플로에서 모델 혼합 -하나의 워크플로 안에서 에이전트별로 다른 모델을 사용하고 싶을 수 있습니다. 예를 들어 분류에는 더 작고 빠른 모델을, 복잡한 작업에는 더 크고 성능이 높은 모델을 사용할 수 있습니다. [`Agent`][agents.Agent] 를 구성할 때 다음 중 하나로 특정 모델을 선택할 수 있습니다: +단일 워크플로 내에서 각 에이전트마다 서로 다른 모델을 사용하고 싶을 수 있습니다. 예를 들어, 분류에는 더 작고 빠른 모델을 사용하고 복잡한 작업에는 더 크고 성능이 높은 모델을 사용할 수 있습니다. [`Agent`][agents.Agent] 를 구성할 때는 다음 중 하나로 특정 모델을 선택할 수 있습니다: 1. 모델 이름 전달 -2. 모델 이름 + 해당 이름을 Model 인스턴스로 매핑할 수 있는 [`ModelProvider`][agents.models.interface.ModelProvider] 전달 -3. [`Model`][agents.models.interface.Model] 구현을 직접 전달 +2. 임의의 모델 이름 + 해당 이름을 Model 인스턴스로 매핑할 수 있는 [`ModelProvider`][agents.models.interface.ModelProvider] 전달 +3. [`Model`][agents.models.interface.Model] 구현을 직접 제공 !!! note - SDK 는 [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] 과 [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] 형태를 모두 지원하지만, 두 형태는 지원 기능과 도구 세트가 다르므로 워크플로별로 하나의 모델 형태만 사용하는 것을 권장합니다. 워크플로에서 모델 형태를 혼합해야 한다면, 사용하는 모든 기능이 양쪽 모두에서 사용 가능한지 확인하세요 + SDK 는 [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] 과 [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] 형태를 모두 지원하지만, 두 형태는 지원 기능과 도구 집합이 다르므로 워크플로마다 단일 모델 형태를 사용하는 것을 권장합니다. 워크플로에서 모델 형태를 혼합해야 한다면 사용 중인 모든 기능이 양쪽 모두에서 사용 가능한지 확인하세요 ```python from agents import Agent, Runner, AsyncOpenAI, OpenAIChatCompletionsModel @@ -253,7 +253,7 @@ async def main(): 1. OpenAI 모델 이름을 직접 설정합니다 2. [`Model`][agents.models.interface.Model] 구현을 제공합니다 -에이전트에 사용되는 모델을 추가로 구성하려면 temperature 같은 선택적 모델 구성 매개변수를 제공하는 [`ModelSettings`][agents.models.interface.ModelSettings] 를 전달할 수 있습니다. +에이전트에 사용되는 모델을 추가로 구성하려면 temperature 같은 선택적 모델 구성 매개변수를 제공하는 [`ModelSettings`][agents.models.interface.ModelSettings] 를 전달할 수 있습니다 ```python from agents import Agent, ModelSettings @@ -268,19 +268,19 @@ english_agent = Agent( ## 고급 OpenAI Responses 설정 -OpenAI Responses 경로에서 더 세밀한 제어가 필요하면 `ModelSettings` 부터 시작하세요. +OpenAI Responses 경로에서 더 많은 제어가 필요하면 `ModelSettings` 부터 시작하세요 -### 공통 고급 `ModelSettings` 옵션 +### 일반적인 고급 `ModelSettings` 옵션 -OpenAI Responses API 를 사용하는 경우, 여러 요청 필드가 이미 `ModelSettings` 에 직접 대응되므로 이를 위해 `extra_args` 를 사용할 필요가 없습니다. +OpenAI Responses API 를 사용할 때는 여러 요청 필드가 이미 `ModelSettings` 필드로 직접 제공되므로 이를 위해 `extra_args` 가 필요하지 않습니다 -- `parallel_tool_calls`: 같은 턴에서 여러 도구 호출을 허용하거나 금지 -- `truncation`: 컨텍스트가 넘쳐 실패하는 대신 Responses API 가 가장 오래된 대화 항목을 삭제하도록 `"auto"` 설정 -- `store`: 생성된 응답을 나중에 조회할 수 있도록 서버 측에 저장할지 제어. 이는 응답 ID 에 의존하는 후속 워크플로와 `store=False` 일 때 로컬 입력으로 폴백이 필요할 수 있는 세션 압축 흐름에 중요합니다 +- `parallel_tool_calls`: 같은 턴에서 여러 도구 호출 허용 또는 금지 +- `truncation`: 컨텍스트가 넘칠 때 실패하는 대신 Responses API 가 가장 오래된 대화 항목을 제거하도록 `"auto"` 설정 +- `store`: 생성된 응답을 이후 조회를 위해 서버 측에 저장할지 제어. 이는 response ID 에 의존하는 후속 워크플로 및 `store=False` 일 때 로컬 입력으로 대체해야 할 수 있는 세션 압축 흐름에 중요합니다 - `prompt_cache_retention`: 예를 들어 `"24h"` 로 캐시된 프롬프트 접두사를 더 오래 유지 -- `response_include`: `web_search_call.action.sources`, `file_search_call.results`, `reasoning.encrypted_content` 같은 더 풍부한 응답 페이로드 요청 -- `top_logprobs`: 출력 텍스트의 top-token logprobs 요청. SDK 는 `message.output_text.logprobs` 도 자동 추가합니다 -- `retry`: 모델 호출에 대해 runner 가 관리하는 재시도 설정 활성화. [Runner 관리 재시도](#runner-managed-retries) 참고 +- `response_include`: `web_search_call.action.sources`, `file_search_call.results`, `reasoning.encrypted_content` 같은 더 풍부한 응답 payload 요청 +- `top_logprobs`: 출력 텍스트의 상위 토큰 logprobs 요청. SDK 는 `message.output_text.logprobs` 도 자동으로 추가합니다 +- `retry`: 모델 호출에 대해 runner 관리 재시도 설정 선택 적용. [Runner 관리 재시도](#runner-managed-retries) 참조 ```python from agents import Agent, ModelSettings @@ -299,13 +299,13 @@ research_agent = Agent( ) ``` -`store=False` 를 설정하면 Responses API 는 해당 응답을 나중에 서버 측에서 조회 가능하게 유지하지 않습니다. 이는 stateless 또는 zero-data-retention 스타일 흐름에 유용하지만, 그렇지 않으면 응답 ID 를 재사용하던 기능이 대신 로컬 관리 상태에 의존해야 함을 의미합니다. 예를 들어 [`OpenAIResponsesCompactionSession`][agents.memory.openai_responses_compaction_session.OpenAIResponsesCompactionSession] 은 마지막 응답이 저장되지 않았을 때 기본 `"auto"` 압축 경로를 입력 기반 압축으로 전환합니다. [세션 가이드](../sessions/index.md#openai-responses-compaction-sessions)를 참고하세요. +`store=False` 를 설정하면 Responses API 는 이후 서버 측 조회를 위해 해당 응답을 보관하지 않습니다. 이는 무상태 또는 zero-data-retention 스타일 흐름에 유용하지만, 응답 ID 를 재사용하던 기능은 대신 로컬 관리 상태에 의존해야 함을 의미합니다. 예를 들어 [`OpenAIResponsesCompactionSession`][agents.memory.openai_responses_compaction_session.OpenAIResponsesCompactionSession] 은 마지막 응답이 저장되지 않았을 때 기본 `"auto"` 압축 경로를 입력 기반 압축으로 전환합니다. [Sessions 가이드](../sessions/index.md#openai-responses-compaction-sessions)를 참조하세요 ### `extra_args` 전달 -SDK 가 아직 최상위에서 직접 노출하지 않는 provider 전용 또는 최신 요청 필드가 필요할 때 `extra_args` 를 사용하세요. +SDK 가 아직 최상위에 직접 노출하지 않는 provider 전용 또는 최신 요청 필드가 필요할 때 `extra_args` 를 사용하세요 -또한 OpenAI Responses API 사용 시 [다른 선택적 매개변수](https://platform.openai.com/docs/api-reference/responses/create) (예: `user`, `service_tier` 등)가 있습니다. 이들이 최상위에 없으면 `extra_args` 로 전달할 수 있습니다. +또한 OpenAI 의 Responses API 사용 시 [추가 선택 매개변수](https://platform.openai.com/docs/api-reference/responses/create) (예: `user`, `service_tier` 등)도 있습니다. 최상위에 없다면 `extra_args` 로 전달할 수 있습니다 ```python from agents import Agent, ModelSettings @@ -323,7 +323,7 @@ english_agent = Agent( ## Runner 관리 재시도 -재시도는 런타임 전용이며 옵트인입니다. `ModelSettings(retry=...)` 를 설정하고 재시도 정책이 재시도를 선택하지 않는 한 SDK 는 일반 모델 요청을 재시도하지 않습니다. +재시도는 런타임 전용이며 옵트인입니다. `ModelSettings(retry=...)` 를 설정하고 재시도 정책이 재시도를 선택하지 않는 한 SDK 는 일반 모델 요청을 재시도하지 않습니다 ```python from agents import Agent, ModelRetrySettings, ModelSettings, retry_policies @@ -363,73 +363,73 @@ agent = Agent( -재시도 정책은 다음 정보를 가진 [`RetryPolicyContext`][agents.retry.RetryPolicyContext] 를 받습니다: +재시도 정책은 [`RetryPolicyContext`][agents.retry.RetryPolicyContext] 를 받으며 다음을 포함합니다: -- `attempt` 와 `max_retries` 로 시도 횟수 인지형 결정 가능 +- `attempt` 와 `max_retries` 로 시도 횟수 인지 의사결정 가능 - `stream` 으로 스트리밍/비스트리밍 동작 분기 가능 -- 원문 확인을 위한 `error` +- 원시 검사 용도의 `error` - `status_code`, `retry_after`, `error_code`, `is_network_error`, `is_timeout`, `is_abort` 같은 `normalized` 정보 -- 기본 모델 어댑터가 재시도 가이드를 제공할 수 있는 경우 `provider_advice` +- 하위 모델 adapter 가 재시도 가이드를 제공할 수 있을 때의 `provider_advice` 정책은 다음 중 하나를 반환할 수 있습니다: - 단순 재시도 결정을 위한 `True` / `False` -- 지연을 재정의하거나 진단 사유를 첨부하려는 경우 [`RetryDecision`][agents.retry.RetryDecision] +- 지연을 재정의하거나 진단 이유를 첨부하려는 경우 [`RetryDecision`][agents.retry.RetryDecision] -SDK 는 `retry_policies` 에서 즉시 사용 가능한 헬퍼를 제공합니다: +SDK 는 `retry_policies` 에 준비된 헬퍼를 제공합니다: | 헬퍼 | 동작 | | --- | --- | -| `retry_policies.never()` | 항상 비활성화 | +| `retry_policies.never()` | 항상 사용 안 함 | | `retry_policies.provider_suggested()` | 가능할 때 provider 재시도 권고를 따름 | -| `retry_policies.network_error()` | 일시적 전송/타임아웃 실패와 매칭 | +| `retry_policies.network_error()` | 일시적 전송 및 timeout 실패와 매칭 | | `retry_policies.http_status([...])` | 선택한 HTTP 상태 코드와 매칭 | | `retry_policies.retry_after()` | retry-after 힌트가 있을 때만 해당 지연으로 재시도 | -| `retry_policies.any(...)` | 중첩 정책 중 하나라도 활성화하면 재시도 | -| `retry_policies.all(...)` | 중첩 정책 모두 활성화할 때만 재시도 | +| `retry_policies.any(...)` | 중첩 정책 중 하나라도 선택하면 재시도 | +| `retry_policies.all(...)` | 중첩 정책 모두가 선택할 때만 재시도 | -정책을 조합할 때 `provider_suggested()` 가 가장 안전한 첫 구성 요소입니다. provider 가 구분 가능한 경우 provider veto 와 replay-safe 승인 정보를 보존하기 때문입니다. +정책 조합 시 `provider_suggested()` 가 가장 안전한 첫 구성 요소입니다. provider 가 이를 구분할 수 있을 때 provider veto 와 replay-safety 승인을 보존하기 때문입니다 ##### 안전 경계 일부 실패는 자동 재시도되지 않습니다: - Abort 오류 -- provider 권고가 replay 를 안전하지 않다고 표시한 요청 -- replay 가 안전하지 않게 되는 방식으로 출력이 이미 시작된 이후의 스트리밍 실행 +- provider 권고에서 replay 가 안전하지 않다고 표시된 요청 +- replay 를 안전하지 않게 만드는 방식으로 출력이 이미 시작된 스트리밍 실행 -`previous_response_id` 또는 `conversation_id` 를 사용하는 상태 기반 후속 요청도 더 보수적으로 처리됩니다. 이런 요청에서는 `network_error()` 나 `http_status([500])` 같은 비-provider 조건만으로는 충분하지 않습니다. 재시도 정책에 일반적으로 `retry_policies.provider_suggested()` 를 통한 replay-safe provider 승인이 포함되어야 합니다. +`previous_response_id` 또는 `conversation_id` 를 사용하는 상태 저장형 후속 요청도 더 보수적으로 처리됩니다. 이런 요청에서는 `network_error()` 또는 `http_status([500])` 같은 비provider predicate 만으로는 충분하지 않습니다. 재시도 정책에는 일반적으로 `retry_policies.provider_suggested()` 를 통한 provider 의 replay-safe 승인이 포함되어야 합니다 ##### Runner 와 에이전트 병합 동작 `retry` 는 runner 수준과 에이전트 수준 `ModelSettings` 사이에서 deep-merge 됩니다: -- 에이전트는 `retry.max_retries` 만 재정의하고 runner 의 `policy` 를 상속할 수 있습니다 -- 에이전트는 `retry.backoff` 의 일부만 재정의하고 runner 의 같은 수준 다른 backoff 필드를 유지할 수 있습니다 -- `policy` 는 런타임 전용이므로 직렬화된 `ModelSettings` 는 `max_retries` 와 `backoff` 는 유지하지만 콜백 자체는 생략합니다 +- 에이전트는 `retry.max_retries` 만 재정의하고 runner 의 `policy` 는 상속할 수 있습니다 +- 에이전트는 `retry.backoff` 일부만 재정의하고 나머지 backoff 필드는 runner 값을 유지할 수 있습니다 +- `policy` 는 런타임 전용이므로 직렬화된 `ModelSettings` 에는 `max_retries` 와 `backoff` 는 남고 콜백 자체는 제외됩니다 -더 자세한 예시는 [`examples/basic/retry.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry.py) 및 [`examples/basic/retry_litellm.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry_litellm.py)를 참고하세요. +더 자세한 예시는 [`examples/basic/retry.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry.py) 및 [adapter 기반 재시도 예시](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry_litellm.py)를 참조하세요 ## OpenAI 가 아닌 provider 문제 해결 ### 트레이싱 클라이언트 오류 401 -트레이싱 관련 오류가 발생하면, 트레이스가 OpenAI 서버로 업로드되는데 OpenAI API 키가 없기 때문입니다. 해결 방법은 세 가지입니다: +트레이싱 관련 오류가 발생한다면, 트레이스가 OpenAI 서버로 업로드되는데 OpenAI API 키가 없기 때문입니다. 해결 방법은 세 가지입니다: -1. 트레이싱을 완전히 비활성화: [`set_tracing_disabled(True)`][agents.set_tracing_disabled] -2. 트레이싱용 OpenAI 키 설정: [`set_tracing_export_api_key(...)`][agents.set_tracing_export_api_key]. 이 API 키는 트레이스 업로드에만 사용되며 [platform.openai.com](https://platform.openai.com/) 의 키여야 합니다 -3. OpenAI 가 아닌 트레이스 프로세서 사용. [트레이싱 문서](../tracing.md#custom-tracing-processors) 참고 +1. 트레이싱 완전 비활성화: [`set_tracing_disabled(True)`][agents.set_tracing_disabled] +2. 트레이싱용 OpenAI 키 설정: [`set_tracing_export_api_key(...)`][agents.set_tracing_export_api_key]. 이 API 키는 트레이스 업로드에만 사용되며 [platform.openai.com](https://platform.openai.com/) 키여야 합니다 +3. OpenAI 가 아닌 트레이스 프로세서 사용. [트레이싱 문서](../tracing.md#custom-tracing-processors) 참조 ### Responses API 지원 -SDK 는 기본적으로 Responses API 를 사용하지만, 많은 다른 LLM provider 는 아직 이를 지원하지 않습니다. 그 결과 404 또는 유사한 문제가 발생할 수 있습니다. 해결하려면 두 가지 옵션이 있습니다: +SDK 는 기본적으로 Responses API 를 사용하지만, 다른 많은 LLM provider 는 아직 이를 지원하지 않습니다. 그 결과 404 또는 유사한 이슈를 볼 수 있습니다. 해결 방법은 두 가지입니다: -1. [`set_default_openai_api("chat_completions")`][agents.set_default_openai_api] 호출. 이는 환경 변수로 `OPENAI_API_KEY` 와 `OPENAI_BASE_URL` 을 설정하는 경우 동작합니다 +1. [`set_default_openai_api("chat_completions")`][agents.set_default_openai_api] 호출. 환경 변수로 `OPENAI_API_KEY` 와 `OPENAI_BASE_URL` 을 설정하는 경우 동작합니다 2. [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] 사용. 예시는 [여기](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/)에 있습니다 ### structured outputs 지원 -일부 모델 provider 는 [structured outputs](https://platform.openai.com/docs/guides/structured-outputs)를 지원하지 않습니다. 이 경우 다음과 유사한 오류가 발생할 수 있습니다: +일부 모델 provider 는 [structured outputs](https://platform.openai.com/docs/guides/structured-outputs)를 지원하지 않습니다. 이 경우 때때로 다음과 같은 오류가 발생합니다: ``` @@ -437,24 +437,34 @@ BadRequestError: Error code: 400 - {'error': {'message': "'response_format.type' ``` -이것은 일부 모델 provider 의 한계입니다. JSON 출력은 지원하지만 출력에 사용할 `json_schema` 지정은 허용하지 않습니다. 이 문제를 해결 중이지만, JSON schema 출력을 지원하는 provider 에 의존하는 것을 권장합니다. 그렇지 않으면 잘못된 JSON 때문에 앱이 자주 깨질 수 있습니다. +이는 일부 모델 provider 의 한계입니다. JSON 출력은 지원하지만 출력에 사용할 `json_schema` 지정은 허용하지 않습니다. 이 문제에 대한 수정 작업을 진행 중이지만, JSON schema 출력을 지원하는 provider 에 의존하는 것을 권장합니다. 그렇지 않으면 잘못된 JSON 때문에 앱이 자주 중단될 수 있습니다 ## provider 간 모델 혼합 -모델 provider 간 기능 차이를 인지해야 하며, 그렇지 않으면 오류가 발생할 수 있습니다. 예를 들어 OpenAI 는 structured outputs, 멀티모달 입력, 호스티드 file search 및 web search 를 지원하지만 많은 다른 provider 는 이러한 기능을 지원하지 않습니다. 다음 제한 사항에 유의하세요: +모델 provider 간 기능 차이를 인지하지 않으면 오류가 발생할 수 있습니다. 예를 들어 OpenAI 는 structured outputs, 멀티모달 입력, 호스티드 file search 와 web search 를 지원하지만 다른 많은 provider 는 이러한 기능을 지원하지 않습니다. 다음 제한 사항에 유의하세요: -- 지원하지 않는 provider 에는 지원되지 않는 `tools` 를 보내지 마세요 -- 텍스트 전용 모델 호출 전에 멀티모달 입력을 필터링하세요 -- structured JSON 출력 미지원 provider 는 때때로 유효하지 않은 JSON 을 생성할 수 있음을 유의하세요 +- 지원하지 않는 `tools` 를 이를 이해하지 못하는 provider 에 보내지 마세요 +- 텍스트 전용 모델 호출 전 멀티모달 입력을 필터링하세요 +- structured JSON 출력을 지원하지 않는 provider 는 때때로 유효하지 않은 JSON 을 생성할 수 있음을 유의하세요 -## LiteLLM +## 서드파티 adapters -LiteLLM 지원은 OpenAI 가 아닌 provider 를 Agents SDK 워크플로에 포함해야 하는 경우를 위한 best-effort 베타 기능으로 제공됩니다. +SDK 의 내장 provider 통합 지점만으로 부족할 때만 서드파티 adapter 를 사용하세요. 이 SDK 에서 OpenAI 모델만 사용하는 경우 Any-LLM 이나 LiteLLM 대신 내장 [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] 경로를 우선하세요. 서드파티 adapter 는 OpenAI 모델과 OpenAI 가 아닌 provider 를 함께 사용해야 하거나, 내장 경로가 제공하지 않는 adapter 관리 provider 범위 또는 라우팅이 필요한 경우를 위한 것입니다. adapter 는 SDK 와 업스트림 모델 provider 사이에 추가 호환성 계층을 더하므로 기능 지원과 요청 의미가 provider 별로 달라질 수 있습니다. SDK 는 현재 Any-LLM 과 LiteLLM 을 best-effort 베타 adapter 통합으로 포함합니다 -이 SDK 와 함께 OpenAI 모델을 사용하는 경우 LiteLLM 대신 내장 [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] 경로를 권장합니다. +### Any-LLM -OpenAI 모델과 OpenAI 가 아닌 provider 를 함께 사용해야 하는 경우, 특히 Chat Completions 호환 API 를 통해 사용한다면 LiteLLM 을 베타 옵션으로 사용할 수 있지만 모든 설정에서 최적 선택은 아닐 수 있습니다. +Any-LLM 지원은 Any-LLM 관리 provider 범위 또는 라우팅이 필요한 경우를 위해 best-effort 베타 형태로 제공됩니다 -OpenAI 가 아닌 provider 에 LiteLLM 이 필요하다면 `openai-agents[litellm]` 를 설치한 뒤 [`examples/model_providers/litellm_auto.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/litellm_auto.py) 또는 [`examples/model_providers/litellm_provider.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/litellm_provider.py)에서 시작하세요. `litellm/...` 모델 이름을 사용하거나 [`LitellmModel`][agents.extensions.models.litellm_model.LitellmModel] 을 직접 인스턴스화할 수 있습니다. +업스트림 provider 경로에 따라 Any-LLM 은 Responses API, Chat Completions 호환 API 또는 provider 전용 호환성 계층을 사용할 수 있습니다 -LiteLLM 응답이 SDK 사용량 메트릭을 채우게 하려면 `ModelSettings(include_usage=True)` 를 전달하세요. \ No newline at end of file +Any-LLM 이 필요하면 `openai-agents[any-llm]` 을 설치한 뒤 [`examples/model_providers/any_llm_auto.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/any_llm_auto.py) 또는 [`examples/model_providers/any_llm_provider.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/any_llm_provider.py)부터 시작하세요. [`MultiProvider`][agents.MultiProvider] 와 함께 `any-llm/...` 모델 이름을 사용하거나, `AnyLLMModel` 을 직접 인스턴스화하거나, 실행 범위에서 `AnyLLMProvider` 를 사용할 수 있습니다. 모델 표면을 명시적으로 고정해야 하면 `AnyLLMModel` 생성 시 `api="responses"` 또는 `api="chat_completions"` 를 전달하세요 + +Any-LLM 은 여전히 서드파티 adapter 계층이므로 provider 의존성과 기능 격차는 SDK 가 아니라 Any-LLM 업스트림에서 정의됩니다. 사용량 지표는 업스트림 provider 가 반환할 때 자동 전파되지만, 스트리밍 Chat Completions backend 는 사용량 청크를 내보내기 전에 `ModelSettings(include_usage=True)` 가 필요할 수 있습니다. structured outputs, 도구 호출, 사용량 보고, Responses 전용 동작에 의존한다면 배포하려는 정확한 provider backend 를 검증하세요 + +### LiteLLM + +LiteLLM 지원은 LiteLLM 전용 provider 범위 또는 라우팅이 필요한 경우를 위해 best-effort 베타 형태로 제공됩니다 + +LiteLLM 이 필요하면 `openai-agents[litellm]` 을 설치한 뒤 [`examples/model_providers/litellm_auto.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/litellm_auto.py) 또는 [`examples/model_providers/litellm_provider.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/litellm_provider.py)부터 시작하세요. `litellm/...` 모델 이름을 사용하거나 [`LitellmModel`][agents.extensions.models.litellm_model.LitellmModel] 을 직접 인스턴스화할 수 있습니다 + +일부 LiteLLM 기반 provider 는 기본적으로 SDK 사용량 지표를 채우지 않습니다. 사용량 보고가 필요하면 `ModelSettings(include_usage=True)` 를 전달하고 structured outputs, 도구 호출, 사용량 보고 또는 adapter 전용 라우팅 동작에 의존한다면 배포하려는 정확한 provider backend 를 검증하세요 \ No newline at end of file diff --git a/docs/ko/models/litellm.md b/docs/ko/models/litellm.md index c610be0644..f6db4dd095 100644 --- a/docs/ko/models/litellm.md +++ b/docs/ko/models/litellm.md @@ -5,9 +5,9 @@ search: # LiteLLM -이 페이지는 [Models의 LiteLLM 섹션](index.md#litellm)(으)로 이동되었습니다 +이 페이지는 [Models의 서드파티 어댑터 섹션](index.md#third-party-adapters)으로 이동되었습니다. -자동으로 리디렉션되지 않으면 위 링크를 사용하세요 \ No newline at end of file +자동으로 리디렉션되지 않으면 위 링크를 사용하세요. \ No newline at end of file diff --git a/docs/ko/tracing.md b/docs/ko/tracing.md index 6a2c46e610..cd360544b7 100644 --- a/docs/ko/tracing.md +++ b/docs/ko/tracing.md @@ -4,31 +4,31 @@ search: --- # 트레이싱 -Agents SDK에는 기본 제공 트레이싱이 포함되어 있어 에이전트 실행 중 발생하는 이벤트의 포괄적인 기록을 수집합니다: LLM 생성, 도구 호출, 핸드오프, 가드레일, 그리고 발생하는 사용자 지정 이벤트까지 포함됩니다. [Traces 대시보드](https://platform.openai.com/traces)를 사용하면 개발 중과 프로덕션에서 워크플로를 디버그하고, 시각화하고, 모니터링할 수 있습니다. +Agents SDK에는 기본 제공 트레이싱이 포함되어 있으며, 에이전트 실행 중 발생하는 이벤트의 포괄적인 기록을 수집합니다: LLM 생성, 도구 호출, 핸드오프, 가드레일, 그리고 발생한 사용자 정의 이벤트까지 포함됩니다. [Traces 대시보드](https://platform.openai.com/traces)를 사용하면 개발 중과 프로덕션에서 워크플로를 디버그, 시각화, 모니터링할 수 있습니다. !!!note - 트레이싱은 기본적으로 활성화되어 있습니다. 다음 세 가지 일반적인 방법으로 비활성화할 수 있습니다: + 트레이싱은 기본적으로 활성화되어 있습니다. 일반적으로 다음 세 가지 방법으로 비활성화할 수 있습니다: - 1. 환경 변수 `OPENAI_AGENTS_DISABLE_TRACING=1`을 설정하여 전역으로 트레이싱을 비활성화할 수 있습니다 - 2. 코드에서 [`set_tracing_disabled(True)`][agents.set_tracing_disabled]로 전역으로 트레이싱을 비활성화할 수 있습니다 - 3. 단일 실행에 대해 [`agents.run.RunConfig.tracing_disabled`][]를 `True`로 설정하여 트레이싱을 비활성화할 수 있습니다 + 1. 환경 변수 `OPENAI_AGENTS_DISABLE_TRACING=1` 을 설정해 전역으로 트레이싱을 비활성화할 수 있습니다 + 2. 코드에서 [`set_tracing_disabled(True)`][agents.set_tracing_disabled]로 전역 트레이싱을 비활성화할 수 있습니다 + 3. 단일 실행에 대해 [`agents.run.RunConfig.tracing_disabled`][]를 `True`로 설정해 트레이싱을 비활성화할 수 있습니다 -***OpenAI API를 사용하며 Zero Data Retention (ZDR) 정책 하에서 운영하는 조직에서는 트레이싱을 사용할 수 없습니다.*** +***OpenAI API를 사용하면서 ZDR(Zero Data Retention) 정책으로 운영되는 조직에서는 트레이싱을 사용할 수 없습니다.*** ## 트레이스와 스팬 -- **트레이스**는 "워크플로"의 단일 end-to-end 작업을 나타냅니다. 트레이스는 스팬으로 구성됩니다. 트레이스에는 다음 속성이 있습니다: +- **트레이스**는 "워크플로"의 단일 엔드투엔드 작업을 나타냅니다. 트레이스는 스팬으로 구성됩니다. 트레이스에는 다음 속성이 있습니다: - `workflow_name`: 논리적 워크플로 또는 앱입니다. 예: "Code generation" 또는 "Customer service" - - `trace_id`: 트레이스의 고유 ID입니다. 전달하지 않으면 자동 생성됩니다. 형식은 `trace_<32_alphanumeric>`이어야 합니다 - - `group_id`: 선택적 그룹 ID로, 같은 대화의 여러 트레이스를 연결하는 데 사용합니다. 예를 들어 채팅 스레드 ID를 사용할 수 있습니다 + - `trace_id`: 트레이스의 고유 ID입니다. 전달하지 않으면 자동 생성됩니다. 형식은 `trace_<32_alphanumeric>` 이어야 합니다 + - `group_id`: 선택적 그룹 ID로, 같은 대화의 여러 트레이스를 연결합니다. 예를 들어 채팅 스레드 ID를 사용할 수 있습니다 - `disabled`: True이면 트레이스가 기록되지 않습니다 - - `metadata`: 트레이스의 선택적 메타데이터입니다 -- **스팬**은 시작 시간과 종료 시간을 가진 작업을 나타냅니다. 스팬에는 다음이 있습니다: + - `metadata`: 트레이스에 대한 선택적 메타데이터입니다 +- **스팬**은 시작 시간과 종료 시간이 있는 작업을 나타냅니다. 스팬에는 다음이 있습니다: - `started_at` 및 `ended_at` 타임스탬프 - - `trace_id`: 소속된 트레이스를 나타냅니다 - - `parent_id`: 이 스팬의 상위 스팬을 가리킵니다(있는 경우) - - `span_data`: 스팬에 대한 정보입니다. 예를 들어 `AgentSpanData`는 Agent 정보를, `GenerationSpanData`는 LLM 생성 정보를 포함합니다 + - `trace_id`: 해당 스팬이 속한 트레이스를 나타냅니다 + - `parent_id`: 이 스팬의 부모 스팬을 가리킵니다(있는 경우) + - `span_data`: 스팬에 대한 정보입니다. 예를 들어 `AgentSpanData`에는 에이전트 정보가, `GenerationSpanData`에는 LLM 생성 정보가 포함됩니다 ## 기본 트레이싱 @@ -42,15 +42,15 @@ Agents SDK에는 기본 제공 트레이싱이 포함되어 있어 에이전트 - 핸드오프는 `handoff_span()`으로 감싸집니다 - 오디오 입력(음성-텍스트)은 `transcription_span()`으로 감싸집니다 - 오디오 출력(텍스트-음성)은 `speech_span()`으로 감싸집니다 -- 관련 오디오 스팬은 `speech_group_span()` 하위로 부모 지정될 수 있습니다 +- 관련 오디오 스팬은 `speech_group_span()` 아래에 부모-자식으로 연결될 수 있습니다 -기본적으로 트레이스 이름은 "Agent workflow"입니다. `trace`를 사용할 때 이 이름을 설정할 수 있으며, [`RunConfig`][agents.run.RunConfig]로 이름 및 기타 속성을 구성할 수도 있습니다. +기본적으로 트레이스 이름은 "Agent workflow"입니다. `trace`를 사용하면 이 이름을 설정할 수 있고, [`RunConfig`][agents.run.RunConfig]로 이름과 기타 속성을 구성할 수도 있습니다. -또한 [사용자 지정 트레이스 프로세서](#custom-tracing-processors)를 설정해 트레이스를 다른 대상으로 전송할 수 있습니다(대체 또는 보조 대상). +또한 [사용자 정의 트레이스 프로세서](#custom-tracing-processors)를 설정하여 트레이스를 다른 대상(대체 또는 보조 대상)으로 전송할 수 있습니다. ## 상위 수준 트레이스 -경우에 따라 여러 번의 `run()` 호출을 하나의 트레이스에 포함하고 싶을 수 있습니다. 이 경우 전체 코드를 `trace()`로 감싸면 됩니다. +때로는 `run()` 여러 호출을 단일 트레이스의 일부로 만들고 싶을 수 있습니다. 이 경우 전체 코드를 `trace()`로 감싸면 됩니다. ```python from agents import Agent, Runner, trace @@ -65,60 +65,60 @@ async def main(): print(f"Rating: {second_result.final_output}") ``` -1. 두 번의 `Runner.run` 호출이 `with trace()`로 감싸져 있으므로, 개별 실행은 각각 두 개의 트레이스를 생성하는 대신 전체 트레이스의 일부가 됩니다 +1. `Runner.run`에 대한 두 호출이 `with trace()`로 감싸져 있으므로, 각 실행은 두 개의 트레이스를 생성하는 대신 전체 트레이스의 일부가 됩니다 ## 트레이스 생성 -[`trace()`][agents.tracing.trace] 함수를 사용해 트레이스를 생성할 수 있습니다. 트레이스는 시작과 종료가 필요합니다. 방법은 두 가지입니다: +[`trace()`][agents.tracing.trace] 함수를 사용해 트레이스를 생성할 수 있습니다. 트레이스는 시작 및 종료되어야 하며, 이를 위한 두 가지 방법이 있습니다: -1. **권장**: 트레이스를 컨텍스트 매니저로 사용합니다. 즉, `with trace(...) as my_trace` 형태입니다. 이렇게 하면 적절한 시점에 트레이스가 자동으로 시작되고 종료됩니다 -2. [`trace.start()`][agents.tracing.Trace.start] 및 [`trace.finish()`][agents.tracing.Trace.finish]를 수동으로 호출할 수도 있습니다 +1. **권장**: 트레이스를 컨텍스트 매니저로 사용합니다. 즉 `with trace(...) as my_trace` 형태입니다. 이렇게 하면 적절한 시점에 트레이스가 자동으로 시작되고 종료됩니다 +2. [`trace.start()`][agents.tracing.Trace.start]와 [`trace.finish()`][agents.tracing.Trace.finish]를 수동으로 호출할 수도 있습니다 -현재 트레이스는 Python [`contextvar`](https://docs.python.org/3/library/contextvars.html)를 통해 추적됩니다. 이는 동시성 환경에서도 자동으로 작동함을 의미합니다. 트레이스를 수동으로 시작/종료하는 경우 현재 트레이스를 업데이트하려면 `start()`/`finish()`에 `mark_as_current`와 `reset_current`를 전달해야 합니다. +현재 트레이스는 Python [`contextvar`](https://docs.python.org/3/library/contextvars.html)를 통해 추적됩니다. 즉, 동시성에서도 자동으로 동작합니다. 트레이스를 수동으로 시작/종료하는 경우 현재 트레이스를 업데이트하려면 `start()`/`finish()`에 `mark_as_current` 및 `reset_current`를 전달해야 합니다. ## 스팬 생성 -다양한 [`*_span()`][agents.tracing.create] 메서드를 사용해 스팬을 생성할 수 있습니다. 일반적으로 스팬을 수동으로 생성할 필요는 없습니다. 사용자 지정 스팬 정보를 추적하기 위한 [`custom_span()`][agents.tracing.custom_span] 함수도 제공됩니다. +다양한 [`*_span()`][agents.tracing.create] 메서드를 사용해 스팬을 생성할 수 있습니다. 일반적으로 스팬을 수동으로 생성할 필요는 없습니다. 사용자 정의 스팬 정보를 추적하기 위한 [`custom_span()`][agents.tracing.custom_span] 함수도 제공됩니다. 스팬은 자동으로 현재 트레이스의 일부가 되며, Python [`contextvar`](https://docs.python.org/3/library/contextvars.html)로 추적되는 가장 가까운 현재 스팬 아래에 중첩됩니다. -## 민감 데이터 +## 민감한 데이터 -일부 스팬은 잠재적으로 민감한 데이터를 캡처할 수 있습니다. +특정 스팬은 잠재적으로 민감한 데이터를 캡처할 수 있습니다. `generation_span()`은 LLM 생성의 입력/출력을 저장하고, `function_span()`은 함수 호출의 입력/출력을 저장합니다. 여기에는 민감한 데이터가 포함될 수 있으므로 [`RunConfig.trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data]를 통해 해당 데이터 캡처를 비활성화할 수 있습니다. -마찬가지로 Audio 스팬은 기본적으로 입력 및 출력 오디오에 대한 base64 인코딩 PCM 데이터를 포함합니다. [`VoicePipelineConfig.trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data]를 구성해 이 오디오 데이터 캡처를 비활성화할 수 있습니다. +마찬가지로, 오디오 스팬은 기본적으로 입력 및 출력 오디오에 대한 base64 인코딩 PCM 데이터를 포함합니다. [`VoicePipelineConfig.trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data]를 구성하여 이 오디오 데이터 캡처를 비활성화할 수 있습니다. -기본적으로 `trace_include_sensitive_data`는 `True`입니다. 앱 실행 전에 `OPENAI_AGENTS_TRACE_INCLUDE_SENSITIVE_DATA` 환경 변수를 `true/1` 또는 `false/0`으로 export하여 코드 변경 없이 기본값을 설정할 수 있습니다. +기본적으로 `trace_include_sensitive_data`는 `True`입니다. 코드 없이 기본값을 설정하려면 앱 실행 전에 `OPENAI_AGENTS_TRACE_INCLUDE_SENSITIVE_DATA` 환경 변수를 `true/1` 또는 `false/0`으로 export하면 됩니다. -## 사용자 지정 트레이싱 프로세서 +## 사용자 정의 트레이싱 프로세서 -트레이싱의 상위 수준 아키텍처는 다음과 같습니다: +트레이싱의 상위 아키텍처는 다음과 같습니다: -- 초기화 시 트레이스 생성을 담당하는 전역 [`TraceProvider`][agents.tracing.setup.TraceProvider]를 생성합니다 -- `TraceProvider`를 [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor]로 구성하며, 이 프로세서는 트레이스/스팬을 배치로 [`BackendSpanExporter`][agents.tracing.processors.BackendSpanExporter]에 전송합니다. `BackendSpanExporter`는 스팬과 트레이스를 배치로 OpenAI 백엔드에 내보냅니다 +- 초기화 시, 트레이스를 생성하는 역할을 담당하는 전역 [`TraceProvider`][agents.tracing.setup.TraceProvider]를 생성합니다 +- `TraceProvider`를 [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor]로 구성하고, 이 프로세서는 트레이스/스팬을 배치로 [`BackendSpanExporter`][agents.tracing.processors.BackendSpanExporter]에 전송합니다. `BackendSpanExporter`는 스팬과 트레이스를 배치로 OpenAI 백엔드에 내보냅니다 -이 기본 설정을 사용자 지정하여 트레이스를 대체 또는 추가 백엔드로 전송하거나 exporter 동작을 수정하려면 두 가지 옵션이 있습니다: +이 기본 설정을 사용자 정의해 트레이스를 대체 또는 추가 백엔드로 전송하거나 exporter 동작을 수정하려면 두 가지 방법이 있습니다: -1. [`add_trace_processor()`][agents.tracing.add_trace_processor]를 사용하면 준비되는 즉시 트레이스와 스팬을 받는 **추가** 트레이스 프로세서를 더할 수 있습니다. 이를 통해 OpenAI 백엔드로 전송하는 것과 별도로 자체 처리를 수행할 수 있습니다 -2. [`set_trace_processors()`][agents.tracing.set_trace_processors]를 사용하면 기본 프로세서를 사용자 지정 트레이스 프로세서로 **대체**할 수 있습니다. 이 경우 해당 기능을 수행하는 `TracingProcessor`를 포함하지 않으면 트레이스가 OpenAI 백엔드로 전송되지 않습니다 +1. [`add_trace_processor()`][agents.tracing.add_trace_processor]를 사용하면 준비되는 즉시 트레이스와 스팬을 수신하는 **추가** 트레이스 프로세서를 추가할 수 있습니다. 이를 통해 OpenAI 백엔드로 전송하는 것에 더해 자체 처리를 수행할 수 있습니다 +2. [`set_trace_processors()`][agents.tracing.set_trace_processors]를 사용하면 기본 프로세서를 사용자 정의 트레이스 프로세서로 **대체**할 수 있습니다. 즉, 이를 수행하는 `TracingProcessor`를 포함하지 않으면 트레이스는 OpenAI 백엔드로 전송되지 않습니다 -## OpenAI가 아닌 모델에서의 트레이싱 +## 비 OpenAI 모델에서의 트레이싱 -트레이싱을 비활성화할 필요 없이 OpenAI Traces 대시보드에서 무료 트레이싱을 활성화하기 위해 OpenAI API 키를 OpenAI가 아닌 모델과 함께 사용할 수 있습니다. +OpenAI API 키를 비 OpenAI 모델과 함께 사용하면 트레이싱을 비활성화하지 않고도 OpenAI Traces 대시보드에서 무료 트레이싱을 사용할 수 있습니다. 어댑터 선택 및 설정 시 주의사항은 Models 가이드의 [서드파티 어댑터](models/index.md#third-party-adapters) 섹션을 참고하세요. ```python import os from agents import set_tracing_export_api_key, Agent, Runner -from agents.extensions.models.litellm_model import LitellmModel +from agents.extensions.models.any_llm_model import AnyLLMModel tracing_api_key = os.environ["OPENAI_API_KEY"] set_tracing_export_api_key(tracing_api_key) -model = LitellmModel( - model="your-model-name", +model = AnyLLMModel( + model="your-provider/your-model-name", api_key="your-api-key", ) diff --git a/docs/ko/usage.md b/docs/ko/usage.md index 7994c3ae88..9eb5d87e98 100644 --- a/docs/ko/usage.md +++ b/docs/ko/usage.md @@ -2,9 +2,9 @@ search: exclude: true --- -# 사용법 +# 사용 -Agents SDK는 모든 실행의 토큰 사용량을 자동으로 추적합니다. 실행 컨텍스트에서 이를 확인하고 비용 모니터링, 한도 적용, 분석 기록에 활용할 수 있습니다 +Agents SDK는 모든 실행에 대해 토큰 사용량을 자동으로 추적합니다. 실행 컨텍스트에서 이를 확인하여 비용 모니터링, 제한 적용, 분석 기록에 활용할 수 있습니다. ## 추적 항목 @@ -17,9 +17,9 @@ Agents SDK는 모든 실행의 토큰 사용량을 자동으로 추적합니다. - `input_tokens_details.cached_tokens` - `output_tokens_details.reasoning_tokens` -## 실행에서 사용량 액세스 +## 실행에서 사용량 접근 -`Runner.run(...)` 이후 `result.context_wrapper.usage`로 사용량에 액세스합니다 +`Runner.run(...)` 이후 `result.context_wrapper.usage`를 통해 사용량에 접근할 수 있습니다. ```python result = await Runner.run(agent, "What's the weather in Tokyo?") @@ -31,29 +31,20 @@ print("Output tokens:", usage.output_tokens) print("Total tokens:", usage.total_tokens) ``` -사용량은 실행 중 발생한 모든 모델 호출(도구 호출 및 핸드오프 포함)에 대해 집계됩니다 +사용량은 실행 중 발생한 모든 모델 호출(도구 호출 및 핸드오프 포함)에 걸쳐 집계됩니다. -### LiteLLM 모델에서 사용량 활성화 +### 서드파티 어댑터에서 사용량 활성화 -LiteLLM 제공자는 기본적으로 사용량 지표를 보고하지 않습니다. [`LitellmModel`][agents.extensions.models.litellm_model.LitellmModel]을 사용하는 경우, LiteLLM 응답이 `result.context_wrapper.usage`를 채우도록 에이전트에 `ModelSettings(include_usage=True)`를 전달하세요. 설정 안내와 코드 예제는 Models 가이드의 [LiteLLM note](models/index.md#litellm)를 참고하세요 +사용량 보고는 서드파티 어댑터와 제공자 백엔드에 따라 달라집니다. 어댑터 기반 모델을 사용하고 정확한 `result.context_wrapper.usage` 값이 필요하다면 다음을 확인하세요: -```python -from agents import Agent, ModelSettings, Runner -from agents.extensions.models.litellm_model import LitellmModel - -agent = Agent( - name="Assistant", - model=LitellmModel(model="your/model", api_key="..."), - model_settings=ModelSettings(include_usage=True), -) +- `AnyLLMModel`에서는 업스트림 제공자가 사용량을 반환하면 자동으로 전파됩니다. 스트리밍 Chat Completions 백엔드의 경우, 사용량 청크가 전송되기 전에 `ModelSettings(include_usage=True)`가 필요할 수 있습니다 +- `LitellmModel`에서는 일부 제공자 백엔드가 기본적으로 사용량을 보고하지 않으므로, `ModelSettings(include_usage=True)`가 자주 필요합니다 -result = await Runner.run(agent, "What's the weather in Tokyo?") -print(result.context_wrapper.usage.total_tokens) -``` +모델 가이드의 [서드파티 어댑터](models/index.md#third-party-adapters) 섹션에서 어댑터별 참고 사항을 확인하고, 배포 예정인 정확한 제공자 백엔드를 검증하세요. ## 요청별 사용량 추적 -SDK는 `request_usage_entries`에서 각 API 요청의 사용량을 자동으로 추적하며, 이는 상세 비용 계산과 컨텍스트 윈도우 사용량 모니터링에 유용합니다 +SDK는 `request_usage_entries`에서 각 API 요청의 사용량을 자동으로 추적하므로, 상세한 비용 계산과 컨텍스트 윈도 소비량 모니터링에 유용합니다. ```python result = await Runner.run(agent, "What's the weather in Tokyo?") @@ -62,9 +53,9 @@ for i, request in enumerate(result.context_wrapper.usage.request_usage_entries): print(f"Request {i + 1}: {request.input_tokens} in, {request.output_tokens} out") ``` -## 세션에서 사용량 액세스 +## 세션에서 사용량 접근 -`Session`(예: `SQLiteSession`)을 사용할 때 `Runner.run(...)`을 호출할 때마다 해당 실행에 대한 사용량이 반환됩니다. 세션은 컨텍스트를 위해 대화 기록을 유지하지만, 각 실행의 사용량은 서로 독립적입니다 +`Session`(예: `SQLiteSession`)을 사용할 때 `Runner.run(...)`의 각 호출은 해당 실행에 대한 사용량을 반환합니다. 세션은 컨텍스트를 위해 대화 이력을 유지하지만, 각 실행의 사용량은 서로 독립적입니다. ```python session = SQLiteSession("my_conversation") @@ -76,11 +67,11 @@ second = await Runner.run(agent, "Can you elaborate?", session=session) print(second.context_wrapper.usage.total_tokens) # Usage for second run ``` -세션은 실행 간 대화 컨텍스트를 보존하지만, 각 `Runner.run()` 호출에서 반환되는 사용량 지표는 해당 실행만을 나타냅니다. 세션에서는 이전 메시지가 각 실행의 입력으로 다시 전달될 수 있으며, 이는 이후 턴의 입력 토큰 수에 영향을 줍니다 +세션은 실행 간 대화 컨텍스트를 보존하지만, 각 `Runner.run()` 호출에서 반환되는 사용량 지표는 해당 실행만을 나타냅니다. 세션에서는 이전 메시지가 각 실행의 입력으로 다시 주입될 수 있으며, 이는 이후 턴의 입력 토큰 수에 영향을 줍니다. ## 훅에서 사용량 활용 -`RunHooks`를 사용하는 경우 각 훅에 전달되는 `context` 객체에 `usage`가 포함됩니다. 이를 통해 주요 라이프사이클 시점에 사용량을 기록할 수 있습니다 +`RunHooks`를 사용하는 경우, 각 훅에 전달되는 `context` 객체에 `usage`가 포함됩니다. 이를 통해 주요 라이프사이클 시점에 사용량을 기록할 수 있습니다. ```python class MyHooks(RunHooks): @@ -95,5 +86,5 @@ class MyHooks(RunHooks): - [`Usage`][agents.usage.Usage] - 사용량 추적 데이터 구조 - [`RequestUsage`][agents.usage.RequestUsage] - 요청별 사용량 세부 정보 -- [`RunContextWrapper`][agents.run.RunContextWrapper] - 실행 컨텍스트에서 사용량 액세스 +- [`RunContextWrapper`][agents.run.RunContextWrapper] - 실행 컨텍스트에서 사용량 접근 - [`RunHooks`][agents.run.RunHooks] - 사용량 추적 라이프사이클에 훅 연결 \ No newline at end of file diff --git a/docs/zh/examples.md b/docs/zh/examples.md index 8ff4c17215..4ca594faf4 100644 --- a/docs/zh/examples.md +++ b/docs/zh/examples.md @@ -4,62 +4,62 @@ search: --- # 示例 -请在 [repo](https://github.com/openai/openai-agents-python/tree/main/examples) 的示例部分查看 SDK 的多种 sample code。这些示例按多个目录组织,用于展示不同的模式与能力。 +在[repo](https://github.com/openai/openai-agents-python/tree/main/examples)的示例部分查看 SDK 的各种 sample code。这些示例按多个目录组织,展示了不同的模式与能力。 ## 目录 - **[agent_patterns](https://github.com/openai/openai-agents-python/tree/main/examples/agent_patterns):** - 此目录中的示例展示了常见的智能体设计模式,例如 + 该目录中的示例展示了常见的智能体设计模式,例如 - 确定性工作流 - Agents as tools - 并行智能体执行 - 条件化工具使用 - 输入/输出安全防护措施 - - LLM 作为评审 + - LLM 作为评判者 - 路由 - 流式传输安全防护措施 - - 审批流程的自定义拒绝消息(`examples/agent_patterns/human_in_the_loop_custom_rejection.py`) + - 用于审批流程的自定义拒绝消息(`examples/agent_patterns/human_in_the_loop_custom_rejection.py`) - **[basic](https://github.com/openai/openai-agents-python/tree/main/examples/basic):** 这些示例展示了 SDK 的基础能力,例如 - - Hello World 示例(默认模型、GPT-5、开源权重模型) + - Hello world 示例(默认模型、GPT-5、开放权重模型) - 智能体生命周期管理 - 动态系统提示词 - 流式传输输出(文本、条目、函数调用参数) - - 跨多轮共享会话辅助器的 Responses websocket 传输(`examples/basic/stream_ws.py`) + - 跨轮次使用共享会话助手的 Responses websocket 传输(`examples/basic/stream_ws.py`) - 提示词模板 - - 文件处理(本地与远程、图像与 PDF) - - 用量追踪 - - Runner 管理的重试设置(`examples/basic/retry.py`) - - 通过 LiteLLM 使用 Runner 管理的重试(`examples/basic/retry_litellm.py`) + - 文件处理(本地与远程,图像与 PDF) + - 用量跟踪 + - 由 Runner 管理的重试设置(`examples/basic/retry.py`) + - 通过第三方适配器进行由 Runner 管理的重试(`examples/basic/retry_litellm.py`) - 非严格输出类型 - - 上一个 response ID 的用法 + - 先前响应 ID 的使用 - **[customer_service](https://github.com/openai/openai-agents-python/tree/main/examples/customer_service):** - 航空公司客户服务系统示例。 + 面向航空公司的客户服务系统示例。 - **[financial_research_agent](https://github.com/openai/openai-agents-python/tree/main/examples/financial_research_agent):** - 一个金融研究智能体,展示了用于金融数据分析的、结合智能体与工具的结构化研究工作流。 + 一个金融研究智能体,展示了使用智能体与工具进行金融数据分析的结构化研究工作流。 - **[handoffs](https://github.com/openai/openai-agents-python/tree/main/examples/handoffs):** 查看带有消息过滤的智能体任务转移实践示例。 - **[hosted_mcp](https://github.com/openai/openai-agents-python/tree/main/examples/hosted_mcp):** - 展示如何使用托管 MCP(Model context protocol)连接器和审批流程的示例。 + 演示如何使用托管 MCP(Model Context Protocol)连接器与审批的示例。 - **[mcp](https://github.com/openai/openai-agents-python/tree/main/examples/mcp):** - 了解如何基于 MCP(Model context protocol)构建智能体,包括: + 了解如何使用 MCP(Model Context Protocol)构建智能体,包括: - 文件系统示例 - Git 示例 - MCP 提示词服务示例 - - SSE(服务端发送事件)示例 + - SSE(Server-Sent Events)示例 - 可流式 HTTP 示例 - **[memory](https://github.com/openai/openai-agents-python/tree/main/examples/memory):** - 智能体的不同内存实现示例,包括: + 不同智能体记忆实现的示例,包括: - SQLite 会话存储 - 高级 SQLite 会话存储 @@ -72,32 +72,32 @@ search: - 使用 `ModelSettings(store=False)` 的无状态 Responses 压缩(`examples/memory/compaction_session_stateless_example.py`) - **[model_providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers):** - 探索如何在 SDK 中使用非 OpenAI 模型,包括自定义提供方和 LiteLLM 集成。 + 探索如何在 SDK 中使用非 OpenAI 模型,包括自定义提供方和第三方适配器。 - **[realtime](https://github.com/openai/openai-agents-python/tree/main/examples/realtime):** 展示如何使用 SDK 构建实时体验的示例,包括: - 使用结构化文本与图像消息的 Web 应用模式 - 命令行音频循环与播放处理 - - 基于 WebSocket 的 Twilio Media Streams 集成 + - 通过 WebSocket 集成 Twilio Media Streams - 使用 Realtime Calls API 附加流程的 Twilio SIP 集成 - **[reasoning_content](https://github.com/openai/openai-agents-python/tree/main/examples/reasoning_content):** - 展示如何处理推理内容与 structured outputs 的示例。 + 演示如何处理推理内容和 structured outputs 的示例。 - **[research_bot](https://github.com/openai/openai-agents-python/tree/main/examples/research_bot):** - 简单的深度研究克隆,展示复杂的多智能体研究工作流。 + 简单的深度研究克隆,展示了复杂的多智能体研究工作流。 - **[tools](https://github.com/openai/openai-agents-python/tree/main/examples/tools):** 了解如何实现由OpenAI托管的工具和实验性 Codex 工具能力,例如: - - 网络检索及带过滤器的网络检索 + - 网络检索与带过滤器的网络检索 - 文件检索 - 代码解释器 - 带内联技能的托管容器 shell(`examples/tools/container_shell_inline_skill.py`) - 带技能引用的托管容器 shell(`examples/tools/container_shell_skill_reference.py`) - 带本地技能的本地 shell(`examples/tools/local_shell_skill.py`) - - 带命名空间和延迟工具的工具检索(`examples/tools/tool_search.py`) + - 带命名空间与延迟工具的工具搜索(`examples/tools/tool_search.py`) - 计算机操作 - 图像生成 - 实验性 Codex 工具工作流(`examples/tools/codex.py`) diff --git a/docs/zh/models/index.md b/docs/zh/models/index.md index 0f2f1cfbab..02a98a3acb 100644 --- a/docs/zh/models/index.md +++ b/docs/zh/models/index.md @@ -4,42 +4,42 @@ search: --- # 模型 -Agents SDK 开箱即用支持两种形式的 OpenAI 模型: +Agents SDK 对 OpenAI 模型提供开箱即用的两种支持方式: -- **推荐**:[`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel],使用新的 [Responses API](https://platform.openai.com/docs/api-reference/responses) 调用 OpenAI API。 -- [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel],使用 [Chat Completions API](https://platform.openai.com/docs/api-reference/chat) 调用 OpenAI API。 +- **推荐**:[`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel],通过新的[Responses API](https://platform.openai.com/docs/api-reference/responses)调用 OpenAI API。 +- [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel],通过[Chat Completions API](https://platform.openai.com/docs/api-reference/chat)调用 OpenAI API。 ## 模型设置选择 -先从最适合你当前设置的最简单路径开始: +从最适合你当前设置的最简单路径开始: -| 如果你想要…… | 推荐路径 | 了解更多 | +| 如果你想要... | 推荐路径 | 了解更多 | | --- | --- | --- | -| 仅使用 OpenAI 模型 | 使用默认 OpenAI provider 的 Responses 模型路径 | [OpenAI 模型](#openai-models) | +| 仅使用 OpenAI 模型 | 使用默认 OpenAI provider 与 Responses 模型路径 | [OpenAI 模型](#openai-models) | | 通过 websocket 传输使用 OpenAI Responses API | 保持 Responses 模型路径并启用 websocket 传输 | [Responses WebSocket 传输](#responses-websocket-transport) | | 使用一个非 OpenAI provider | 从内置 provider 集成点开始 | [非 OpenAI 模型](#non-openai-models) | -| 在多个智能体之间混用模型或 provider | 按每次 run 或每个智能体选择 provider,并检查功能差异 | [在单个工作流中混用模型](#mixing-models-in-one-workflow) 和 [跨 provider 混用模型](#mixing-models-across-providers) | +| 在智能体之间混合模型或 providers | 按每次运行或每个智能体选择 provider,并检查功能差异 | [在单个工作流中混合模型](#mixing-models-in-one-workflow) 和 [跨 providers 混合模型](#mixing-models-across-providers) | | 调整高级 OpenAI Responses 请求设置 | 在 OpenAI Responses 路径上使用 `ModelSettings` | [高级 OpenAI Responses 设置](#advanced-openai-responses-settings) | -| 为非 OpenAI Chat Completions provider 使用 LiteLLM | 将 LiteLLM 视为 beta 备用方案 | [LiteLLM](#litellm) | +| 使用第三方适配器进行非 OpenAI 或混合 provider 路由 | 比较受支持的 beta 适配器,并验证你计划上线的 provider 路径 | [第三方适配器](#third-party-adapters) | ## OpenAI 模型 -对于大多数仅使用 OpenAI 的应用,推荐路径是使用字符串模型名称配合默认 OpenAI provider,并保持在 Responses 模型路径上。 +对于大多数仅使用 OpenAI 的应用,推荐路径是使用字符串模型名与默认 OpenAI provider,并保持在 Responses 模型路径上。 -当你在初始化 `Agent` 时未指定模型,将使用默认模型。当前默认模型是 [`gpt-4.1`](https://developers.openai.com/api/docs/models/gpt-4.1),以兼顾兼容性和低延迟。如果你有权限,我们建议将智能体设置为 [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4) 以获得更高质量,同时保持显式 `model_settings`。 +当你在初始化 `Agent` 时未指定模型,将使用默认模型。当前默认是 [`gpt-4.1`](https://developers.openai.com/api/docs/models/gpt-4.1),以兼顾兼容性与低延迟。如果你有权限,我们建议将智能体设置为 [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4) 以获得更高质量,同时显式设置 `model_settings`。 -如果你想切换到 [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4) 等其他模型,可通过两种方式配置智能体。 +如果你想切换到其他模型(如 [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4)),有两种方式配置智能体。 ### 默认模型 -首先,如果你希望所有未设置自定义模型的智能体都持续使用某个特定模型,请在运行智能体前设置 `OPENAI_DEFAULT_MODEL` 环境变量。 +首先,如果你希望所有未设置自定义模型的智能体都稳定使用某个特定模型,请在运行智能体前设置 `OPENAI_DEFAULT_MODEL` 环境变量。 ```bash export OPENAI_DEFAULT_MODEL=gpt-5.4 python3 my_awesome_agent.py ``` -其次,你可以通过 `RunConfig` 为一次 run 设置默认模型。如果你未为智能体设置模型,将使用该 run 的模型。 +其次,你可以通过 `RunConfig` 为一次运行设置默认模型。如果某个智能体未设置模型,将使用该运行的模型。 ```python from agents import Agent, RunConfig, Runner @@ -58,7 +58,7 @@ result = await Runner.run( #### GPT-5 模型 -当你以这种方式使用任意 GPT-5 模型(如 [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4))时,SDK 会应用默认 `ModelSettings`。它会设置最适合大多数用例的选项。若要调整默认模型的推理强度,请传入你自己的 `ModelSettings`: +当你以这种方式使用任意 GPT-5 模型(例如 [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4))时,SDK 会应用默认 `ModelSettings`。它会设置适用于大多数场景的最佳选项。若要调整默认模型的推理强度,请传入你自己的 `ModelSettings`: ```python from openai.types.shared import Reasoning @@ -74,21 +74,21 @@ my_agent = Agent( ) ``` -为了降低延迟,建议在 `gpt-5.4` 上使用 `reasoning.effort="none"`。gpt-4.1 系列(包括 mini 和 nano 变体)在构建交互式智能体应用时也依然是稳健选择。 +为了更低延迟,建议在 `gpt-5.4` 上使用 `reasoning.effort="none"`。gpt-4.1 系列(包括 mini 和 nano 变体)在构建交互式智能体应用时也依然是稳健选择。 #### ComputerTool 模型选择 -如果智能体包含 [`ComputerTool`][agents.tool.ComputerTool],则实际 Responses 请求中生效的模型会决定 SDK 发送哪种 computer-tool 载荷。显式 `gpt-5.4` 请求会使用 GA 内置 `computer` 工具,而显式 `computer-use-preview` 请求会保留旧的 `computer_use_preview` 载荷。 +如果智能体包含 [`ComputerTool`][agents.tool.ComputerTool],实际 Responses 请求中的生效模型将决定 SDK 发送哪种 computer-tool 负载。显式的 `gpt-5.4` 请求会使用 GA 内置 `computer` 工具;显式的 `computer-use-preview` 请求则保持旧版 `computer_use_preview` 负载。 -由提示词管理的调用是主要例外。如果提示词模板持有模型且 SDK 在请求中省略 `model`,SDK 会默认使用与 preview 兼容的 computer 载荷,以避免猜测提示词绑定了哪个模型。要在该流程中保持 GA 路径,可在请求中显式设置 `model="gpt-5.4"`,或通过 `ModelSettings(tool_choice="computer")` 或 `ModelSettings(tool_choice="computer_use")` 强制使用 GA 选择器。 +由提示词管理的调用是主要例外。如果提示模板拥有模型且 SDK 在请求中省略 `model`,SDK 会默认使用与 preview 兼容的 computer 负载,以避免猜测提示绑定了哪个模型。要在该流程中保持 GA 路径,可在请求中显式设置 `model="gpt-5.4"`,或通过 `ModelSettings(tool_choice="computer")` 或 `ModelSettings(tool_choice="computer_use")` 强制 GA 选择器。 -注册了 [`ComputerTool`][agents.tool.ComputerTool] 后,`tool_choice="computer"`、`"computer_use"` 和 `"computer_use_preview"` 会被规范化为与生效请求模型匹配的内置选择器。如果未注册 `ComputerTool`,这些字符串会继续按普通函数名处理。 +在已注册 [`ComputerTool`][agents.tool.ComputerTool] 的情况下,`tool_choice="computer"`、`"computer_use"` 和 `"computer_use_preview"` 会被标准化为与生效请求模型匹配的内置选择器。如果未注册 `ComputerTool`,这些字符串会继续按普通函数名处理。 -与 preview 兼容的请求必须预先序列化 `environment` 和显示尺寸,因此使用 [`ComputerProvider`][agents.tool.ComputerProvider] 工厂的提示词管理流程应在发送请求前传入具体的 `Computer` 或 `AsyncComputer` 实例,或强制 GA 选择器。完整迁移细节见 [工具](../tools.md#computertool-and-the-responses-computer-tool)。 +与 preview 兼容的请求必须预先序列化 `environment` 和显示尺寸,因此使用 [`ComputerProvider`][agents.tool.ComputerProvider] 工厂的提示词管理流程应传入具体的 `Computer` 或 `AsyncComputer` 实例,或在发送请求前强制使用 GA 选择器。完整迁移细节见 [Tools](../tools.md#computertool-and-the-responses-computer-tool)。 #### 非 GPT-5 模型 -如果你传入非 GPT-5 模型名且未自定义 `model_settings`,SDK 会回退为与任意模型兼容的通用 `ModelSettings`。 +如果你传入非 GPT-5 模型名且未提供自定义 `model_settings`,SDK 会回退到与任意模型兼容的通用 `ModelSettings`。 ### 仅 Responses 的工具检索功能 @@ -98,7 +98,7 @@ my_agent = Agent( - [`tool_namespace()`][agents.tool.tool_namespace] - `@function_tool(defer_loading=True)` 以及其他延迟加载的 Responses 工具接口 -这些功能在 Chat Completions 模型和非 Responses 后端上会被拒绝。使用延迟加载工具时,请将 `ToolSearchTool()` 添加到智能体,并让模型通过 `auto` 或 `required` 的工具选择来加载工具,而不是强制使用裸命名空间名称或仅延迟加载函数名。设置细节与当前限制见 [工具](../tools.md#hosted-tool-search)。 +这些功能在 Chat Completions 模型和非 Responses 后端上会被拒绝。当你使用延迟加载工具时,请将 `ToolSearchTool()` 添加到智能体,并让模型通过 `auto` 或 `required` 的 tool choice 加载工具,而不是强制使用裸命名空间名或仅延迟加载的函数名。设置细节与当前限制见 [Tools](../tools.md#hosted-tool-search)。 ### Responses WebSocket 传输 @@ -112,13 +112,13 @@ from agents import set_default_openai_responses_transport set_default_openai_responses_transport("websocket") ``` -这会影响由默认 OpenAI provider 解析的 OpenAI Responses 模型(包括 `"gpt-5.4"` 这样的字符串模型名)。 +这会影响由默认 OpenAI provider 解析出的 OpenAI Responses 模型(包括如 `"gpt-5.4"` 的字符串模型名)。 -传输方式的选择发生在 SDK 将模型名解析为模型实例时。如果你传入具体的 [`Model`][agents.models.interface.Model] 对象,其传输方式已固定:[`OpenAIResponsesWSModel`][agents.models.openai_responses.OpenAIResponsesWSModel] 使用 websocket,[`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] 使用 HTTP,[`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] 保持 Chat Completions。若你传入 `RunConfig(model_provider=...)`,则由该 provider 控制传输选择,而不是全局默认值。 +传输方式的选择发生在 SDK 将模型名解析为模型实例时。如果你传入具体的 [`Model`][agents.models.interface.Model] 对象,其传输方式已固定:[`OpenAIResponsesWSModel`][agents.models.openai_responses.OpenAIResponsesWSModel] 使用 websocket,[`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] 使用 HTTP,而 [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] 保持使用 Chat Completions。若传入 `RunConfig(model_provider=...)`,则由该 provider 控制传输选择而非全局默认值。 -#### Provider 或 run 级设置 +#### Provider 或运行级设置 -你也可以按 provider 或按 run 配置 websocket 传输: +你也可以按 provider 或按运行配置 websocket 传输: ```python from agents import Agent, OpenAIProvider, RunConfig, Runner @@ -139,14 +139,14 @@ result = await Runner.run( #### 使用 `MultiProvider` 的高级路由 -如果你需要基于前缀的模型路由(例如在一次 run 中混用 `openai/...` 和 `litellm/...` 模型名),请使用 [`MultiProvider`][agents.MultiProvider],并在其中设置 `openai_use_responses_websocket=True`。 +如果你需要基于前缀的模型路由(例如在一次运行中混用 `openai/...` 和 `any-llm/...` 模型名),请使用 [`MultiProvider`][agents.MultiProvider],并在其中设置 `openai_use_responses_websocket=True`。 -`MultiProvider` 保留了两个历史默认行为: +`MultiProvider` 保留两个历史默认行为: -- `openai/...` 被视为 OpenAI provider 的别名,因此 `openai/gpt-4.1` 会被路由为模型 `gpt-4.1`。 +- `openai/...` 被视为 OpenAI provider 的别名,因此 `openai/gpt-4.1` 会按模型 `gpt-4.1` 路由。 - 未知前缀会抛出 `UserError`,而不是透传。 -当你将 OpenAI provider 指向一个期望字面命名空间模型 ID 的 OpenAI 兼容端点时,请显式启用透传行为。在启用 websocket 的设置中,也要在 `MultiProvider` 上保持 `openai_use_responses_websocket=True`: +当你将 OpenAI provider 指向一个期望字面量命名空间模型 ID 的 OpenAI 兼容端点时,请显式启用透传行为。在启用 websocket 的设置中,也请在 `MultiProvider` 上保持 `openai_use_responses_websocket=True`: ```python from agents import Agent, MultiProvider, RunConfig, Runner @@ -172,48 +172,48 @@ result = await Runner.run( ) ``` -当后端期望字面字符串 `openai/...` 时,使用 `openai_prefix_mode="model_id"`。当后端期望其他命名空间模型 ID(如 `openrouter/openai/gpt-4.1-mini`)时,使用 `unknown_prefix_mode="model_id"`。这些选项在非 websocket 传输的 `MultiProvider` 上同样可用;本示例保持 websocket 启用,因为它属于本节描述的传输设置。相同选项也可用于 [`responses_websocket_session()`][agents.responses_websocket_session]。 +当后端期望字面量 `openai/...` 字符串时,使用 `openai_prefix_mode="model_id"`。当后端期望其他命名空间模型 ID(如 `openrouter/openai/gpt-4.1-mini`)时,使用 `unknown_prefix_mode="model_id"`。这些选项在非 websocket 传输的 `MultiProvider` 上同样适用;本示例保持 websocket 启用,因为它属于本节描述的传输设置。相同选项也可用于 [`responses_websocket_session()`][agents.responses_websocket_session]。 -如果你使用自定义 OpenAI 兼容端点或代理,websocket 传输还要求兼容的 websocket `/responses` 端点。在这些设置中,你可能需要显式设置 `websocket_base_url`。 +如果你使用自定义 OpenAI 兼容端点或代理,websocket 传输还要求有兼容的 websocket `/responses` 端点。在这些设置下,你可能需要显式设置 `websocket_base_url`。 #### 说明 -- 这是通过 websocket 传输的 Responses API,不是 [Realtime API](../realtime/guide.md)。它不适用于 Chat Completions 或非 OpenAI provider,除非它们支持 Responses websocket `/responses` 端点。 +- 这是基于 websocket 传输的 Responses API,不是[Realtime API](../realtime/guide.md)。除非支持 Responses websocket `/responses` 端点,否则不适用于 Chat Completions 或非 OpenAI providers。 - 如果你的环境中尚未安装,请安装 `websockets` 包。 -- 启用 websocket 传输后,你可以直接使用 [`Runner.run_streamed()`][agents.run.Runner.run_streamed]。对于希望在多轮工作流(以及嵌套 agent-as-tool 调用)中复用同一 websocket 连接的场景,推荐使用 [`responses_websocket_session()`][agents.responses_websocket_session] 辅助函数。参见 [运行智能体](../running_agents.md) 指南和 [`examples/basic/stream_ws.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/stream_ws.py)。 +- 启用 websocket 传输后,你可以直接使用 [`Runner.run_streamed()`][agents.run.Runner.run_streamed]。对于希望在多轮流程中跨轮次(以及嵌套 agent-as-tool 调用)复用同一 websocket 连接的场景,推荐使用 [`responses_websocket_session()`][agents.responses_websocket_session] 辅助方法。参见[运行智能体](../running_agents.md)指南和 [`examples/basic/stream_ws.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/stream_ws.py)。 ## 非 OpenAI 模型 -如果你需要非 OpenAI provider,请先从 SDK 内置 provider 集成点开始。在很多设置中,无需添加 LiteLLM 就足够了。每种模式的示例位于 [examples/model_providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/)。 +如果你需要非 OpenAI provider,请先使用 SDK 的内置 provider 集成点。在许多设置中,这已经足够,无需增加第三方适配器。每种模式的示例见 [examples/model_providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/)。 ### 非 OpenAI provider 集成方式 | 方式 | 适用场景 | 范围 | | --- | --- | --- | -| [`set_default_openai_client`][agents.set_default_openai_client] | 一个 OpenAI 兼容端点应作为大多数或全部智能体的默认值 | 全局默认 | -| [`ModelProvider`][agents.models.interface.ModelProvider] | 一个自定义 provider 应用于单次 run | 每次 run | +| [`set_default_openai_client`][agents.set_default_openai_client] | 一个 OpenAI 兼容端点应作为大多数或所有智能体的默认值 | 全局默认 | +| [`ModelProvider`][agents.models.interface.ModelProvider] | 一个自定义 provider 应用于单次运行 | 每次运行 | | [`Agent.model`][agents.agent.Agent.model] | 不同智能体需要不同 provider 或具体模型对象 | 每个智能体 | -| LiteLLM(beta) | 你需要 LiteLLM 特有的 provider 覆盖或路由 | 见 [LiteLLM](#litellm) | +| 第三方适配器 | 你需要内置路径未提供的适配器管理 provider 覆盖或路由 | 见[第三方适配器](#third-party-adapters) | -你可以通过这些内置路径集成其他 LLM provider: +你可以通过这些内置路径集成其他 LLM providers: -1. 在你希望全局使用 `AsyncOpenAI` 实例作为 LLM 客户端时,[`set_default_openai_client`][agents.set_default_openai_client] 很有用。这适用于 LLM provider 提供 OpenAI 兼容 API 端点,且你可设置 `base_url` 和 `api_key` 的场景。可配置示例见 [examples/model_providers/custom_example_global.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_global.py)。 -2. [`ModelProvider`][agents.models.interface.ModelProvider] 位于 `Runner.run` 层级。这让你可以指定“本次 run 的所有智能体都使用一个自定义模型 provider”。可配置示例见 [examples/model_providers/custom_example_provider.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_provider.py)。 -3. [`Agent.model`][agents.agent.Agent.model] 让你在特定 Agent 实例上指定模型。这使你可以为不同智能体混用不同 provider。可配置示例见 [examples/model_providers/custom_example_agent.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_agent.py)。 +1. [`set_default_openai_client`][agents.set_default_openai_client] 适用于你希望全局使用 `AsyncOpenAI` 实例作为 LLM 客户端的场景。这适用于 LLM provider 提供 OpenAI 兼容 API 端点,并且你可以设置 `base_url` 与 `api_key` 的情况。可配置示例见 [examples/model_providers/custom_example_global.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_global.py)。 +2. [`ModelProvider`][agents.models.interface.ModelProvider] 位于 `Runner.run` 级别。这使你可以声明“本次运行中的所有智能体都使用一个自定义模型 provider”。可配置示例见 [examples/model_providers/custom_example_provider.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_provider.py)。 +3. [`Agent.model`][agents.agent.Agent.model] 允许你在特定 Agent 实例上指定模型。这使你可以为不同智能体混合搭配不同 providers。可配置示例见 [examples/model_providers/custom_example_agent.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_agent.py)。 在你没有 `platform.openai.com` API key 的情况下,我们建议通过 `set_tracing_disabled()` 禁用追踪,或设置[其他追踪进程](../tracing.md)。 !!! note - 在这些示例中,我们使用 Chat Completions API/模型,因为许多 LLM provider 仍不支持 Responses API。如果你的 LLM provider 支持,我们建议使用 Responses。 + 在这些示例中,我们使用 Chat Completions API/模型,因为许多 LLM providers 仍不支持 Responses API。如果你的 LLM provider 支持,我们建议使用 Responses。 -## 在单个工作流中混用模型 +## 在单个工作流中混合模型 -在单个工作流中,你可能希望为每个智能体使用不同模型。例如,你可以为分流使用更小、更快的模型,同时为复杂任务使用更大、能力更强的模型。配置 [`Agent`][agents.Agent] 时,你可以通过以下方式选择特定模型: +在单个工作流内,你可能希望每个智能体使用不同模型。例如,你可以在分流阶段使用更小、更快的模型,在复杂任务中使用更大、更强的模型。配置 [`Agent`][agents.Agent] 时,你可以通过以下方式选择特定模型: 1. 传入模型名称。 -2. 传入任意模型名称 + 一个可将该名称映射为 Model 实例的 [`ModelProvider`][agents.models.interface.ModelProvider]。 -3. 直接提供 [`Model`][agents.models.interface.Model] 实现。 +2. 传入任意模型名 + 一个可将该名称映射为 Model 实例的 [`ModelProvider`][agents.models.interface.ModelProvider]。 +3. 直接提供一个 [`Model`][agents.models.interface.Model] 实现。 !!! note @@ -251,9 +251,9 @@ async def main(): ``` 1. 直接设置 OpenAI 模型名称。 -2. 提供 [`Model`][agents.models.interface.Model] 实现。 +2. 提供一个 [`Model`][agents.models.interface.Model] 实现。 -当你希望进一步配置某个智能体使用的模型时,可以传入 [`ModelSettings`][agents.models.interface.ModelSettings],它提供诸如 temperature 等可选模型配置参数。 +当你希望进一步配置智能体所用模型时,可传入 [`ModelSettings`][agents.models.interface.ModelSettings],它提供如 temperature 等可选模型配置参数。 ```python from agents import Agent, ModelSettings @@ -268,19 +268,19 @@ english_agent = Agent( ## 高级 OpenAI Responses 设置 -当你使用 OpenAI Responses 路径并需要更精细控制时,请从 `ModelSettings` 开始。 +当你走 OpenAI Responses 路径并需要更多控制时,请从 `ModelSettings` 开始。 ### 常见高级 `ModelSettings` 选项 -使用 OpenAI Responses API 时,若干请求字段在 `ModelSettings` 中已有直接对应字段,因此无需通过 `extra_args` 传递。 +当你使用 OpenAI Responses API 时,多个请求字段已经有对应的 `ModelSettings` 直接字段,因此不需要用 `extra_args` 传入。 - `parallel_tool_calls`:允许或禁止同一轮中的多个工具调用。 -- `truncation`:设置为 `"auto"`,让 Responses API 在上下文将溢出时丢弃最旧的会话项,而不是直接失败。 -- `store`:控制是否将生成的响应存储在服务端以便后续检索。这会影响依赖 response ID 的后续工作流,以及在 `store=False` 时可能需要回退到本地输入的会话压缩流程。 -- `prompt_cache_retention`:更长时间保留已缓存的提示词前缀,例如 `"24h"`。 -- `response_include`:请求更丰富的响应载荷,例如 `web_search_call.action.sources`、`file_search_call.results` 或 `reasoning.encrypted_content`。 -- `top_logprobs`:请求输出文本的 top-token logprobs。SDK 还会自动添加 `message.output_text.logprobs`。 -- `retry`:为模型调用启用由 runner 管理的重试设置。参见[由 Runner 管理的重试](#runner-managed-retries)。 +- `truncation`:设为 `"auto"` 可让 Responses API 在上下文将溢出时丢弃最旧会话项,而不是直接失败。 +- `store`:控制生成的响应是否存储在服务端供后续检索。这会影响依赖响应 ID 的后续工作流,以及在 `store=False` 时可能需要回退到本地输入的会话压缩流程。 +- `prompt_cache_retention`:延长提示词缓存前缀保留时间,例如设为 `"24h"`。 +- `response_include`:请求更丰富的响应负载,如 `web_search_call.action.sources`、`file_search_call.results` 或 `reasoning.encrypted_content`。 +- `top_logprobs`:请求输出文本的 top-token logprobs。SDK 也会自动加入 `message.output_text.logprobs`。 +- `retry`:为模型调用启用由 runner 管理的重试设置。见[Runner 管理的重试](#runner-managed-retries)。 ```python from agents import Agent, ModelSettings @@ -299,13 +299,13 @@ research_agent = Agent( ) ``` -当你设置 `store=False` 时,Responses API 不会保留该响应供后续服务端检索。这对无状态或零数据保留风格流程很有用,但也意味着原本可复用 response ID 的功能需要改为依赖本地管理状态。例如,当上一条响应未存储时,[`OpenAIResponsesCompactionSession`][agents.memory.openai_responses_compaction_session.OpenAIResponsesCompactionSession] 会将其默认 `"auto"` 压缩路径切换为基于输入的压缩。参见[会话指南](../sessions/index.md#openai-responses-compaction-sessions)。 +当你设置 `store=False` 时,Responses API 不会保留该响应以供后续服务端检索。这适用于无状态或零数据保留风格流程,但也意味着原本可复用响应 ID 的功能需要依赖本地管理状态。例如,当上一条响应未存储时,[`OpenAIResponsesCompactionSession`][agents.memory.openai_responses_compaction_session.OpenAIResponsesCompactionSession] 会将其默认 `"auto"` 压缩路径切换为基于输入的压缩。参见[Sessions 指南](../sessions/index.md#openai-responses-compaction-sessions)。 ### 传递 `extra_args` -当你需要 SDK 顶层尚未直接暴露的 provider 特定字段或较新的请求字段时,请使用 `extra_args`。 +当你需要 SDK 尚未直接在顶层暴露的 provider 特定字段或较新请求字段时,使用 `extra_args`。 -另外,当你使用 OpenAI 的 Responses API 时,[还有一些其他可选参数](https://platform.openai.com/docs/api-reference/responses/create)(如 `user`、`service_tier` 等)。如果它们在顶层不可用,也可用 `extra_args` 传递。 +另外,当你使用 OpenAI 的 Responses API 时,[还有一些其他可选参数](https://platform.openai.com/docs/api-reference/responses/create)(例如 `user`、`service_tier` 等)。如果它们未在顶层提供,你也可以通过 `extra_args` 传递。 ```python from agents import Agent, ModelSettings @@ -321,9 +321,9 @@ english_agent = Agent( ) ``` -## 由 Runner 管理的重试 +## Runner 管理的重试 -重试是仅运行时生效并需显式启用的功能。除非你设置 `ModelSettings(retry=...)` 且重试策略选择重试,否则 SDK 不会重试一般模型请求。 +重试是仅运行时生效且需显式启用的。除非你设置 `ModelSettings(retry=...)` 且重试策略选择重试,否则 SDK 不会重试普通模型请求。 ```python from agents import Agent, ModelRetrySettings, ModelSettings, retry_policies @@ -357,79 +357,79 @@ agent = Agent( | 字段 | 类型 | 说明 | | --- | --- | --- | -| `max_retries` | `int | None` | 初始请求之后允许的重试次数。 | -| `backoff` | `ModelRetryBackoffSettings | dict | None` | 当策略重试但未返回显式延迟时的默认延迟策略。 | -| `policy` | `RetryPolicy | None` | 决定是否重试的回调。该字段仅在运行时使用,不会被序列化。 | +| `max_retries` | `int | None` | 初次请求之后允许的重试次数。 | +| `backoff` | `ModelRetryBackoffSettings | dict | None` | 当策略选择重试但未返回显式延迟时使用的默认延迟策略。 | +| `policy` | `RetryPolicy | None` | 决定是否重试的回调。此字段仅在运行时生效,不会被序列化。 | -重试策略会接收一个 [`RetryPolicyContext`][agents.retry.RetryPolicyContext],其中包含: +重试策略会收到一个 [`RetryPolicyContext`][agents.retry.RetryPolicyContext],其中包含: -- `attempt` 和 `max_retries`,便于你基于尝试次数决策。 -- `stream`,用于在流式与非流式行为之间分支。 -- `error`,用于原始检查。 -- `normalized` 事实,例如 `status_code`、`retry_after`、`error_code`、`is_network_error`、`is_timeout` 和 `is_abort`。 -- 当底层模型适配器可提供重试指引时的 `provider_advice`。 +- `attempt` 和 `max_retries`,便于做与尝试次数相关的决策。 +- `stream`,便于区分流式与非流式行为。 +- `error`,用于原始错误检查。 +- `normalized` 事实,如 `status_code`、`retry_after`、`error_code`、`is_network_error`、`is_timeout` 和 `is_abort`。 +- 当底层模型适配器可提供重试建议时的 `provider_advice`。 策略可返回: -- `True` / `False`,用于简单的重试决策。 +- `True` / `False`,用于简单重试决策。 - [`RetryDecision`][agents.retry.RetryDecision],用于覆盖延迟或附加诊断原因。 -SDK 在 `retry_policies` 中导出了一组现成辅助函数: +SDK 在 `retry_policies` 中导出了现成辅助函数: | 辅助函数 | 行为 | | --- | --- | | `retry_policies.never()` | 始终不重试。 | -| `retry_policies.provider_suggested()` | 若可用则遵循 provider 的重试建议。 | -| `retry_policies.network_error()` | 匹配瞬时传输错误与超时失败。 | -| `retry_policies.http_status([...])` | 匹配选定的 HTTP 状态码。 | -| `retry_policies.retry_after()` | 仅当存在 retry-after 提示时重试,并使用该延迟。 | -| `retry_policies.any(...)` | 任一嵌套策略选择重试即重试。 | +| `retry_policies.provider_suggested()` | 在可用时遵循 provider 的重试建议。 | +| `retry_policies.network_error()` | 匹配临时传输错误与超时失败。 | +| `retry_policies.http_status([...])` | 匹配指定的 HTTP 状态码。 | +| `retry_policies.retry_after()` | 仅在存在 retry-after 提示时重试,并使用该延迟。 | +| `retry_policies.any(...)` | 任一嵌套策略选择重试时即重试。 | | `retry_policies.all(...)` | 仅当所有嵌套策略都选择重试时才重试。 | -组合策略时,`provider_suggested()` 是最安全的首个构件,因为当 provider 能区分时,它可保留 provider 的否决与重放安全批准。 +组合策略时,`provider_suggested()` 是最安全的首个构建块,因为当 provider 可区分时,它能保留 provider 否决与重放安全批准。 ##### 安全边界 某些失败永远不会自动重试: - Abort 错误。 -- provider 建议标记重放不安全的请求。 -- 流式 run 中输出已开始且重放会不安全的情况。 +- provider 建议将重放标记为不安全的请求。 +- 在输出已开始且重放会不安全的流式运行中发生的错误。 -使用 `previous_response_id` 或 `conversation_id` 的有状态后续请求也会被更保守地处理。对于这些请求,仅使用 `network_error()` 或 `http_status([500])` 等非 provider 谓词本身并不足够。重试策略应包含来自 provider 的重放安全批准,通常通过 `retry_policies.provider_suggested()`。 +使用 `previous_response_id` 或 `conversation_id` 的有状态后续请求也会更保守处理。对这类请求,仅有 `network_error()` 或 `http_status([500])` 等非 provider 谓词还不够。重试策略应包含来自 provider 的重放安全批准,通常通过 `retry_policies.provider_suggested()`。 -##### Runner 与智能体合并行为 +##### Runner 与智能体的合并行为 -在 runner 级和智能体级 `ModelSettings` 之间,`retry` 会进行深度合并: +`retry` 会在 runner 级和智能体级 `ModelSettings` 之间进行深度合并: -- 智能体可仅覆盖 `retry.max_retries`,并继承 runner 的 `policy`。 -- 智能体可仅覆盖 `retry.backoff` 的一部分,并保留 runner 中同级的其他 backoff 字段。 -- `policy` 仅运行时有效,因此序列化后的 `ModelSettings` 会保留 `max_retries` 和 `backoff`,但省略回调本身。 +- 智能体可以只覆盖 `retry.max_retries`,并继续继承 runner 的 `policy`。 +- 智能体可以只覆盖 `retry.backoff` 的一部分,并保留 runner 中同级其他 backoff 字段。 +- `policy` 仅在运行时生效,因此序列化后的 `ModelSettings` 会保留 `max_retries` 与 `backoff`,但省略回调本身。 -更完整示例见 [`examples/basic/retry.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry.py) 和 [`examples/basic/retry_litellm.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry_litellm.py)。 +更完整示例见 [`examples/basic/retry.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry.py) 和[基于适配器的重试示例](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry_litellm.py)。 -## 非 OpenAI provider 故障排查 +## 非 OpenAI providers 故障排查 ### 追踪客户端错误 401 -如果你遇到与追踪相关的错误,是因为追踪数据会上传到 OpenAI 服务端,而你没有 OpenAI API key。你有三种解决方案: +如果你遇到与追踪相关的错误,这是因为 trace 会上传到 OpenAI 服务端,而你没有 OpenAI API key。你有三种解决方式: 1. 完全禁用追踪:[`set_tracing_disabled(True)`][agents.set_tracing_disabled]。 -2. 为追踪设置 OpenAI key:[`set_tracing_export_api_key(...)`][agents.set_tracing_export_api_key]。该 API key 仅用于上传追踪,且必须来自 [platform.openai.com](https://platform.openai.com/)。 -3. 使用非 OpenAI 的追踪进程。参见[追踪文档](../tracing.md#custom-tracing-processors)。 +2. 为追踪设置 OpenAI key:[`set_tracing_export_api_key(...)`][agents.set_tracing_export_api_key]。该 API key 仅用于上传 trace,且必须来自 [platform.openai.com](https://platform.openai.com/)。 +3. 使用非 OpenAI 的 trace 进程。见[追踪文档](../tracing.md#custom-tracing-processors)。 ### Responses API 支持 -SDK 默认使用 Responses API,但许多其他 LLM provider 仍不支持它。因此你可能会看到 404 或类似问题。可通过以下两种方式解决: +SDK 默认使用 Responses API,但许多其他 LLM providers 仍不支持它。因此你可能会看到 404 或类似问题。你有两种解决方式: -1. 调用 [`set_default_openai_api("chat_completions")`][agents.set_default_openai_api]。当你通过环境变量设置 `OPENAI_API_KEY` 和 `OPENAI_BASE_URL` 时,此方式可用。 +1. 调用 [`set_default_openai_api("chat_completions")`][agents.set_default_openai_api]。这适用于你通过环境变量设置 `OPENAI_API_KEY` 和 `OPENAI_BASE_URL` 的情况。 2. 使用 [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel]。示例在[这里](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/)。 ### structured outputs 支持 -某些模型 provider 不支持 [structured outputs](https://platform.openai.com/docs/guides/structured-outputs)。这有时会导致类似如下错误: +一些模型 provider 不支持 [structured outputs](https://platform.openai.com/docs/guides/structured-outputs)。这有时会导致如下错误: ``` @@ -437,24 +437,34 @@ BadRequestError: Error code: 400 - {'error': {'message': "'response_format.type' ``` -这是某些模型 provider 的不足——它们支持 JSON 输出,但不允许你指定用于输出的 `json_schema`。我们正在修复这一问题,但建议你依赖支持 JSON schema 输出的 provider,否则应用会经常因 JSON 格式错误而中断。 +这是一些模型 provider 的短板——它们支持 JSON 输出,但不允许你指定输出使用的 `json_schema`。我们正在修复此问题,但建议你依赖支持 JSON schema 输出的 provider,否则应用会经常因 JSON 格式错误而中断。 -## 跨 provider 混用模型 +## 跨 providers 混合模型 -你需要了解不同模型 provider 的功能差异,否则可能遇到错误。例如,OpenAI 支持 structured outputs、多模态输入、托管文件检索和网络检索,但许多其他 provider 不支持这些功能。请注意以下限制: +你需要了解不同模型 provider 的功能差异,否则可能会遇到错误。例如,OpenAI 支持 structured outputs、多模态输入以及托管文件检索和网络检索,但许多其他 providers 不支持这些功能。请注意以下限制: - 不要向不支持的 provider 发送其无法理解的 `tools` -- 在调用仅文本模型前,先过滤掉多模态输入 -- 注意不支持 structured JSON 输出的 provider 偶尔会生成无效 JSON +- 在调用纯文本模型前过滤掉多模态输入 +- 注意不支持结构化 JSON 输出的 providers 偶尔会产生无效 JSON。 -## LiteLLM +## 第三方适配器 -对于需要将非 OpenAI provider 引入 Agents SDK 工作流的场景,LiteLLM 支持以尽力而为的 beta 形式提供。 +仅当 SDK 的内置 provider 集成点不足时再使用第三方适配器。如果你在此 SDK 中仅使用 OpenAI 模型,优先使用内置 [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] 路径,而不是 Any-LLM 或 LiteLLM。第三方适配器适用于你需要将 OpenAI 模型与非 OpenAI providers 组合使用,或需要内置路径未提供的适配器管理 provider 覆盖或路由的场景。适配器在 SDK 与上游模型 provider 之间增加了一层兼容层,因此功能支持和请求语义会因 provider 而异。SDK 当前将 Any-LLM 和 LiteLLM 作为尽力支持的 beta 适配器集成。 -如果你在此 SDK 中使用 OpenAI 模型,我们建议使用内置 [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] 路径,而非 LiteLLM。 +### Any-LLM -如果你需要将 OpenAI 模型与非 OpenAI provider 组合使用,尤其是通过 Chat Completions 兼容 API,LiteLLM 可作为 beta 选项,但未必是每种设置下的最优选择。 +Any-LLM 支持以尽力支持的 beta 形式提供,适用于你需要 Any-LLM 管理的 provider 覆盖或路由的场景。 -如果你需要为非 OpenAI provider 使用 LiteLLM,请安装 `openai-agents[litellm]`,然后从 [`examples/model_providers/litellm_auto.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/litellm_auto.py) 或 [`examples/model_providers/litellm_provider.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/litellm_provider.py) 开始。你可以使用 `litellm/...` 模型名,或直接实例化 [`LitellmModel`][agents.extensions.models.litellm_model.LitellmModel]。 +根据上游 provider 路径,Any-LLM 可能使用 Responses API、兼容 Chat Completions 的 API,或 provider 特定兼容层。 -如果你希望 LiteLLM 响应填充 SDK 的用量指标,请传入 `ModelSettings(include_usage=True)`。 \ No newline at end of file +如果你需要 Any-LLM,请安装 `openai-agents[any-llm]`,然后从 [`examples/model_providers/any_llm_auto.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/any_llm_auto.py) 或 [`examples/model_providers/any_llm_provider.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/any_llm_provider.py) 开始。你可以在 [`MultiProvider`][agents.MultiProvider] 中使用 `any-llm/...` 模型名,直接实例化 `AnyLLMModel`,或在运行范围使用 `AnyLLMProvider`。若需显式固定模型接口,可在构造 `AnyLLMModel` 时传入 `api="responses"` 或 `api="chat_completions"`。 + +Any-LLM 仍是第三方适配器层,因此 provider 依赖与能力缺口由 Any-LLM 上游定义,而非 SDK 定义。当上游 provider 返回使用量指标时会自动透传;但流式 Chat Completions 后端可能需要先设置 `ModelSettings(include_usage=True)` 才会输出使用量分片。若你依赖 structured outputs、工具调用、使用量上报或 Responses 特定行为,请验证你计划部署的具体 provider 后端。 + +### LiteLLM + +LiteLLM 支持以尽力支持的 beta 形式提供,适用于你需要 LiteLLM 特定 provider 覆盖或路由的场景。 + +如果你需要 LiteLLM,请安装 `openai-agents[litellm]`,然后从 [`examples/model_providers/litellm_auto.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/litellm_auto.py) 或 [`examples/model_providers/litellm_provider.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/litellm_provider.py) 开始。你可以使用 `litellm/...` 模型名,或直接实例化 [`LitellmModel`][agents.extensions.models.litellm_model.LitellmModel]。 + +一些 LiteLLM 支持的 providers 默认不会填充 SDK 使用量指标。如果你需要使用量上报,请传入 `ModelSettings(include_usage=True)`,并在依赖 structured outputs、工具调用、使用量上报或适配器特定路由行为时,验证你计划部署的具体 provider 后端。 \ No newline at end of file diff --git a/docs/zh/models/litellm.md b/docs/zh/models/litellm.md index 3352d3966e..3c32c8df8b 100644 --- a/docs/zh/models/litellm.md +++ b/docs/zh/models/litellm.md @@ -5,9 +5,9 @@ search: # LiteLLM -本页面已移动到[模型中的 LiteLLM 部分](index.md#litellm)。 +本页面已移动到[模型中的第三方适配器部分](index.md#third-party-adapters)。 如果未自动重定向,请使用上方链接。 \ No newline at end of file diff --git a/docs/zh/tracing.md b/docs/zh/tracing.md index ced155a3e5..6580b0f350 100644 --- a/docs/zh/tracing.md +++ b/docs/zh/tracing.md @@ -8,49 +8,49 @@ Agents SDK 内置了追踪功能,可在智能体运行期间收集完整的事 !!!note - 默认启用追踪。你可以通过三种常见方式禁用它: + 默认启用追踪。你可以通过三种常见方式禁用: - 1. 你可以通过设置环境变量 `OPENAI_AGENTS_DISABLE_TRACING=1` 全局禁用追踪 - 2. 你可以在代码中通过 [`set_tracing_disabled(True)`][agents.set_tracing_disabled] 全局禁用追踪 - 3. 你可以通过将 [`agents.run.RunConfig.tracing_disabled`][] 设为 `True` 来禁用单次运行的追踪 + 1. 通过设置环境变量 `OPENAI_AGENTS_DISABLE_TRACING=1` 全局禁用追踪 + 2. 在代码中使用 [`set_tracing_disabled(True)`][agents.set_tracing_disabled] 全局禁用追踪 + 3. 通过将 [`agents.run.RunConfig.tracing_disabled`][] 设为 `True`,为单次运行禁用追踪 -***对于在 OpenAI API 下使用零数据保留(ZDR)策略的组织,追踪不可用。*** +***对于在 OpenAI API 上使用零数据保留(ZDR)策略运行的组织,追踪不可用。*** -## Trace 与 Span +## Traces 与 spans -- **Traces** 表示一个“工作流”的单次端到端操作。它们由 Spans 组成。Traces 具有以下属性: - - `workflow_name`:逻辑工作流或应用名称。例如“代码生成”或“客户服务”。 - - `trace_id`:Trace 的唯一 ID。如果你不传入会自动生成。格式必须为 `trace_<32_alphanumeric>`。 +- **Traces** 表示“工作流”的一次端到端操作。它由 Span 组成。Trace 具有以下属性: + - `workflow_name`:逻辑工作流或应用。例如“代码生成”或“客户服务”。 + - `trace_id`:Trace 的唯一 ID。如果你未传入则自动生成。格式必须为 `trace_<32_alphanumeric>`。 - `group_id`:可选分组 ID,用于关联同一会话中的多个 Trace。例如,你可以使用聊天线程 ID。 - `disabled`:若为 True,则不会记录该 Trace。 - `metadata`:Trace 的可选元数据。 - **Spans** 表示具有开始和结束时间的操作。Span 具有: - `started_at` 和 `ended_at` 时间戳。 - - `trace_id`,表示它们所属的 Trace - - `parent_id`,指向该 Span 的父 Span(如果有) - - `span_data`,即关于 Span 的信息。例如,`AgentSpanData` 包含智能体信息,`GenerationSpanData` 包含 LLM 生成信息,等等。 + - `trace_id`,表示其所属的 Trace + - `parent_id`,指向该 Span 的父 Span(若有) + - `span_data`,即 Span 的信息。例如,`AgentSpanData` 包含智能体信息,`GenerationSpanData` 包含 LLM 生成信息,等等。 ## 默认追踪 默认情况下,SDK 会追踪以下内容: -- 整个 `Runner.{run, run_sync, run_streamed}()` 会包裹在 `trace()` 中。 -- 每次智能体运行都会包裹在 `agent_span()` 中 -- LLM 生成会包裹在 `generation_span()` 中 -- 每次函数工具调用都会包裹在 `function_span()` 中 -- 安全防护措施会包裹在 `guardrail_span()` 中 -- 任务转移会包裹在 `handoff_span()` 中 -- 音频输入(语音转文本)会包裹在 `transcription_span()` 中 -- 音频输出(文本转语音)会包裹在 `speech_span()` 中 -- 相关音频 Span 可能会作为子级归入 `speech_group_span()` 下 +- 整个 `Runner.{run, run_sync, run_streamed}()` 被包裹在 `trace()` 中。 +- 每次智能体运行时,都会包裹在 `agent_span()` 中 +- LLM 生成包裹在 `generation_span()` 中 +- 每次函数工具调用都包裹在 `function_span()` 中 +- 安全防护措施包裹在 `guardrail_span()` 中 +- 任务转移包裹在 `handoff_span()` 中 +- 音频输入(语音转文本)包裹在 `transcription_span()` 中 +- 音频输出(文本转语音)包裹在 `speech_span()` 中 +- 相关音频 Span 可能作为 `speech_group_span()` 的子级 -默认情况下,Trace 名称为“Agent workflow”。如果你使用 `trace`,可以设置此名称;也可以通过 [`RunConfig`][agents.run.RunConfig] 配置名称和其他属性。 +默认情况下,Trace 名称为“Agent workflow”。如果你使用 `trace`,可以设置该名称;也可以通过[`RunConfig`][agents.run.RunConfig]配置名称和其他属性。 -此外,你还可以设置[自定义追踪进程](#custom-tracing-processors),将 Trace 推送到其他目标(作为替代或辅助目标)。 +此外,你还可以设置[自定义追踪进程](#custom-tracing-processors),将追踪发送到其他目标(作为替代目标或次要目标)。 -## 更高层级的 Trace +## 更高层级的追踪 -有时,你可能希望多次对 `run()` 的调用都属于同一个 Trace。你可以通过将整段代码包裹在 `trace()` 中来实现。 +有时,你可能希望将多次 `run()` 调用纳入同一个 Trace。你可以通过将整段代码包裹在 `trace()` 中来实现。 ```python from agents import Agent, Runner, trace @@ -65,60 +65,60 @@ async def main(): print(f"Rating: {second_result.final_output}") ``` -1. 因为两次对 `Runner.run` 的调用都包裹在 `with trace()` 中,所以这些单独运行会属于同一个总体 Trace,而不是创建两个 Trace。 +1. 因为这两次 `Runner.run` 调用被包裹在 `with trace()` 中,所以这些单独运行会成为整体 Trace 的一部分,而不是创建两个 Trace。 ## 创建 Trace -你可以使用 [`trace()`][agents.tracing.trace] 函数来创建 Trace。Trace 需要开始和结束。你有两种方式: +你可以使用 [`trace()`][agents.tracing.trace] 函数创建 Trace。Trace 需要被启动和结束。你有两种方式: -1. **推荐**:将 trace 用作上下文管理器,即 `with trace(...) as my_trace`。这会在正确的时间自动开始和结束 Trace。 +1. **推荐**:将 Trace 用作上下文管理器,即 `with trace(...) as my_trace`。这会在正确的时间自动启动和结束 Trace。 2. 你也可以手动调用 [`trace.start()`][agents.tracing.Trace.start] 和 [`trace.finish()`][agents.tracing.Trace.finish]。 -当前 Trace 通过 Python 的 [`contextvar`](https://docs.python.org/3/library/contextvars.html) 跟踪。这意味着它可自动适配并发。如果你手动开始/结束 Trace,需要向 `start()`/`finish()` 传递 `mark_as_current` 和 `reset_current` 来更新当前 Trace。 +当前 Trace 通过 Python 的 [`contextvar`](https://docs.python.org/3/library/contextvars.html) 跟踪。这意味着它可自动适配并发。如果你手动启动/结束 Trace,则需要向 `start()`/`finish()` 传递 `mark_as_current` 和 `reset_current` 来更新当前 Trace。 ## 创建 Span -你可以使用各种 [`*_span()`][agents.tracing.create] 方法来创建 Span。通常你不需要手动创建 Span。可使用 [`custom_span()`][agents.tracing.custom_span] 函数来跟踪自定义 Span 信息。 +你可以使用各种 [`*_span()`][agents.tracing.create] 方法创建 Span。通常你不需要手动创建 Span。可使用 [`custom_span()`][agents.tracing.custom_span] 函数来跟踪自定义 Span 信息。 -Span 会自动归属于当前 Trace,并嵌套在最近的当前 Span 下;这同样通过 Python 的 [`contextvar`](https://docs.python.org/3/library/contextvars.html) 跟踪。 +Span 会自动归属于当前 Trace,并嵌套在最近的当前 Span 之下,后者通过 Python 的 [`contextvar`](https://docs.python.org/3/library/contextvars.html) 进行跟踪。 ## 敏感数据 某些 Span 可能会捕获潜在的敏感数据。 -`generation_span()` 会存储 LLM 生成的输入/输出,`function_span()` 会存储函数调用的输入/输出。这些可能包含敏感数据,因此你可以通过 [`RunConfig.trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data] 禁用此类数据的捕获。 +`generation_span()` 会存储 LLM 生成的输入/输出,`function_span()` 会存储函数调用的输入/输出。这些可能包含敏感数据,因此你可以通过 [`RunConfig.trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data] 禁用这类数据采集。 -类似地,默认情况下,音频 Span 会包含输入和输出音频的 base64 编码 PCM 数据。你可以通过配置 [`VoicePipelineConfig.trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data] 来禁用此音频数据的捕获。 +类似地,音频 Span 默认包含输入和输出音频的 base64 编码 PCM 数据。你可以通过配置 [`VoicePipelineConfig.trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data] 禁用该音频数据采集。 -默认情况下,`trace_include_sensitive_data` 为 `True`。你也可以在不改代码的情况下,通过在运行应用前导出 `OPENAI_AGENTS_TRACE_INCLUDE_SENSITIVE_DATA` 环境变量为 `true/1` 或 `false/0` 来设置默认值。 +默认情况下,`trace_include_sensitive_data` 为 `True`。你可以在运行应用前,通过导出 `OPENAI_AGENTS_TRACE_INCLUDE_SENSITIVE_DATA` 环境变量为 `true/1` 或 `false/0`,在不改代码的情况下设置默认值。 ## 自定义追踪进程 -追踪的高层架构为: +追踪的高层架构如下: - 初始化时,我们会创建一个全局 [`TraceProvider`][agents.tracing.setup.TraceProvider],负责创建 Trace。 -- 我们使用 [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor] 配置 `TraceProvider`,它会将 Trace/Span 批量发送到 [`BackendSpanExporter`][agents.tracing.processors.BackendSpanExporter],后者会将 Span 和 Trace 批量导出到 OpenAI 后端。 +- 我们为 `TraceProvider` 配置一个 [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor],它会将 Trace/Span 批量发送给 [`BackendSpanExporter`][agents.tracing.processors.BackendSpanExporter],后者再将 Span 和 Trace 批量导出到 OpenAI 后端。 -若要自定义此默认设置,将 Trace 发送到替代或附加后端,或修改导出器行为,你有两种选择: +要自定义此默认设置,将追踪发送到替代或附加后端,或修改导出器行为,你有两个选项: -1. [`add_trace_processor()`][agents.tracing.add_trace_processor] 允许你添加一个**额外的**追踪进程,在 Trace 和 Span 就绪时接收它们。这使你可以在发送到 OpenAI 后端之外执行自己的处理。 -2. [`set_trace_processors()`][agents.tracing.set_trace_processors] 允许你用自己的追踪进程**替换**默认进程。这意味着除非你包含一个会执行该发送的 `TracingProcessor`,否则 Trace 不会发送到 OpenAI 后端。 +1. [`add_trace_processor()`][agents.tracing.add_trace_processor] 允许你添加一个**额外**的追踪进程,它会在 Trace 和 Span 就绪时接收它们。这样你就可以在发送到 OpenAI 后端之外执行自己的处理。 +2. [`set_trace_processors()`][agents.tracing.set_trace_processors] 允许你用自己的追踪进程**替换**默认进程。这意味着除非你包含可执行该操作的 `TracingProcessor`,否则 Trace 不会发送到 OpenAI 后端。 -## 使用非 OpenAI 模型进行追踪 +## 对非 OpenAI 模型进行追踪 -你可以在非 OpenAI 模型中使用 OpenAI API key,在无需禁用追踪的情况下,于 OpenAI Traces 仪表板启用免费追踪。 +你可以将 OpenAI API key 与非 OpenAI 模型一起使用,从而在无需禁用追踪的情况下,在 OpenAI Traces 仪表板启用免费追踪。有关适配器选择和设置注意事项,请参阅 Models 指南中的[第三方适配器](models/index.md#third-party-adapters)部分。 ```python import os from agents import set_tracing_export_api_key, Agent, Runner -from agents.extensions.models.litellm_model import LitellmModel +from agents.extensions.models.any_llm_model import AnyLLMModel tracing_api_key = os.environ["OPENAI_API_KEY"] set_tracing_export_api_key(tracing_api_key) -model = LitellmModel( - model="your-model-name", +model = AnyLLMModel( + model="your-provider/your-model-name", api_key="your-api-key", ) @@ -141,12 +141,12 @@ await Runner.run( ``` ## 附加说明 -- 在 Openai Traces 仪表板查看免费 Trace。 +- 在 Openai Traces 仪表板查看免费追踪。 ## 生态系统集成 -以下社区和供应商集成支持 OpenAI Agents SDK 追踪接口。 +以下社区和供应商集成支持 OpenAI Agents SDK 的追踪接口。 ### 外部追踪进程列表 diff --git a/docs/zh/usage.md b/docs/zh/usage.md index b02d7097e0..743098f221 100644 --- a/docs/zh/usage.md +++ b/docs/zh/usage.md @@ -4,7 +4,7 @@ search: --- # 用法 -Agents SDK 会自动追踪每次运行的 token 使用量。你可以从运行上下文中访问它,并用它来监控成本、实施限制或记录分析数据。 +Agents SDK 会自动追踪每次运行的 token 使用情况。你可以从运行上下文中访问这些数据,并用它来监控成本、执行限制或记录分析数据。 ## 追踪内容 @@ -12,14 +12,14 @@ Agents SDK 会自动追踪每次运行的 token 使用量。你可以从运行 - **input_tokens**: 发送的输入 token 总数 - **output_tokens**: 接收的输出 token 总数 - **total_tokens**: 输入 + 输出 -- **request_usage_entries**: 按请求划分的使用量明细列表 +- **request_usage_entries**: 按请求划分的使用明细列表 - **details**: - `input_tokens_details.cached_tokens` - `output_tokens_details.reasoning_tokens` -## 从运行中访问使用量 +## 从一次运行中访问使用情况 -在 `Runner.run(...)` 之后,可通过 `result.context_wrapper.usage` 访问使用量。 +在 `Runner.run(...)` 之后,可通过 `result.context_wrapper.usage` 访问使用情况。 ```python result = await Runner.run(agent, "What's the weather in Tokyo?") @@ -31,29 +31,20 @@ print("Output tokens:", usage.output_tokens) print("Total tokens:", usage.total_tokens) ``` -使用量会聚合该次运行期间的所有模型调用(包括工具调用和任务转移)。 +使用量会汇总该次运行期间所有模型调用(包括工具调用和任务转移)。 -### 为 LiteLLM 模型启用使用量追踪 +### 在第三方适配器中启用使用情况追踪 -LiteLLM 提供方默认不会上报使用量指标。使用 [`LitellmModel`][agents.extensions.models.litellm_model.LitellmModel] 时,请向你的智能体传入 `ModelSettings(include_usage=True)`,以便 LiteLLM 响应填充 `result.context_wrapper.usage`。有关设置指南和示例,请参阅模型指南中的 [LiteLLM 说明](models/index.md#litellm)。 +不同第三方适配器和提供方后端的使用情况上报方式有所不同。如果你依赖由适配器支持的模型并且需要准确的 `result.context_wrapper.usage` 值: -```python -from agents import Agent, ModelSettings, Runner -from agents.extensions.models.litellm_model import LitellmModel - -agent = Agent( - name="Assistant", - model=LitellmModel(model="your/model", api_key="..."), - model_settings=ModelSettings(include_usage=True), -) +- 使用 `AnyLLMModel` 时,如果上游提供方返回了使用数据,则会自动透传。对于流式 Chat Completions 后端,在发出 usage 分块前,你可能需要设置 `ModelSettings(include_usage=True)`。 +- 使用 `LitellmModel` 时,某些提供方后端默认不会上报使用数据,因此通常需要 `ModelSettings(include_usage=True)`。 -result = await Runner.run(agent, "What's the weather in Tokyo?") -print(result.context_wrapper.usage.total_tokens) -``` +请查看 Models 指南中[第三方适配器](models/index.md#third-party-adapters)章节的适配器说明,并验证你计划部署的具体提供方后端。 -## 按请求追踪使用量 +## 按请求追踪使用情况 -SDK 会自动在 `request_usage_entries` 中追踪每个 API 请求的使用量,这有助于进行精细化成本计算和上下文窗口消耗监控。 +SDK 会自动在 `request_usage_entries` 中追踪每个 API 请求的使用情况,这对精细化成本计算和上下文窗口消耗监控很有帮助。 ```python result = await Runner.run(agent, "What's the weather in Tokyo?") @@ -62,9 +53,9 @@ for i, request in enumerate(result.context_wrapper.usage.request_usage_entries): print(f"Request {i + 1}: {request.input_tokens} in, {request.output_tokens} out") ``` -## 在会话中访问使用量 +## 在会话中访问使用情况 -使用 `Session`(例如 `SQLiteSession`)时,每次调用 `Runner.run(...)` 都会返回该次运行对应的使用量。会话会为上下文维护对话历史,但每次运行的使用量彼此独立。 +当你使用 `Session`(例如 `SQLiteSession`)时,每次调用 `Runner.run(...)` 都会返回该次运行对应的使用数据。会话会维护对话历史以提供上下文,但每次运行的使用数据彼此独立。 ```python session = SQLiteSession("my_conversation") @@ -76,11 +67,11 @@ second = await Runner.run(agent, "Can you elaborate?", session=session) print(second.context_wrapper.usage.total_tokens) # Usage for second run ``` -请注意,虽然会话会在多次运行之间保留对话上下文,但每次 `Runner.run()` 调用返回的使用量指标仅代表该次执行。在会话中,之前的消息可能会在每次运行时作为输入重新提供,这会影响后续轮次中的输入 token 计数。 +请注意,尽管会话会在多次运行之间保留对话上下文,但每次 `Runner.run()` 调用返回的使用指标只代表该次执行。在会话中,先前消息可能会在每次运行时作为输入再次传入,这会影响后续轮次的输入 token 计数。 -## 在 hooks 中使用使用量 +## 在 hooks 中使用使用情况 -如果你正在使用 `RunHooks`,传递给每个 hook 的 `context` 对象都包含 `usage`。这使你可以在关键生命周期节点记录使用量。 +如果你使用 `RunHooks`,传递给每个 hook 的 `context` 对象都包含 `usage`。这使你可以在关键生命周期节点记录使用情况。 ```python class MyHooks(RunHooks): @@ -91,9 +82,9 @@ class MyHooks(RunHooks): ## API 参考 -有关详细 API 文档,请参阅: +详细 API 文档请参见: -- [`Usage`][agents.usage.Usage] - 使用量追踪数据结构 -- [`RequestUsage`][agents.usage.RequestUsage] - 按请求划分的使用量详情 -- [`RunContextWrapper`][agents.run.RunContextWrapper] - 从运行上下文访问使用量 -- [`RunHooks`][agents.run.RunHooks] - 接入使用量追踪生命周期 hooks \ No newline at end of file +- [`Usage`][agents.usage.Usage] - 使用情况追踪数据结构 +- [`RequestUsage`][agents.usage.RequestUsage] - 按请求划分的使用详情 +- [`RunContextWrapper`][agents.run.RunContextWrapper] - 从运行上下文访问使用情况 +- [`RunHooks`][agents.run.RunHooks] - 挂接到使用情况追踪生命周期 \ No newline at end of file From abd5cef032458e1c07f64c75598fb059f879bc89 Mon Sep 17 00:00:00 2001 From: Lucas Wang Date: Wed, 25 Mar 2026 21:09:58 +0800 Subject: [PATCH 07/40] fix: pin liteLLM upper bound to 1.82.6 to mitigate supply chain attack (#2772) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fe08cae8d7..6d3347264c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ Repository = "https://github.com/openai/openai-agents-python" [project.optional-dependencies] voice = ["numpy>=2.2.0, <3; python_version>='3.10'", "websockets>=15.0, <16"] viz = ["graphviz>=0.17"] -litellm = ["litellm>=1.81.0, <2"] +litellm = ["litellm>=1.81.0, <=1.82.6"] any-llm = ["any-llm-sdk>=1.11.0, <2; python_version >= '3.11'"] realtime = ["websockets>=15.0, <16"] sqlalchemy = ["SQLAlchemy>=2.0", "asyncpg>=0.29.0"] From 8fdb45da695aa2892ed2585514cdb6fe267f31bc Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Wed, 25 Mar 2026 22:10:15 +0900 Subject: [PATCH 08/40] fix: update default reasoning effort for newer models (#2773) --- src/agents/models/default_models.py | 67 ++++++++++--- tests/models/test_default_models.py | 140 ++++++++++++++++++---------- 2 files changed, 147 insertions(+), 60 deletions(-) diff --git a/src/agents/models/default_models.py b/src/agents/models/default_models.py index 3a8a122e8b..d869945e8d 100644 --- a/src/agents/models/default_models.py +++ b/src/agents/models/default_models.py @@ -1,6 +1,7 @@ import copy import os -from typing import Optional +import re +from typing import Literal, Optional from openai.types.shared.reasoning import Reasoning @@ -8,9 +9,11 @@ OPENAI_DEFAULT_MODEL_ENV_VARIABLE_NAME = "OPENAI_DEFAULT_MODEL" -# discourage directly accessing this constant +GPT5DefaultReasoningEffort = Literal["none", "low", "medium"] + +# discourage directly accessing these constants # use the get_default_model and get_default_model_settings() functions instead -_GPT_5_DEFAULT_MODEL_SETTINGS: ModelSettings = ModelSettings( +_GPT_5_LOW_DEFAULT_MODEL_SETTINGS: ModelSettings = ModelSettings( # We chose "low" instead of "minimal" because some of the built-in tools # (e.g., file search, image generation, etc.) do not support "minimal" # If you want to use "minimal" reasoning effort, you can pass your own model settings @@ -21,20 +24,59 @@ reasoning=Reasoning(effort="none"), verbosity="low", ) +_GPT_5_MEDIUM_DEFAULT_MODEL_SETTINGS: ModelSettings = ModelSettings( + reasoning=Reasoning(effort="medium"), + verbosity="low", +) +_GPT_5_TEXT_ONLY_DEFAULT_MODEL_SETTINGS: ModelSettings = ModelSettings( + verbosity="low", +) -_GPT_5_NONE_EFFORT_MODELS = {"gpt-5.1", "gpt-5.2"} +_GPT_5_CHAT_MODEL_PATTERNS: tuple[re.Pattern[str], ...] = ( + re.compile(r"^gpt-5-chat-latest$"), + re.compile(r"^gpt-5\.1-chat-latest$"), + re.compile(r"^gpt-5\.2-chat-latest$"), + re.compile(r"^gpt-5\.3-chat-latest$"), +) + +_GPT_5_DEFAULT_MODEL_SETTINGS_BY_REASONING_EFFORT: dict[ + GPT5DefaultReasoningEffort, ModelSettings +] = { + "none": _GPT_5_NONE_DEFAULT_MODEL_SETTINGS, + "low": _GPT_5_LOW_DEFAULT_MODEL_SETTINGS, + "medium": _GPT_5_MEDIUM_DEFAULT_MODEL_SETTINGS, +} + +_GPT_5_DEFAULT_REASONING_EFFORT_PATTERNS: tuple[ + tuple[re.Pattern[str], GPT5DefaultReasoningEffort], + ..., +] = ( + (re.compile(r"^gpt-5(?:-\d{4}-\d{2}-\d{2})?$"), "low"), + (re.compile(r"^gpt-5\.1(?:-\d{4}-\d{2}-\d{2})?$"), "none"), + (re.compile(r"^gpt-5\.2(?:-\d{4}-\d{2}-\d{2})?$"), "none"), + (re.compile(r"^gpt-5\.2-pro(?:-\d{4}-\d{2}-\d{2})?$"), "medium"), + (re.compile(r"^gpt-5\.2-codex$"), "low"), + (re.compile(r"^gpt-5\.3-codex$"), "none"), + (re.compile(r"^gpt-5\.4(?:-\d{4}-\d{2}-\d{2})?$"), "none"), + (re.compile(r"^gpt-5\.4-pro(?:-\d{4}-\d{2}-\d{2})?$"), "medium"), + (re.compile(r"^gpt-5\.4-mini(?:-\d{4}-\d{2}-\d{2})?$"), "none"), + (re.compile(r"^gpt-5\.4-nano(?:-\d{4}-\d{2}-\d{2})?$"), "none"), +) -def _is_gpt_5_none_effort_model(model_name: str) -> bool: - return model_name in _GPT_5_NONE_EFFORT_MODELS +def _get_default_reasoning_effort(model_name: str) -> GPT5DefaultReasoningEffort | None: + for pattern, effort in _GPT_5_DEFAULT_REASONING_EFFORT_PATTERNS: + if pattern.fullmatch(model_name): + return effort + return None def gpt_5_reasoning_settings_required(model_name: str) -> bool: """ Returns True if the model name is a GPT-5 model and reasoning settings are required. """ - if model_name.startswith("gpt-5-chat"): - # gpt-5-chat-latest does not require reasoning settings + if any(pattern.fullmatch(model_name) for pattern in _GPT_5_CHAT_MODEL_PATTERNS): + # Chat-latest aliases do not accept reasoning.effort. return False # matches any of gpt-5 models return model_name.startswith("gpt-5") @@ -64,7 +106,10 @@ def get_default_model_settings(model: Optional[str] = None) -> ModelSettings: """ _model = model if model is not None else get_default_model() if gpt_5_reasoning_settings_required(_model): - if _is_gpt_5_none_effort_model(_model): - return copy.deepcopy(_GPT_5_NONE_DEFAULT_MODEL_SETTINGS) - return copy.deepcopy(_GPT_5_DEFAULT_MODEL_SETTINGS) + effort = _get_default_reasoning_effort(_model) + if effort is not None: + return copy.deepcopy(_GPT_5_DEFAULT_MODEL_SETTINGS_BY_REASONING_EFFORT[effort]) + # Keep the GPT-5 verbosity default, but omit reasoning.effort for + # variants whose supported values are not confirmed yet. + return copy.deepcopy(_GPT_5_TEXT_ONLY_DEFAULT_MODEL_SETTINGS) return ModelSettings() diff --git a/tests/models/test_default_models.py b/tests/models/test_default_models.py index d291aac1e3..d0904cd4e2 100644 --- a/tests/models/test_default_models.py +++ b/tests/models/test_default_models.py @@ -1,6 +1,9 @@ import os +from typing import Literal from unittest.mock import patch +from openai.types.shared.reasoning import Reasoning + from agents import Agent from agents.model_settings import ModelSettings from agents.models import ( @@ -11,6 +14,14 @@ ) +def _gpt_5_default_settings( + reasoning_effort: Literal["none", "low", "medium"] | None, +) -> ModelSettings: + if reasoning_effort is None: + return ModelSettings(verbosity="low") + return ModelSettings(reasoning=Reasoning(effort=reasoning_effort), verbosity="low") + + def test_default_model_is_gpt_4_1(): assert get_default_model() == "gpt-4.1" assert is_gpt_5_default() is False @@ -18,68 +29,99 @@ def test_default_model_is_gpt_4_1(): assert get_default_model_settings().reasoning is None -@patch.dict(os.environ, {"OPENAI_DEFAULT_MODEL": "gpt-5"}) -def test_default_model_env_gpt_5(): - assert get_default_model() == "gpt-5" +@patch.dict(os.environ, {"OPENAI_DEFAULT_MODEL": "gpt-5.4"}) +def test_is_gpt_5_default_with_real_model_name(): + assert get_default_model() == "gpt-5.4" assert is_gpt_5_default() is True - assert gpt_5_reasoning_settings_required(get_default_model()) is True - assert get_default_model_settings().reasoning.effort == "low" # type: ignore[union-attr] -@patch.dict(os.environ, {"OPENAI_DEFAULT_MODEL": "gpt-5.1"}) -def test_default_model_env_gpt_5_1(): - assert get_default_model() == "gpt-5.1" - assert is_gpt_5_default() is True - assert gpt_5_reasoning_settings_required(get_default_model()) is True - assert get_default_model_settings().reasoning.effort == "none" # type: ignore[union-attr] +@patch.dict(os.environ, {"OPENAI_DEFAULT_MODEL": "gpt-4.1"}) +def test_is_gpt_5_default_returns_false_for_non_gpt_5_default_model(): + assert get_default_model() == "gpt-4.1" + assert is_gpt_5_default() is False -@patch.dict(os.environ, {"OPENAI_DEFAULT_MODEL": "gpt-5.2"}) -def test_default_model_env_gpt_5_2(): - assert get_default_model() == "gpt-5.2" - assert is_gpt_5_default() is True - assert gpt_5_reasoning_settings_required(get_default_model()) is True - assert get_default_model_settings().reasoning.effort == "none" # type: ignore[union-attr] +def test_gpt_5_reasoning_settings_required_detects_gpt_5_models_while_ignoring_chat_latest(): + assert gpt_5_reasoning_settings_required("gpt-5") is True + assert gpt_5_reasoning_settings_required("gpt-5.1") is True + assert gpt_5_reasoning_settings_required("gpt-5.2") is True + assert gpt_5_reasoning_settings_required("gpt-5.2-codex") is True + assert gpt_5_reasoning_settings_required("gpt-5.2-pro") is True + assert gpt_5_reasoning_settings_required("gpt-5.4-pro") is True + assert gpt_5_reasoning_settings_required("gpt-5-mini") is True + assert gpt_5_reasoning_settings_required("gpt-5-nano") is True + assert gpt_5_reasoning_settings_required("gpt-5-chat-latest") is False + assert gpt_5_reasoning_settings_required("gpt-5.1-chat-latest") is False + assert gpt_5_reasoning_settings_required("gpt-5.2-chat-latest") is False + assert gpt_5_reasoning_settings_required("gpt-5.3-chat-latest") is False -@patch.dict(os.environ, {"OPENAI_DEFAULT_MODEL": "gpt-5.2-codex"}) -def test_default_model_env_gpt_5_2_codex(): - assert get_default_model() == "gpt-5.2-codex" - assert is_gpt_5_default() is True - assert gpt_5_reasoning_settings_required(get_default_model()) is True - assert get_default_model_settings().reasoning.effort == "low" # type: ignore[union-attr] +def test_gpt_5_reasoning_settings_required_returns_false_for_non_gpt_5_models(): + assert gpt_5_reasoning_settings_required("gpt-4.1") is False -@patch.dict(os.environ, {"OPENAI_DEFAULT_MODEL": "gpt-5-mini"}) -def test_default_model_env_gpt_5_mini(): - assert get_default_model() == "gpt-5-mini" - assert is_gpt_5_default() is True - assert gpt_5_reasoning_settings_required(get_default_model()) is True - assert get_default_model_settings().reasoning.effort == "low" # type: ignore[union-attr] +def test_get_default_model_settings_returns_none_reasoning_defaults_for_gpt_5_1_models(): + assert get_default_model_settings("gpt-5.1") == _gpt_5_default_settings("none") + assert get_default_model_settings("gpt-5.1-2025-11-13") == _gpt_5_default_settings("none") -@patch.dict(os.environ, {"OPENAI_DEFAULT_MODEL": "gpt-5-nano"}) -def test_default_model_env_gpt_5_nano(): - assert get_default_model() == "gpt-5-nano" - assert is_gpt_5_default() is True - assert gpt_5_reasoning_settings_required(get_default_model()) is True - assert get_default_model_settings().reasoning.effort == "low" # type: ignore[union-attr] +def test_get_default_model_settings_returns_none_reasoning_defaults_for_gpt_5_2_models(): + assert get_default_model_settings("gpt-5.2") == _gpt_5_default_settings("none") + assert get_default_model_settings("gpt-5.2-2025-12-11") == _gpt_5_default_settings("none") -@patch.dict(os.environ, {"OPENAI_DEFAULT_MODEL": "gpt-5-chat-latest"}) -def test_default_model_env_gpt_5_chat_latest(): - assert get_default_model() == "gpt-5-chat-latest" - assert is_gpt_5_default() is False - assert gpt_5_reasoning_settings_required(get_default_model()) is False - assert get_default_model_settings().reasoning is None +def test_get_default_model_settings_returns_none_reasoning_defaults_for_gpt_5_3_codex_models(): + assert get_default_model_settings("gpt-5.3-codex") == _gpt_5_default_settings("none") -@patch.dict(os.environ, {"OPENAI_DEFAULT_MODEL": "gpt-4o"}) -def test_default_model_env_gpt_4o(): - assert get_default_model() == "gpt-4o" - assert is_gpt_5_default() is False - assert gpt_5_reasoning_settings_required(get_default_model()) is False - assert get_default_model_settings().reasoning is None +def test_get_default_model_settings_returns_none_reasoning_defaults_for_gpt_5_4_models(): + assert get_default_model_settings("gpt-5.4") == _gpt_5_default_settings("none") + + +def test_get_default_model_settings_returns_none_reasoning_defaults_for_gpt_5_4_snapshot_families(): + assert get_default_model_settings("gpt-5.4-2026-03-05") == _gpt_5_default_settings("none") + assert get_default_model_settings("gpt-5.4-mini-2026-03-17") == _gpt_5_default_settings("none") + assert get_default_model_settings("gpt-5.4-nano-2026-03-17") == _gpt_5_default_settings("none") + + +def test_get_default_model_settings_returns_none_reasoning_defaults_for_gpt_5_4_mini_and_nano(): + assert get_default_model_settings("gpt-5.4-mini") == _gpt_5_default_settings("none") + assert get_default_model_settings("gpt-5.4-nano") == _gpt_5_default_settings("none") + + +def test_get_default_model_settings_returns_low_reasoning_defaults_for_base_gpt_5(): + assert get_default_model_settings("gpt-5") == _gpt_5_default_settings("low") + assert get_default_model_settings("gpt-5-2025-08-07") == _gpt_5_default_settings("low") + + +def test_get_default_model_settings_returns_low_reasoning_defaults_for_gpt_5_2_codex(): + assert get_default_model_settings("gpt-5.2-codex") == _gpt_5_default_settings("low") + + +def test_get_default_model_settings_returns_medium_reasoning_defaults_for_gpt_5_pro_models(): + assert get_default_model_settings("gpt-5.2-pro") == _gpt_5_default_settings("medium") + assert get_default_model_settings("gpt-5.2-pro-2025-12-11") == _gpt_5_default_settings("medium") + assert get_default_model_settings("gpt-5.4-pro") == _gpt_5_default_settings("medium") + assert get_default_model_settings("gpt-5.4-pro-2026-03-05") == _gpt_5_default_settings("medium") + + +def test_get_default_model_settings_omits_reasoning_for_unconfirmed_gpt_5_variants(): + assert get_default_model_settings("gpt-5-mini") == _gpt_5_default_settings(None) + assert get_default_model_settings("gpt-5-mini-2025-08-07") == _gpt_5_default_settings(None) + assert get_default_model_settings("gpt-5-nano") == _gpt_5_default_settings(None) + assert get_default_model_settings("gpt-5-nano-2025-08-07") == _gpt_5_default_settings(None) + assert get_default_model_settings("gpt-5.1-codex") == _gpt_5_default_settings(None) + + +def test_get_default_model_settings_returns_empty_settings_for_gpt_5_chat_latest_aliases(): + assert get_default_model_settings("gpt-5-chat-latest") == ModelSettings() + assert get_default_model_settings("gpt-5.1-chat-latest") == ModelSettings() + assert get_default_model_settings("gpt-5.2-chat-latest") == ModelSettings() + assert get_default_model_settings("gpt-5.3-chat-latest") == ModelSettings() + + +def test_get_default_model_settings_returns_empty_settings_for_non_gpt_5_models(): + assert get_default_model_settings("gpt-4.1") == ModelSettings() @patch.dict(os.environ, {"OPENAI_DEFAULT_MODEL": "gpt-5"}) @@ -94,6 +136,6 @@ def test_agent_uses_gpt_5_default_model_settings(): @patch.dict(os.environ, {"OPENAI_DEFAULT_MODEL": "gpt-5"}) def test_agent_resets_model_settings_for_non_gpt_5_models(): """Agent should reset default GPT-5 settings when using a non-GPT-5 model.""" - agent = Agent(name="test", model="gpt-4o") - assert agent.model == "gpt-4o" + agent = Agent(name="test", model="gpt-4.1") + assert agent.model == "gpt-4.1" assert agent.model_settings == ModelSettings() From 67fa4d89a5ff90afaa91c15f86507bd9fc772d13 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Thu, 26 Mar 2026 14:26:22 +0900 Subject: [PATCH 09/40] chore: make sync --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index f4e4b0e36f..fe9b1a28b4 100644 --- a/uv.lock +++ b/uv.lock @@ -1992,7 +1992,7 @@ requires-dist = [ { name = "graphviz", marker = "extra == 'viz'", specifier = ">=0.17" }, { name = "griffe", specifier = ">=1.5.6,<2" }, { name = "grpcio", marker = "extra == 'dapr'", specifier = ">=1.60.0" }, - { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.81.0,<2" }, + { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.81.0,<=1.82.6" }, { name = "mcp", marker = "python_full_version >= '3.10'", specifier = ">=1.19.0,<2" }, { name = "numpy", marker = "python_full_version >= '3.10' and extra == 'voice'", specifier = ">=2.2.0,<3" }, { name = "openai", specifier = ">=2.26.0,<3" }, From 9a96d9e787414510074dfbc84dab960d0d6d5c1d Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Thu, 26 Mar 2026 15:05:22 +0900 Subject: [PATCH 10/40] fix: #2778 keep LiteLLM reasoning_effort portable across providers (#2782) --- src/agents/extensions/models/litellm_model.py | 71 +++++++++++-------- tests/models/test_litellm_extra_body.py | 59 +++++++++------ 2 files changed, 77 insertions(+), 53 deletions(-) diff --git a/src/agents/extensions/models/litellm_model.py b/src/agents/extensions/models/litellm_model.py index 906130b87f..d9ce3963f9 100644 --- a/src/agents/extensions/models/litellm_model.py +++ b/src/agents/extensions/models/litellm_model.py @@ -159,6 +159,45 @@ def get_retry_advice(self, request: ModelRetryAdviceRequest) -> ModelRetryAdvice # Reuse the same normalization to expose retry-after and explicit retry/no-retry hints. return get_openai_retry_advice(request) + def _get_reasoning_effort(self, model_settings: ModelSettings) -> Any | None: + """ + Resolve the top-level LiteLLM reasoning_effort argument for the chat-completions path. + + LiteLLM's public acompletion() surface accepts a scalar reasoning_effort value. Keep the + ModelSettings.reasoning path aligned with that contract and leave extra_body / extra_args as + the explicit escape hatches for advanced provider-specific overrides. + """ + reasoning_effort: Any | None = None + + if model_settings.reasoning: + reasoning_effort = model_settings.reasoning.effort + if model_settings.reasoning.summary is not None: + logger.warning( + "LitellmModel does not forward Reasoning.summary on the LiteLLM " + "chat-completions path; ignoring summary and passing reasoning_effort only." + ) + + # Enable developers to pass non-OpenAI compatible reasoning_effort data like "none". + # Priority order: + # 1. model_settings.reasoning.effort + # 2. model_settings.extra_body["reasoning_effort"] + # 3. model_settings.extra_args["reasoning_effort"] + if ( + reasoning_effort is None + and isinstance(model_settings.extra_body, dict) + and "reasoning_effort" in model_settings.extra_body + ): + reasoning_effort = model_settings.extra_body["reasoning_effort"] + + if ( + reasoning_effort is None + and model_settings.extra_args + and "reasoning_effort" in model_settings.extra_args + ): + reasoning_effort = model_settings.extra_args["reasoning_effort"] + + return reasoning_effort + async def get_response( self, system_instructions: str | None, @@ -456,37 +495,7 @@ async def _fetch_response( f"Response format: {response_format}\n" ) - # Build reasoning_effort - use dict only when summary is present (OpenAI feature) - # Otherwise pass string for backward compatibility with all providers - reasoning_effort: dict[str, Any] | str | None = None - if model_settings.reasoning: - if model_settings.reasoning.summary is not None: - # Dict format when summary is needed (OpenAI only) - reasoning_effort = { - "effort": model_settings.reasoning.effort, - "summary": model_settings.reasoning.summary, - } - elif model_settings.reasoning.effort is not None: - # String format for compatibility with all providers - reasoning_effort = model_settings.reasoning.effort - - # Enable developers to pass non-OpenAI compatible reasoning_effort data like "none" - # Priority order: - # 1. model_settings.reasoning (effort + summary) - # 2. model_settings.extra_body["reasoning_effort"] - # 3. model_settings.extra_args["reasoning_effort"] - if ( - reasoning_effort is None # Unset in model_settings - and isinstance(model_settings.extra_body, dict) - and "reasoning_effort" in model_settings.extra_body - ): - reasoning_effort = model_settings.extra_body["reasoning_effort"] - if ( - reasoning_effort is None # Unset in both model_settings and model_settings.extra_body - and model_settings.extra_args - and "reasoning_effort" in model_settings.extra_args - ): - reasoning_effort = model_settings.extra_args["reasoning_effort"] + reasoning_effort = self._get_reasoning_effort(model_settings) stream_options = None if stream and model_settings.include_usage is not None: diff --git a/tests/models/test_litellm_extra_body.py b/tests/models/test_litellm_extra_body.py index e85d2c3e84..f303b29ee6 100644 --- a/tests/models/test_litellm_extra_body.py +++ b/tests/models/test_litellm_extra_body.py @@ -1,3 +1,5 @@ +import logging + import litellm import pytest from litellm.types.utils import Choices, Message, ModelResponse, Usage @@ -160,15 +162,22 @@ async def fake_acompletion(model, messages=None, **kwargs): @pytest.mark.allow_call_model_methods @pytest.mark.asyncio -async def test_reasoning_summary_is_preserved(monkeypatch): +@pytest.mark.parametrize( + "model_name", + [ + "openai/gpt-5-mini", + "anthropic/claude-sonnet-4-5", + "gemini/gemini-2.5-pro", + ], +) +async def test_reasoning_summary_uses_scalar_effort_and_warns( + monkeypatch, caplog: pytest.LogCaptureFixture, model_name: str +): """ - Ensure reasoning.summary is preserved when passing ModelSettings.reasoning. - - This test verifies the fix for GitHub issue: - https://github.com/BerriAI/litellm/issues/17428 + Ensure reasoning.summary does not change the LiteLLM chat-completions argument shape. - Previously, only reasoning.effort was extracted, losing the summary field. - Now we pass a dict with both effort and summary to LiteLLM. + LitellmModel should continue to pass a scalar reasoning_effort value and warn that summary + is ignored on this path, regardless of the provider encoded in the model string. """ from openai.types.shared import Reasoning @@ -184,18 +193,24 @@ async def fake_acompletion(model, messages=None, **kwargs): settings = ModelSettings( reasoning=Reasoning(effort="medium", summary="auto"), ) - model = LitellmModel(model="test-model") - - await model.get_response( - system_instructions=None, - input=[], - model_settings=settings, - tools=[], - output_schema=None, - handoffs=[], - tracing=ModelTracing.DISABLED, - previous_response_id=None, - ) - - # Both effort and summary should be preserved in the dict - assert captured["reasoning_effort"] == {"effort": "medium", "summary": "auto"} + model = LitellmModel(model=model_name) + + with caplog.at_level(logging.WARNING, logger="openai.agents"): + await model.get_response( + system_instructions=None, + input=[], + model_settings=settings, + tools=[], + output_schema=None, + handoffs=[], + tracing=ModelTracing.DISABLED, + previous_response_id=None, + ) + + assert captured["reasoning_effort"] == "medium" + warning_messages = [ + record.message + for record in caplog.records + if "does not forward Reasoning.summary" in record.message + ] + assert len(warning_messages) == 1 From c2f6690ff242bd990c031dbc9701370b34a3b559 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Thu, 26 Mar 2026 20:09:37 +0900 Subject: [PATCH 11/40] fix: #2776 keep private tool metadata out of persisted session items (#2781) --- .../openai_responses_compaction_session.py | 15 +- src/agents/run_internal/items.py | 35 ++++- .../run_internal/session_persistence.py | 25 ++- ...est_openai_responses_compaction_session.py | 102 ++++++++++++ tests/test_agent_runner.py | 145 ++++++++++++++++++ tests/test_items_helpers.py | 30 +++- tests/test_run_internal_items.py | 76 ++++++++- 7 files changed, 412 insertions(+), 16 deletions(-) diff --git a/src/agents/memory/openai_responses_compaction_session.py b/src/agents/memory/openai_responses_compaction_session.py index d1adca9954..4f8fbb37ae 100644 --- a/src/agents/memory/openai_responses_compaction_session.py +++ b/src/agents/memory/openai_responses_compaction_session.py @@ -6,6 +6,7 @@ from openai import AsyncOpenAI from ..models._openai_shared import get_default_openai_client +from ..run_internal.items import normalize_input_items_for_api from .openai_conversations_session import OpenAIConversationsSession from .session import ( OpenAIResponsesCompactionArgs, @@ -270,11 +271,12 @@ def _clear_deferred_compaction(self) -> None: async def add_items(self, items: list[TResponseInputItem]) -> None: await self.underlying_session.add_items(items) if self._compaction_candidate_items is not None: - new_candidates = select_compaction_candidate_items(items) + new_items = _normalize_compaction_session_items(items) + new_candidates = select_compaction_candidate_items(new_items) if new_candidates: self._compaction_candidate_items.extend(new_candidates) if self._session_items is not None: - self._session_items.extend(items) + self._session_items.extend(_normalize_compaction_session_items(items)) async def pop_item(self) -> TResponseInputItem | None: popped = await self.underlying_session.pop_item() @@ -296,7 +298,7 @@ async def _ensure_compaction_candidates( if self._compaction_candidate_items is not None and self._session_items is not None: return (self._compaction_candidate_items[:], self._session_items[:]) - history = await self.underlying_session.get_items() + history = _normalize_compaction_session_items(await self.underlying_session.get_items()) candidates = select_compaction_candidate_items(history) self._compaction_candidate_items = candidates self._session_items = history @@ -336,6 +338,13 @@ def _strip_orphaned_assistant_ids( return cleaned +def _normalize_compaction_session_items( + items: list[TResponseInputItem], +) -> list[TResponseInputItem]: + """Normalize compaction input so SDK-only metadata never reaches responses.compact.""" + return normalize_input_items_for_api(list(items)) + + _ResolvedCompactionMode = Literal["previous_response_id", "input"] diff --git a/src/agents/run_internal/items.py b/src/agents/run_internal/items.py index ebf402034f..3e0693b02e 100644 --- a/src/agents/run_internal/items.py +++ b/src/agents/run_internal/items.py @@ -18,6 +18,8 @@ from ..tool import DEFAULT_APPROVAL_REJECTION_MESSAGE REJECTION_MESSAGE = DEFAULT_APPROVAL_REJECTION_MESSAGE +TOOL_CALL_SESSION_DESCRIPTION_KEY = "_agents_tool_description" +TOOL_CALL_SESSION_TITLE_KEY = "_agents_tool_title" _TOOL_CALL_TO_OUTPUT_TYPE: dict[str, str] = { "function_call": "function_call_output", "shell_call": "shell_call_output", @@ -30,6 +32,8 @@ __all__ = [ "ReasoningItemIdPolicy", "REJECTION_MESSAGE", + "TOOL_CALL_SESSION_DESCRIPTION_KEY", + "TOOL_CALL_SESSION_TITLE_KEY", "copy_input_items", "drop_orphan_function_calls", "ensure_input_item_format", @@ -41,6 +45,7 @@ "fingerprint_input_item", "deduplicate_input_items", "deduplicate_input_items_preferring_latest", + "strip_internal_input_item_metadata", "function_rejection_item", "shell_rejection_item", "apply_patch_rejection_item", @@ -148,8 +153,8 @@ def normalize_input_items_for_api(items: list[TResponseInputItem]) -> list[TResp normalized.append(item) continue - normalized_item = dict(coerced) - normalized.append(cast(TResponseInputItem, normalized_item)) + normalized_item = strip_internal_input_item_metadata(cast(TResponseInputItem, coerced)) + normalized.append(normalized_item) return normalized @@ -188,12 +193,25 @@ def fingerprint_input_item(item: Any, *, ignore_ids_for_matching: bool = False) payload = _model_dump_without_warnings(item) if payload is None: return None + if isinstance(payload, dict): + payload = cast( + dict[str, Any], + strip_internal_input_item_metadata(cast(TResponseInputItem, payload)), + ) elif isinstance(item, dict): - payload = dict(item) + payload = cast( + dict[str, Any], + strip_internal_input_item_metadata(cast(TResponseInputItem, item)), + ) if ignore_ids_for_matching: payload.pop("id", None) else: payload = ensure_input_item_format(item) + if isinstance(payload, dict): + payload = cast( + dict[str, Any], + strip_internal_input_item_metadata(cast(TResponseInputItem, payload)), + ) if ignore_ids_for_matching and isinstance(payload, dict): payload.pop("id", None) @@ -231,6 +249,17 @@ def _dedupe_key(item: TResponseInputItem) -> str | None: return None +def strip_internal_input_item_metadata(item: TResponseInputItem) -> TResponseInputItem: + """Remove SDK-only session metadata before sending items back to the model.""" + if not isinstance(item, dict): + return item + + cleaned = dict(item) + cleaned.pop(TOOL_CALL_SESSION_DESCRIPTION_KEY, None) + cleaned.pop(TOOL_CALL_SESSION_TITLE_KEY, None) + return cast(TResponseInputItem, cleaned) + + def _should_omit_reasoning_item_ids(reasoning_item_id_policy: ReasoningItemIdPolicy | None) -> bool: return reasoning_item_id_policy == "omit" diff --git a/src/agents/run_internal/session_persistence.py b/src/agents/run_internal/session_persistence.py index d63c5f0526..6f27dfd8c1 100644 --- a/src/agents/run_internal/session_persistence.py +++ b/src/agents/run_internal/session_persistence.py @@ -33,6 +33,7 @@ fingerprint_input_item, normalize_input_items_for_api, run_item_to_input_item, + strip_internal_input_item_metadata, ) from .oai_conversation import OpenAIServerConversationTracker from .run_steps import SingleStepResult @@ -85,7 +86,9 @@ async def prepare_input_with_session( history = await session.get_items(limit=resolved_settings.limit) else: history = await session.get_items() - converted_history = [ensure_input_item_format(item) for item in history] + converted_history = [ + strip_internal_input_item_metadata(ensure_input_item_format(item)) for item in history + ] new_input_list = [ ensure_input_item_format(item) for item in ItemHelpers.input_to_new_input_list(input) @@ -164,7 +167,8 @@ async def prepare_input_with_session( normalized = normalize_input_items_for_api(filtered) deduplicated = deduplicate_input_items_preferring_latest(normalized) - return deduplicated, [ensure_input_item_format(item) for item in appended_items] + appended_as_inputs = [ensure_input_item_format(item) for item in appended_items] + return deduplicated, normalize_input_items_for_api(appended_as_inputs) async def persist_session_items_for_guardrail_trip( @@ -262,10 +266,12 @@ async def save_result_to_session( input_list: list[TResponseInputItem] = [] if original_input: - input_list = [ - ensure_input_item_format(item) - for item in ItemHelpers.input_to_new_input_list(original_input) - ] + input_list = normalize_input_items_for_api( + [ + ensure_input_item_format(item) + for item in ItemHelpers.input_to_new_input_list(original_input) + ] + ) resolved_reasoning_item_id_policy = ( reasoning_item_id_policy @@ -562,7 +568,7 @@ def _ignore_ids_for_matching(session: Session) -> bool: def _sanitize_openai_conversation_item(item: TResponseInputItem) -> TResponseInputItem: """Remove provider-specific fields before fingerprinting or persistence.""" if isinstance(item, dict): - clean_item = dict(item) + clean_item = cast(dict[str, Any], strip_internal_input_item_metadata(item)) clean_item.pop("id", None) clean_item.pop("provider_data", None) return cast(TResponseInputItem, clean_item) @@ -585,6 +591,11 @@ def _session_item_key(item: Any) -> str: payload = item else: payload = ensure_input_item_format(item) + if isinstance(payload, dict): + payload = cast( + dict[str, Any], + strip_internal_input_item_metadata(cast(TResponseInputItem, payload)), + ) return json.dumps(payload, sort_keys=True, default=str) except Exception: return repr(item) diff --git a/tests/memory/test_openai_responses_compaction_session.py b/tests/memory/test_openai_responses_compaction_session.py index 653b175618..fd08388f1a 100644 --- a/tests/memory/test_openai_responses_compaction_session.py +++ b/tests/memory/test_openai_responses_compaction_session.py @@ -20,6 +20,10 @@ is_openai_model_name, select_compaction_candidate_items, ) +from agents.run_internal.items import ( + TOOL_CALL_SESSION_DESCRIPTION_KEY, + TOOL_CALL_SESSION_TITLE_KEY, +) from tests.fake_model import FakeModel from tests.test_responses import get_function_tool, get_function_tool_call, get_text_message from tests.utils.simple_session import SimpleListSession @@ -215,6 +219,104 @@ async def test_run_compaction_auto_without_response_id_uses_input(self) -> None: assert "previous_response_id" not in call_kwargs assert call_kwargs.get("input") == items + @pytest.mark.asyncio + async def test_run_compaction_input_mode_strips_internal_tool_call_metadata(self) -> None: + mock_session = self.create_mock_session() + items: list[TResponseInputItem] = [ + cast( + TResponseInputItem, + { + "type": "function_call", + "call_id": "call_123", + "name": "lookup_account", + "arguments": "{}", + TOOL_CALL_SESSION_DESCRIPTION_KEY: "Lookup customer records.", + TOOL_CALL_SESSION_TITLE_KEY: "Lookup Account", + }, + ), + cast( + TResponseInputItem, + { + "type": "function_call_output", + "call_id": "call_123", + "output": "ok", + }, + ), + ] + mock_session.get_items.return_value = items + + mock_compact_response = MagicMock() + mock_compact_response.output = [] + + mock_client = MagicMock() + mock_client.responses.compact = AsyncMock(return_value=mock_compact_response) + + session = OpenAIResponsesCompactionSession( + session_id="test", + underlying_session=mock_session, + client=mock_client, + compaction_mode="input", + ) + + await session.run_compaction({"force": True}) + + call_kwargs = mock_client.responses.compact.call_args.kwargs + compact_input = cast(list[dict[str, Any]], call_kwargs["input"]) + assert compact_input[0]["type"] == "function_call" + assert TOOL_CALL_SESSION_DESCRIPTION_KEY not in compact_input[0] + assert TOOL_CALL_SESSION_TITLE_KEY not in compact_input[0] + + @pytest.mark.asyncio + async def test_run_compaction_uses_sanitized_cached_items_after_add(self) -> None: + mock_session = self.create_mock_session() + mock_session.get_items.return_value = [] + + mock_compact_response = MagicMock() + mock_compact_response.output = [] + + mock_client = MagicMock() + mock_client.responses.compact = AsyncMock(return_value=mock_compact_response) + + session = OpenAIResponsesCompactionSession( + session_id="test", + underlying_session=mock_session, + client=mock_client, + compaction_mode="input", + ) + + await session._ensure_compaction_candidates() + await session.add_items( + [ + cast( + TResponseInputItem, + { + "type": "function_call", + "call_id": "call_cached", + "name": "lookup_account", + "arguments": "{}", + TOOL_CALL_SESSION_DESCRIPTION_KEY: "Lookup customer records.", + TOOL_CALL_SESSION_TITLE_KEY: "Lookup Account", + }, + ), + cast( + TResponseInputItem, + { + "type": "function_call_output", + "call_id": "call_cached", + "output": "ok", + }, + ), + ] + ) + + await session.run_compaction({"force": True}) + + call_kwargs = mock_client.responses.compact.call_args.kwargs + compact_input = cast(list[dict[str, Any]], call_kwargs["input"]) + assert compact_input[0]["type"] == "function_call" + assert TOOL_CALL_SESSION_DESCRIPTION_KEY not in compact_input[0] + assert TOOL_CALL_SESSION_TITLE_KEY not in compact_input[0] + @pytest.mark.asyncio async def test_run_compaction_auto_uses_input_when_store_false(self) -> None: mock_session = self.create_mock_session() diff --git a/tests/test_agent_runner.py b/tests/test_agent_runner.py index ae688fe4e9..0db1032c88 100644 --- a/tests/test_agent_runner.py +++ b/tests/test_agent_runner.py @@ -48,6 +48,7 @@ ReasoningItem, RunItem, ToolApprovalItem, + ToolCallItem, ToolCallOutputItem, TResponseInputItem, ) @@ -55,6 +56,8 @@ from agents.run import AgentRunner, get_default_agent_runner, set_default_agent_runner from agents.run_config import _default_trace_include_sensitive_data from agents.run_internal.items import ( + TOOL_CALL_SESSION_DESCRIPTION_KEY, + TOOL_CALL_SESSION_TITLE_KEY, drop_orphan_function_calls, ensure_input_item_format, fingerprint_input_item, @@ -2394,6 +2397,148 @@ async def test_save_result_to_session_omits_reasoning_ids_when_policy_is_omit() assert "id" not in saved_reasoning +@pytest.mark.asyncio +async def test_save_result_to_session_keeps_tool_call_payload_api_safe() -> None: + session = SimpleListSession() + agent = Agent(name="agent", model=FakeModel()) + tool_call = ToolCallItem( + agent=agent, + raw_item=ResponseFunctionToolCall( + id="fc_session", + call_id="call_session", + name="lookup_account", + arguments="{}", + type="function_call", + status="completed", + ), + description="Lookup customer records.", + title="Lookup Account", + ) + + saved_count = await save_result_to_session( + session, + [], + cast(list[RunItem], [tool_call]), + None, + ) + + assert saved_count == 1 + assert len(session.saved_items) == 1 + saved_tool_call = cast(dict[str, Any], session.saved_items[0]) + assert saved_tool_call["type"] == "function_call" + assert TOOL_CALL_SESSION_DESCRIPTION_KEY not in saved_tool_call + assert TOOL_CALL_SESSION_TITLE_KEY not in saved_tool_call + assert "description" not in saved_tool_call + assert "title" not in saved_tool_call + + +@pytest.mark.asyncio +async def test_save_result_to_session_sanitizes_original_input_items() -> None: + session = SimpleListSession() + + saved_count = await save_result_to_session( + session, + [ + cast( + TResponseInputItem, + { + "type": "function_call", + "call_id": "call_input", + "name": "lookup_account", + "arguments": "{}", + TOOL_CALL_SESSION_DESCRIPTION_KEY: "Lookup customer records.", + TOOL_CALL_SESSION_TITLE_KEY: "Lookup Account", + }, + ) + ], + [], + None, + ) + + assert saved_count == 0 + assert len(session.saved_items) == 1 + saved_tool_call = cast(dict[str, Any], session.saved_items[0]) + assert saved_tool_call["type"] == "function_call" + assert TOOL_CALL_SESSION_DESCRIPTION_KEY not in saved_tool_call + assert TOOL_CALL_SESSION_TITLE_KEY not in saved_tool_call + assert "description" not in saved_tool_call + assert "title" not in saved_tool_call + + +@pytest.mark.asyncio +async def test_prepare_input_with_session_strips_internal_tool_call_metadata() -> None: + tool_call = cast( + TResponseInputItem, + { + "type": "function_call", + "call_id": "call_history", + "name": "lookup_account", + "arguments": "{}", + TOOL_CALL_SESSION_DESCRIPTION_KEY: "Lookup customer records.", + TOOL_CALL_SESSION_TITLE_KEY: "Lookup Account", + }, + ) + tool_output = cast( + TResponseInputItem, + { + "type": "function_call_output", + "call_id": "call_history", + "output": "ok", + }, + ) + session = SimpleListSession(history=[tool_call, tool_output]) + + prepared_input, session_items = await prepare_input_with_session("hello", session, None) + + assert isinstance(prepared_input, list) + prepared_tool_calls = [ + cast(dict[str, Any], item) + for item in prepared_input + if isinstance(item, dict) + and item.get("type") == "function_call" + and item.get("call_id") == "call_history" + ] + assert len(prepared_tool_calls) == 1 + assert TOOL_CALL_SESSION_DESCRIPTION_KEY not in prepared_tool_calls[0] + assert TOOL_CALL_SESSION_TITLE_KEY not in prepared_tool_calls[0] + assert len(session_items) == 1 + assert cast(dict[str, Any], session_items[0])["role"] == "user" + + +@pytest.mark.asyncio +async def test_prepare_input_with_session_sanitizes_new_tool_call_session_items() -> None: + prepared_input, session_items = await prepare_input_with_session( + [ + cast( + TResponseInputItem, + { + "type": "function_call", + "call_id": "call_new", + "name": "lookup_account", + "arguments": "{}", + TOOL_CALL_SESSION_DESCRIPTION_KEY: "Lookup customer records.", + TOOL_CALL_SESSION_TITLE_KEY: "Lookup Account", + }, + ) + ], + SimpleListSession(), + None, + ) + + assert isinstance(prepared_input, list) + assert len(prepared_input) == 1 + prepared_tool_call = cast(dict[str, Any], prepared_input[0]) + assert prepared_tool_call["type"] == "function_call" + assert TOOL_CALL_SESSION_DESCRIPTION_KEY not in prepared_tool_call + assert TOOL_CALL_SESSION_TITLE_KEY not in prepared_tool_call + + assert len(session_items) == 1 + session_tool_call = cast(dict[str, Any], session_items[0]) + assert session_tool_call["type"] == "function_call" + assert TOOL_CALL_SESSION_DESCRIPTION_KEY not in session_tool_call + assert TOOL_CALL_SESSION_TITLE_KEY not in session_tool_call + + @pytest.mark.asyncio async def test_session_persists_only_new_step_items(monkeypatch: pytest.MonkeyPatch) -> None: """Ensure only per-turn new_step_items are persisted to the session.""" diff --git a/tests/test_items_helpers.py b/tests/test_items_helpers.py index 41fe9fb684..e3e52874c8 100644 --- a/tests/test_items_helpers.py +++ b/tests/test_items_helpers.py @@ -3,7 +3,7 @@ import gc import json import weakref -from typing import cast +from typing import Any, cast from openai.types.responses.computer_action import Click as BatchedClick, Type as BatchedType from openai.types.responses.response_computer_tool_call import ( @@ -45,7 +45,7 @@ TResponseInputItem, Usage, ) -from agents.items import ToolCallOutputItem +from agents.items import ToolCallItem, ToolCallOutputItem def make_message( @@ -568,3 +568,29 @@ def test_input_to_new_input_list_copies_the_ones_produced_by_pydantic() -> None: # This used to fail when validated payloads retained ValidatorIterator fields. json.dumps(new_list) + + +def test_tool_call_item_to_input_item_keeps_payload_api_safe() -> None: + agent = Agent(name="test", instructions="test") + raw_item = ResponseFunctionToolCall( + id="fc_1", + call_id="call_1", + name="my_tool", + arguments="{}", + type="function_call", + status="completed", + ) + item = ToolCallItem( + agent=agent, + raw_item=raw_item, + title="My Tool", + description="A helpful tool", + ) + + result = item.to_input_item() + result_dict = cast(dict[str, Any], result) + + assert isinstance(result, dict) + assert result_dict["type"] == "function_call" + assert "title" not in result_dict + assert "description" not in result_dict diff --git a/tests/test_run_internal_items.py b/tests/test_run_internal_items.py index ef2632d1f8..e7daafa577 100644 --- a/tests/test_run_internal_items.py +++ b/tests/test_run_internal_items.py @@ -3,13 +3,18 @@ from typing import Any, cast import pytest -from openai.types.responses import ResponseToolSearchCall, ResponseToolSearchOutputItem +from openai.types.responses import ( + ResponseFunctionToolCall, + ResponseToolSearchCall, + ResponseToolSearchOutputItem, +) from openai.types.responses.response_reasoning_item import ResponseReasoningItem from agents import Agent from agents.exceptions import AgentsException from agents.items import ( ReasoningItem, + ToolCallItem, ToolSearchCallItem, ToolSearchOutputItem, TResponseInputItem, @@ -459,6 +464,75 @@ def test_run_item_to_input_item_strips_tool_search_created_by() -> None: assert "created_by" not in converted_output +def test_run_item_to_input_item_omits_tool_call_metadata() -> None: + agent = Agent(name="A") + tool_call = ToolCallItem( + agent=agent, + raw_item=ResponseFunctionToolCall( + id="fc_123", + call_id="call_123", + name="lookup_account", + arguments="{}", + type="function_call", + status="completed", + ), + description="Lookup customer records.", + title="Lookup Account", + ) + + result = run_items.run_item_to_input_item(tool_call) + result_dict = cast(dict[str, Any], result) + + assert isinstance(result, dict) + assert result_dict["type"] == "function_call" + assert "description" not in result_dict + assert "title" not in result_dict + + +def test_normalize_input_items_for_api_strips_internal_tool_call_metadata() -> None: + item = cast( + TResponseInputItem, + { + "type": "function_call", + "call_id": "call_123", + "name": "lookup_account", + "arguments": "{}", + run_items.TOOL_CALL_SESSION_DESCRIPTION_KEY: "Lookup customer records.", + run_items.TOOL_CALL_SESSION_TITLE_KEY: "Lookup Account", + }, + ) + + normalized = run_items.normalize_input_items_for_api([item]) + normalized_item = cast(dict[str, Any], normalized[0]) + + assert run_items.TOOL_CALL_SESSION_DESCRIPTION_KEY not in normalized_item + assert run_items.TOOL_CALL_SESSION_TITLE_KEY not in normalized_item + + +def test_fingerprint_input_item_ignores_internal_tool_call_metadata() -> None: + base_item = cast( + TResponseInputItem, + { + "type": "function_call", + "call_id": "call_123", + "name": "lookup_account", + "arguments": "{}", + }, + ) + with_metadata = cast( + TResponseInputItem, + { + **cast(dict[str, Any], base_item), + run_items.TOOL_CALL_SESSION_DESCRIPTION_KEY: "Lookup customer records.", + run_items.TOOL_CALL_SESSION_TITLE_KEY: "Lookup Account", + }, + ) + + assert run_items.fingerprint_input_item(base_item) == run_items.fingerprint_input_item( + with_metadata + ) + + def test_run_result_to_input_list_preserves_tool_search_items() -> None: agent = Agent(name="A") result = RunResult( From 570b114770f51854ea2ea3a83ff495edaf6a6e8d Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Thu, 26 Mar 2026 20:52:17 +0900 Subject: [PATCH 12/40] docs: update readme --- README.md | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d7c3e062b1..3fb925a2f0 100644 --- a/README.md +++ b/README.md @@ -70,10 +70,22 @@ Explore the [examples](https://github.com/openai/openai-agents-python/tree/main/ We'd like to acknowledge the excellent work of the open-source community, especially: -- [Pydantic](https://docs.pydantic.dev/latest/) (data validation) and [PydanticAI](https://ai.pydantic.dev/) (advanced agent framework) -- [LiteLLM](https://github.com/BerriAI/litellm) (unified interface for 100+ LLMs) -- [MkDocs](https://github.com/squidfunk/mkdocs-material) -- [Griffe](https://github.com/mkdocstrings/griffe) -- [uv](https://github.com/astral-sh/uv) and [ruff](https://github.com/astral-sh/ruff) +- [Pydantic](https://docs.pydantic.dev/latest/) +- [Requests](https://github.com/psf/requests) +- [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk) +- [Griffe](https://github.com/mkdocstrings/griffe) -We're committed to continuing to build the Agents SDK as an open source framework so others in the community can expand on our approach. +This library has these optional dependencies: + +- [websockets](https://github.com/python-websockets/websockets) +- [SQLAlchemy](https://github.com/sqlalchemy/sqlalchemy) +- [any-llm](https://github.com/mozilla-ai/any-llm) and [LiteLLM](https://github.com/BerriAI/litellm) + +We also rely on the following tools to manage the project: + +- [uv](https://github.com/astral-sh/uv) and [ruff](https://github.com/astral-sh/ruff) +- [mypy](https://github.com/python/mypy) and [Pyright](https://github.com/microsoft/pyright) +- [pytest](https://github.com/pytest-dev/pytest) and [Coverage.py](https://github.com/coveragepy/coveragepy) +- [MkDocs](https://github.com/squidfunk/mkdocs-material) + +We're committed to continuing to build the Agents SDK as an open source framework so others in the community can expand on our approach. \ No newline at end of file From c52d25f4f93c5ac3c8021b3846d5800b829eeaa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=B5=E3=81=81=E3=83=BC?= <47295014+ymuichiro@users.noreply.github.com> Date: Thu, 26 Mar 2026 21:40:18 +0900 Subject: [PATCH 13/40] feat: #2785 add external_web_access to WebSearchTool (#2786) --- src/agents/models/openai_responses.py | 17 +++++++++-------- src/agents/tool.py | 7 +++++++ tests/test_openai_responses_converter.py | 18 ++++++++++++++++++ 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/agents/models/openai_responses.py b/src/agents/models/openai_responses.py index f98da12344..683e15a6d7 100644 --- a/src/agents/models/openai_responses.py +++ b/src/agents/models/openai_responses.py @@ -1935,15 +1935,16 @@ def _convert_tool( if isinstance(tool, FunctionTool): return cls._convert_function_tool(tool) elif isinstance(tool, WebSearchTool): + web_search_tool: dict[str, Any] = { + "type": "web_search", + "filters": tool.filters.model_dump() if tool.filters is not None else None, + "user_location": tool.user_location, + "search_context_size": tool.search_context_size, + } + if tool.external_web_access is not None: + web_search_tool["external_web_access"] = tool.external_web_access return ( - _require_responses_tool_param( - { - "type": "web_search", - "filters": tool.filters.model_dump() if tool.filters is not None else None, - "user_location": tool.user_location, - "search_context_size": tool.search_context_size, - } - ), + _require_responses_tool_param(web_search_tool), None, ) elif isinstance(tool, FileSearchTool): diff --git a/src/agents/tool.py b/src/agents/tool.py index ed428a90b9..1ac3c29ae3 100644 --- a/src/agents/tool.py +++ b/src/agents/tool.py @@ -499,6 +499,13 @@ class WebSearchTool: search_context_size: Literal["low", "medium", "high"] = "medium" """The amount of context to use for the search.""" + external_web_access: bool | None = None + """Whether the web search tool may fetch live internet content. + + When omitted, the API default is used. Set to `False` to request cached or + indexed-only behavior where supported. + """ + @property def name(self): return "web_search" diff --git a/tests/test_openai_responses_converter.py b/tests/test_openai_responses_converter.py index 034d80d310..a461785ede 100644 --- a/tests/test_openai_responses_converter.py +++ b/tests/test_openai_responses_converter.py @@ -437,6 +437,7 @@ def test_convert_tools_basic_types_and_includes(): web_params = next(ct for ct in converted.tools if ct["type"] == "web_search") assert web_params.get("user_location") == web_tool.user_location assert web_params.get("search_context_size") == web_tool.search_context_size + assert "external_web_access" not in web_params # Verify computer tool uses the GA built-in tool payload. comp_params = next(ct for ct in converted.tools if ct["type"] == "computer") assert comp_params == {"type": "computer"} @@ -450,6 +451,23 @@ def test_convert_tools_basic_types_and_includes(): Converter.convert_tools(tools=[comp_tool, comp_tool], handoffs=[]) +def test_convert_tools_includes_explicit_false_external_web_access() -> None: + web_tool = WebSearchTool(external_web_access=False) + + converted = Converter.convert_tools([web_tool], handoffs=[], model="gpt-5.4") + + assert converted.includes == [] + assert converted.tools == [ + { + "type": "web_search", + "filters": None, + "user_location": None, + "search_context_size": "medium", + "external_web_access": False, + } + ] + + def test_convert_tools_uses_preview_computer_payload_for_preview_model() -> None: comp_tool = ComputerTool(computer=DummyComputer()) From 7fea1a549e727944ad7522bf3601583b885f0ebb Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Thu, 26 Mar 2026 21:54:45 +0900 Subject: [PATCH 14/40] fix: improve a flaky test for realtime module (#2787) --- tests/realtime/test_openai_realtime.py | 135 ++++++++++++------------- 1 file changed, 62 insertions(+), 73 deletions(-) diff --git a/tests/realtime/test_openai_realtime.py b/tests/realtime/test_openai_realtime.py index 45d7890d19..157c575b24 100644 --- a/tests/realtime/test_openai_realtime.py +++ b/tests/realtime/test_openai_realtime.py @@ -1863,6 +1863,40 @@ def mock_create_task_func(coro): assert captured_kwargs_long.get("ping_interval") == 5.0 assert captured_kwargs_long.get("ping_timeout") == 10.0 + @pytest.mark.asyncio + async def test_handshake_timeout_config_is_applied(self): + """Test that handshake_timeout is passed through as websockets open_timeout.""" + captured_kwargs: dict[str, Any] = {} + + async def capture_connect(*args, **kwargs): + captured_kwargs.update(kwargs) + mock_ws = AsyncMock() + mock_ws.close_code = None + return mock_ws + + transport: TransportConfig = { + "handshake_timeout": 0.75, + } + model = OpenAIRealtimeWebSocketModel(transport_config=transport) + with patch("websockets.connect", side_effect=capture_connect): + with patch("asyncio.create_task") as mock_create_task: + mock_task = AsyncMock() + + def mock_create_task_func(coro): + coro.close() + return mock_task + + mock_create_task.side_effect = mock_create_task_func + + config: RealtimeModelConfig = { + "api_key": "test-key", + "url": "ws://localhost:8080/v1/realtime", + "initial_model_settings": {"model_name": "gpt-4o-realtime-preview"}, + } + await model.connect(config) + + assert captured_kwargs.get("open_timeout") == 0.75 + @pytest.mark.asyncio async def test_ping_timeout_disabled_vs_enabled(self): """Test that ping timeout can be disabled (None) vs enabled with a value.""" @@ -1978,78 +2012,37 @@ async def test_handshake_timeout_with_delayed_server(self): - Success: client timeout > server delay - Failure: client timeout < server delay """ - import base64 - import hashlib - # Server handshake delay threshold (in seconds) - SERVER_HANDSHAKE_DELAY = 0.05 + SERVER_HANDSHAKE_DELAY = 0.5 shutdown_event = asyncio.Event() - connections_attempted = [] - - async def delayed_websocket_server(reader, writer): - """A WebSocket server that delays the handshake by a fixed amount.""" - connections_attempted.append(True) - try: - # Read HTTP upgrade request - request = b"" - while b"\r\n\r\n" not in request: - chunk = await asyncio.wait_for(reader.read(1024), timeout=5.0) - if not chunk: - return - request += chunk - - # Extract Sec-WebSocket-Key - key = None - for line in request.decode().split("\r\n"): - if line.lower().startswith("sec-websocket-key:"): - key = line.split(":", 1)[1].strip() - break - - if not key: - writer.close() - return - - # Intentional delay before completing handshake - await asyncio.sleep(SERVER_HANDSHAKE_DELAY) - - # Generate accept key - GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" - accept = base64.b64encode(hashlib.sha1((key + GUID).encode()).digest()).decode() - - # Send HTTP 101 Switching Protocols response - response = ( - "HTTP/1.1 101 Switching Protocols\r\n" - "Upgrade: websocket\r\n" - "Connection: Upgrade\r\n" - f"Sec-WebSocket-Accept: {accept}\r\n" - "\r\n" - ) - writer.write(response.encode()) - await writer.drain() - - # Keep connection open until shutdown, then send a close frame so - # the client can complete close() without waiting for a timeout. - await shutdown_event.wait() - writer.write(b"\x88\x00") - await writer.drain() - - except asyncio.TimeoutError: - pass - except Exception: - pass - finally: - writer.close() - - server = await asyncio.start_server(delayed_websocket_server, "127.0.0.1", 0) - port = server.sockets[0].getsockname()[1] - url = f"ws://127.0.0.1:{port}/v1/realtime" + handshake_started = asyncio.Event() + handshake_attempts = 0 + + async def process_request(_connection, _request): + nonlocal handshake_attempts + handshake_attempts += 1 + handshake_started.set() + await asyncio.sleep(SERVER_HANDSHAKE_DELAY) + return None + + async def delayed_handler(_websocket): + await shutdown_event.wait() + + async with websockets.serve( + delayed_handler, + "127.0.0.1", + 0, + process_request=process_request, + ) as server: + sockets = list(server.sockets) + port = sockets[0].getsockname()[1] + url = f"ws://127.0.0.1:{port}/v1/realtime" - try: # Test 1: FAILURE - Client timeout < server delay # Client gives up before server completes handshake transport_fail: TransportConfig = { - "handshake_timeout": 0.01, + "handshake_timeout": 0.2, } model_fail = OpenAIRealtimeWebSocketModel(transport_config=transport_fail) config_fail: RealtimeModelConfig = { @@ -2061,13 +2054,14 @@ async def delayed_websocket_server(reader, writer): with pytest.raises((TimeoutError, asyncio.TimeoutError)): await model_fail.connect(config_fail) - # Verify connection was attempted - assert len(connections_attempted) >= 1 + # Wait briefly for the server to observe the request before asserting. + await asyncio.wait_for(handshake_started.wait(), timeout=1.0) + assert handshake_attempts >= 1 # Test 2: SUCCESS - Client timeout > server delay # Client waits long enough for server to complete handshake transport_success: TransportConfig = { - "handshake_timeout": 0.2, + "handshake_timeout": 1.0, } model_success = OpenAIRealtimeWebSocketModel(transport_config=transport_success) config_success: RealtimeModelConfig = { @@ -2085,11 +2079,6 @@ async def delayed_websocket_server(reader, writer): shutdown_event.set() await model_success.close() - finally: - shutdown_event.set() - server.close() - await server.wait_closed() - @pytest.mark.asyncio async def test_ping_interval_comparison_fast_vs_slow(self): """Test that faster ping intervals detect issues sooner than slower ones.""" From c5e132142cc72d13cbeb99ea5a0c2b73f2e586b9 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Fri, 27 Mar 2026 08:18:04 +0900 Subject: [PATCH 15/40] fix: #2783 depend on griffelib directly for docstring parsing (#2791) --- pyproject.toml | 2 +- src/agents/function_schema.py | 3 ++- uv.lock | 29 +++++++++++++---------------- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6d3347264c..ad0a22a99d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ authors = [{ name = "OpenAI", email = "support@openai.com" }] dependencies = [ "openai>=2.26.0,<3", "pydantic>=2.12.2, <3", - "griffe>=1.5.6, <2", + "griffelib>=2, <3", "typing-extensions>=4.12.2, <5", "requests>=2.0, <3", "types-requests>=2.0, <3", diff --git a/src/agents/function_schema.py b/src/agents/function_schema.py index cff7f987e6..881ebdf00f 100644 --- a/src/agents/function_schema.py +++ b/src/agents/function_schema.py @@ -7,7 +7,8 @@ from dataclasses import dataclass from typing import Annotated, Any, Callable, Literal, get_args, get_origin, get_type_hints -from griffe import Docstring, DocstringSectionKind +# griffelib exposes the `griffe` package at runtime but currently does not ship typing markers. +from griffe import Docstring, DocstringSectionKind # type: ignore[import-untyped] from pydantic import BaseModel, Field, create_model from pydantic.fields import FieldInfo diff --git a/uv.lock b/uv.lock index fe9b1a28b4..88d2363567 100644 --- a/uv.lock +++ b/uv.lock @@ -936,15 +936,12 @@ wheels = [ ] [[package]] -name = "griffe" -version = "1.11.1" +name = "griffelib" +version = "2.0.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/18/0f/9cbd56eb047de77a4b93d8d4674e70cd19a1ff64d7410651b514a1ed93d5/griffe-1.11.1.tar.gz", hash = "sha256:d54ffad1ec4da9658901eb5521e9cddcdb7a496604f67d8ae71077f03f549b7e", size = 410996, upload-time = "2025-08-11T11:38:35.528Z" } +sdist = { url = "https://files.pythonhosted.org/packages/71/d7/2b805e89cdc609e5b304361d80586b272ef00f6287ee63de1e571b1f71ec/griffelib-2.0.1.tar.gz", hash = "sha256:59f39eabb4c777483a3823e39e8f9e03e69df271a7e49aee64e91a8cfa91bdf5", size = 166383, upload-time = "2026-03-23T21:05:25.882Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/a3/451ffd422ce143758a39c0290aaa7c9727ecc2bcc19debd7a8f3c6075ce9/griffe-1.11.1-py3-none-any.whl", hash = "sha256:5799cf7c513e4b928cfc6107ee6c4bc4a92e001f07022d97fd8dee2f612b6064", size = 138745, upload-time = "2025-08-11T11:38:33.964Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/cc8c68196db727cfc1432f2ad5de50aa6707e630d44b2e6361dc06d8f134/griffelib-2.0.1-py3-none-any.whl", hash = "sha256:b769eed581c0e857d362fc8fcd8e57ecd2330c124b6104ac8b4c1c86d76970aa", size = 142377, upload-time = "2026-03-23T21:04:01.116Z" }, ] [[package]] @@ -1534,7 +1531,7 @@ wheels = [ [[package]] name = "mkdocstrings" -version = "0.30.0" +version = "1.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, @@ -1544,9 +1541,9 @@ dependencies = [ { name = "mkdocs-autorefs" }, { name = "pymdown-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e2/0a/7e4776217d4802009c8238c75c5345e23014a4706a8414a62c0498858183/mkdocstrings-0.30.0.tar.gz", hash = "sha256:5d8019b9c31ddacd780b6784ffcdd6f21c408f34c0bd1103b5351d609d5b4444", size = 106597, upload-time = "2025-07-22T23:48:45.998Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/62/0dfc5719514115bf1781f44b1d7f2a0923fcc01e9c5d7990e48a05c9ae5d/mkdocstrings-1.0.3.tar.gz", hash = "sha256:ab670f55040722b49bb45865b2e93b824450fb4aef638b00d7acb493a9020434", size = 100946, upload-time = "2026-02-07T14:31:40.973Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/b4/3c5eac68f31e124a55d255d318c7445840fa1be55e013f507556d6481913/mkdocstrings-0.30.0-py3-none-any.whl", hash = "sha256:ae9e4a0d8c1789697ac776f2e034e2ddd71054ae1cf2c2bb1433ccfd07c226f2", size = 36579, upload-time = "2025-07-22T23:48:44.152Z" }, + { url = "https://files.pythonhosted.org/packages/04/41/1cf02e3df279d2dd846a1bf235a928254eba9006dd22b4a14caa71aed0f7/mkdocstrings-1.0.3-py3-none-any.whl", hash = "sha256:0d66d18430c2201dc7fe85134277382baaa15e6b30979f3f3bdbabd6dbdb6046", size = 35523, upload-time = "2026-02-07T14:31:39.27Z" }, ] [package.optional-dependencies] @@ -1556,17 +1553,17 @@ python = [ [[package]] name = "mkdocstrings-python" -version = "1.16.12" +version = "2.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "griffe" }, + { name = "griffelib" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/ed/b886f8c714fd7cccc39b79646b627dbea84cd95c46be43459ef46852caf0/mkdocstrings_python-1.16.12.tar.gz", hash = "sha256:9b9eaa066e0024342d433e332a41095c4e429937024945fea511afe58f63175d", size = 206065, upload-time = "2025-06-03T12:52:49.276Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/33/c225eaf898634bdda489a6766fc35d1683c640bffe0e0acd10646b13536d/mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8", size = 199083, upload-time = "2026-02-20T10:38:36.368Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/dd/a24ee3de56954bfafb6ede7cd63c2413bb842cc48eb45e41c43a05a33074/mkdocstrings_python-1.16.12-py3-none-any.whl", hash = "sha256:22ded3a63b3d823d57457a70ff9860d5a4de9e8b1e482876fc9baabaf6f5f374", size = 124287, upload-time = "2025-06-03T12:52:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" }, ] [[package]] @@ -1908,7 +1905,7 @@ name = "openai-agents" version = "0.13.1" source = { editable = "." } dependencies = [ - { name = "griffe" }, + { name = "griffelib" }, { name = "mcp" }, { name = "openai" }, { name = "pydantic" }, @@ -1990,7 +1987,7 @@ requires-dist = [ { name = "cryptography", marker = "extra == 'encrypt'", specifier = ">=45.0,<46" }, { name = "dapr", marker = "extra == 'dapr'", specifier = ">=1.16.0" }, { name = "graphviz", marker = "extra == 'viz'", specifier = ">=0.17" }, - { name = "griffe", specifier = ">=1.5.6,<2" }, + { name = "griffelib", specifier = ">=2,<3" }, { name = "grpcio", marker = "extra == 'dapr'", specifier = ">=1.60.0" }, { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.81.0,<=1.82.6" }, { name = "mcp", marker = "python_full_version >= '3.10'", specifier = ">=1.19.0,<2" }, From dddbce144e596b1a837348939f36dab6aa75a429 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 08:54:52 +0900 Subject: [PATCH 16/40] Release 0.13.2 (#2774) --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ad0a22a99d..37edaf05b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openai-agents" -version = "0.13.1" +version = "0.13.2" description = "OpenAI Agents SDK" readme = "README.md" requires-python = ">=3.10" diff --git a/uv.lock b/uv.lock index 88d2363567..b718f5de26 100644 --- a/uv.lock +++ b/uv.lock @@ -1902,7 +1902,7 @@ wheels = [ [[package]] name = "openai-agents" -version = "0.13.1" +version = "0.13.2" source = { editable = "." } dependencies = [ { name = "griffelib" }, From 149f5ce6318d7401681eca725c6af69f5fc61246 Mon Sep 17 00:00:00 2001 From: Muttaqi110 <140283743+Muttaqi110@users.noreply.github.com> Date: Fri, 27 Mar 2026 12:41:03 +0500 Subject: [PATCH 17/40] docs: add non-OpenAI provider code example (#2792) --- docs/models/index.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/models/index.md b/docs/models/index.md index fa818611d1..021da1fb24 100644 --- a/docs/models/index.md +++ b/docs/models/index.md @@ -199,6 +199,17 @@ You can integrate other LLM providers with these built-in paths: In cases where you do not have an API key from `platform.openai.com`, we recommend disabling tracing via `set_tracing_disabled()`, or setting up a [different tracing processor](../tracing.md). +``` python +from agents import Agent, AsyncOpenAI, OpenAIChatCompletionsModel, set_tracing_disabled + +set_tracing_disabled(disabled=True) + +provider = AsyncOpenAI(api_key="Api_Key", base_url="Base URL of Provider") +model = OpenAIChatCompletionsModel(model="Model_Name", openai_client=provider) + +agent= Agent(name="Helping Agent", instructions="You are a Helping Agent", model=model) +``` + !!! note In these examples, we use the Chat Completions API/model, because many LLM providers still do not support the Responses API. If your LLM provider does support it, we recommend using Responses. From a4db3e5519d3b3d5146ace202472734737222845 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 16:56:48 +0900 Subject: [PATCH 18/40] docs: update translated document pages (#2793) --- docs/ja/models/index.md | 261 +++++++++++++++++++++------------------- docs/ko/models/index.md | 239 ++++++++++++++++++------------------ docs/zh/models/index.md | 215 +++++++++++++++++---------------- 3 files changed, 374 insertions(+), 341 deletions(-) diff --git a/docs/ja/models/index.md b/docs/ja/models/index.md index 4e8a239deb..981e501dd8 100644 --- a/docs/ja/models/index.md +++ b/docs/ja/models/index.md @@ -4,42 +4,42 @@ search: --- # モデル -Agents SDK には、 OpenAI モデル向けの即時利用可能なサポートが 2 つの形で用意されています。 +Agents SDK には、OpenAI モデルをすぐに使える 2 つの方式のサポートがあります。 -- **推奨**: [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel]。新しい [Responses API](https://platform.openai.com/docs/api-reference/responses) を使用して OpenAI API を呼び出します。 -- [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel]。 [Chat Completions API](https://platform.openai.com/docs/api-reference/chat) を使用して OpenAI API を呼び出します。 +- **推奨**: [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel]。新しい [Responses API](https://platform.openai.com/docs/api-reference/responses) を使って OpenAI API を呼び出します。 +- [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel]。 [Chat Completions API](https://platform.openai.com/docs/api-reference/chat) を使って OpenAI API を呼び出します。 ## モデル設定の選択 -まずは、構成に合う最もシンプルな方法から始めてください。 +ご利用の構成に合う最もシンプルな方法から始めてください。 | 次のことをしたい場合 | 推奨パス | 詳細 | | --- | --- | --- | -| OpenAI モデルのみを使用する | 既定の OpenAI プロバイダーと Responses モデルパスを使用する | [OpenAI モデル](#openai-models) | -| websocket トランスポートで OpenAI Responses API を使用する | Responses モデルパスを維持し、 websocket トランスポートを有効化する | [Responses WebSocket トランスポート](#responses-websocket-transport) | -| 1 つの非 OpenAI プロバイダーを使用する | 組み込みのプロバイダー統合ポイントから始める | [非 OpenAI モデル](#non-openai-models) | -| エージェント間でモデルまたはプロバイダーを混在させる | 実行ごとまたはエージェントごとにプロバイダーを選択し、機能差を確認する | [1 つのワークフローでのモデル混在](#mixing-models-in-one-workflow) と [プロバイダー間でのモデル混在](#mixing-models-across-providers) | -| OpenAI Responses の高度なリクエスト設定を調整する | OpenAI Responses パスで `ModelSettings` を使用する | [高度な OpenAI Responses 設定](#advanced-openai-responses-settings) | -| 非 OpenAI または混在プロバイダーのルーティングにサードパーティーアダプターを使う | サポートされているベータアダプターを比較し、出荷予定のプロバイダーパスを検証する | [サードパーティーアダプター](#third-party-adapters) | +| OpenAI モデルのみを使用する | デフォルトの OpenAI provider と Responses モデルパスを使用する | [OpenAI モデル](#openai-models) | +| websocket トランスポート経由で OpenAI Responses API を使用する | Responses モデルパスを維持し、 websocket トランスポートを有効化する | [Responses WebSocket トランスポート](#responses-websocket-transport) | +| 1 つの非 OpenAI provider を使用する | 組み込みの provider 統合ポイントから始める | [非 OpenAI モデル](#non-openai-models) | +| エージェント間でモデルや provider を混在させる | 実行単位またはエージェント単位で provider を選択し、機能差を確認する | [1 つのワークフローでのモデル混在](#mixing-models-in-one-workflow) と [provider 間でのモデル混在](#mixing-models-across-providers) | +| OpenAI Responses の高度なリクエスト設定を調整する | OpenAI Responses パスで `ModelSettings` を使用する | [OpenAI Responses の高度な設定](#advanced-openai-responses-settings) | +| 非 OpenAI または mixed-provider ルーティング向けにサードパーティアダプターを使う | 対応する beta アダプターを比較し、リリース予定の provider パスを検証する | [サードパーティアダプター](#third-party-adapters) | ## OpenAI モデル -ほとんどの OpenAI 専用アプリでは、既定の OpenAI プロバイダーで文字列のモデル名を使い、 Responses モデルパスを使い続けるのが推奨です。 +多くの OpenAI 専用アプリでは、推奨パスはデフォルトの OpenAI provider と文字列のモデル名を使い、 Responses モデルパスを維持することです。 -`Agent` 初期化時にモデルを指定しない場合は、既定モデルが使われます。現在の既定は互換性と低レイテンシのため [`gpt-4.1`](https://developers.openai.com/api/docs/models/gpt-4.1) です。利用可能な場合は、明示的な `model_settings` を維持したまま、より高品質な [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4) にエージェントを設定することを推奨します。 +`Agent` 初期化時にモデルを指定しない場合は、デフォルトモデルが使われます。現在のデフォルトは、互換性と低レイテンシのため [`gpt-4.1`](https://developers.openai.com/api/docs/models/gpt-4.1) です。利用可能であれば、明示的な `model_settings` を維持したまま、より高品質な [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4) の設定を推奨します。 -[`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4) のような他モデルへ切り替えるには、エージェントの設定方法が 2 つあります。 +[`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4) のような他モデルに切り替えるには、エージェントの設定方法が 2 つあります。 -### 既定モデル +### デフォルトモデル -1 つ目として、カスタムモデルを設定しないすべてのエージェントで特定モデルを一貫して使いたい場合は、エージェント実行前に環境変数 `OPENAI_DEFAULT_MODEL` を設定します。 +まず、カスタムモデルを設定しないすべてのエージェントで一貫して特定モデルを使いたい場合は、エージェント実行前に `OPENAI_DEFAULT_MODEL` 環境変数を設定します。 ```bash export OPENAI_DEFAULT_MODEL=gpt-5.4 python3 my_awesome_agent.py ``` -2 つ目として、 `RunConfig` で実行単位の既定モデルを設定できます。エージェントにモデルを設定しなければ、この実行のモデルが使われます。 +次に、 `RunConfig` で実行単位のデフォルトモデルを設定できます。エージェントにモデルを設定しない場合、この実行のモデルが使われます。 ```python from agents import Agent, RunConfig, Runner @@ -58,7 +58,7 @@ result = await Runner.run( #### GPT-5 モデル -この方法で [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4) などの GPT-5 モデルを使う場合、 SDK は既定の `ModelSettings` を適用します。これは多くのユースケースで最適に動く設定です。既定モデルの推論 effort を調整するには、独自の `ModelSettings` を渡してください。 +この方法で [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4) などの GPT-5 モデルを使うと、 SDK はデフォルトの `ModelSettings` を適用します。多くの用途で最適に動作する設定が使われます。デフォルトモデルの reasoning effort を調整するには、独自の `ModelSettings` を渡してください。 ```python from openai.types.shared import Reasoning @@ -74,21 +74,21 @@ my_agent = Agent( ) ``` -低レイテンシには、 `gpt-5.4` で `reasoning.effort="none"` を使うことを推奨します。 gpt-4.1 ファミリー( mini と nano を含む)も、対話型エージェントアプリ構築において引き続き有力な選択肢です。 +低レイテンシが必要な場合は、 `gpt-5.4` で `reasoning.effort="none"` を使うことを推奨します。 gpt-4.1 系列( mini と nano を含む)も、対話型エージェントアプリ構築の有力な選択肢です。 #### ComputerTool モデル選択 エージェントに [`ComputerTool`][agents.tool.ComputerTool] が含まれる場合、実際の Responses リクエストで有効なモデルによって、 SDK が送信するコンピュータツール payload が決まります。明示的な `gpt-5.4` リクエストでは GA の組み込み `computer` ツールを使い、明示的な `computer-use-preview` リクエストでは従来の `computer_use_preview` payload を維持します。 -主な例外はプロンプト管理呼び出しです。プロンプトテンプレートがモデルを保持し、 SDK がリクエストから `model` を省略する場合、 SDK はプロンプトが固定するモデルを推測しないため、 preview 互換のコンピュータ payload を既定で使います。このフローで GA パスを維持するには、リクエストで `model="gpt-5.4"` を明示するか、 `ModelSettings(tool_choice="computer")` または `ModelSettings(tool_choice="computer_use")` で GA セレクターを強制してください。 +主な例外は prompt 管理の呼び出しです。 prompt template がモデルを所有し、 SDK がリクエストから `model` を省略する場合、 SDK は prompt が固定するモデルを推測しないよう、 preview 互換のコンピュータ payload を既定で使います。このフローで GA パスを維持するには、リクエストで `model="gpt-5.4"` を明示するか、 `ModelSettings(tool_choice="computer")` または `ModelSettings(tool_choice="computer_use")` で GA セレクターを強制してください。 -[`ComputerTool`][agents.tool.ComputerTool] が登録されている場合、 `tool_choice="computer"` 、 `"computer_use"` 、 `"computer_use_preview"` は、有効なリクエストモデルに一致する組み込みセレクターに正規化されます。 `ComputerTool` が登録されていない場合、これらの文字列は通常の関数名として動作し続けます。 +[`ComputerTool`][agents.tool.ComputerTool] が登録されている場合、 `tool_choice="computer"` 、 `"computer_use"` 、 `"computer_use_preview"` は、有効なリクエストモデルに一致する組み込みセレクターへ正規化されます。 `ComputerTool` が登録されていない場合、これらの文字列は通常の関数名として動作し続けます。 -preview 互換リクエストでは、 `environment` と表示寸法を先にシリアライズする必要があるため、 [`ComputerProvider`][agents.tool.ComputerProvider] ファクトリーを使うプロンプト管理フローでは、具体的な `Computer` または `AsyncComputer` インスタンスを渡すか、リクエスト送信前に GA セレクターを強制する必要があります。移行の詳細は [Tools](../tools.md#computertool-and-the-responses-computer-tool) を参照してください。 +preview 互換リクエストでは `environment` と表示サイズを事前にシリアライズする必要があるため、 [`ComputerProvider`][agents.tool.ComputerProvider] ファクトリーを使う prompt 管理フローでは、具体的な `Computer` または `AsyncComputer` インスタンスを渡すか、リクエスト送信前に GA セレクターを強制してください。移行の詳細は [Tools](../tools.md#computertool-and-the-responses-computer-tool) を参照してください。 #### 非 GPT-5 モデル -カスタム `model_settings` なしで非 GPT-5 モデル名を渡すと、 SDK は任意モデル互換の汎用 `ModelSettings` に戻ります。 +非 GPT-5 のモデル名をカスタム `model_settings` なしで渡すと、 SDK は任意モデル互換の汎用 `ModelSettings` に戻ります。 ### Responses 専用ツール検索機能 @@ -96,13 +96,13 @@ preview 互換リクエストでは、 `environment` と表示寸法を先にシ - [`ToolSearchTool`][agents.tool.ToolSearchTool] - [`tool_namespace()`][agents.tool.tool_namespace] -- `@function_tool(defer_loading=True)` と、その他の遅延読み込み Responses ツールサーフェス +- `@function_tool(defer_loading=True)` と、その他の遅延ロード Responses ツール surface -これらの機能は Chat Completions モデルおよび非 Responses バックエンドでは拒否されます。遅延読み込みツールを使う場合は、エージェントに `ToolSearchTool()` を追加し、 namespace 名や遅延専用関数名を強制する代わりに、 `auto` または `required` の tool choice でモデルにツールを読み込ませてください。設定詳細と現在の制約は [Tools](../tools.md#hosted-tool-search) を参照してください。 +これらの機能は Chat Completions モデルおよび非 Responses バックエンドでは拒否されます。遅延ロードツールを使う場合は、エージェントに `ToolSearchTool()` を追加し、 namespace 名や遅延専用関数名を直接強制せず、 `auto` または `required` の tool choice でモデルにツールを読み込ませてください。設定と現在の制約は [Tools](../tools.md#hosted-tool-search) を参照してください。 ### Responses WebSocket トランスポート -既定では、 OpenAI Responses API リクエストは HTTP トランスポートを使用します。 OpenAI バックエンドモデル使用時は websocket トランスポートを有効化できます。 +デフォルトでは、 OpenAI Responses API リクエストは HTTP トランスポートを使います。 OpenAI バックのモデルを使う場合は websocket トランスポートを有効化できます。 #### 基本設定 @@ -112,13 +112,13 @@ from agents import set_default_openai_responses_transport set_default_openai_responses_transport("websocket") ``` -これは、既定の OpenAI プロバイダーで解決される OpenAI Responses モデル( `"gpt-5.4"` のような文字列モデル名を含む)に影響します。 +これは、デフォルト OpenAI provider で解決される OpenAI Responses モデル( `"gpt-5.4"` のような文字列モデル名を含む)に影響します。 -トランスポート選択は、 SDK がモデル名をモデルインスタンスへ解決する際に行われます。具体的な [`Model`][agents.models.interface.Model] オブジェクトを渡す場合、そのトランスポートは既に固定されています。 [`OpenAIResponsesWSModel`][agents.models.openai_responses.OpenAIResponsesWSModel] は websocket、 [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] は HTTP、 [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] は Chat Completions のままです。 `RunConfig(model_provider=...)` を渡した場合、グローバル既定ではなくそのプロバイダーがトランスポート選択を制御します。 +トランスポート選択は、 SDK がモデル名をモデルインスタンスへ解決する際に行われます。具体的な [`Model`][agents.models.interface.Model] オブジェクトを渡した場合、そのトランスポートはすでに固定されています。 [`OpenAIResponsesWSModel`][agents.models.openai_responses.OpenAIResponsesWSModel] は websocket、 [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] は HTTP、 [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] は Chat Completions のままです。 `RunConfig(model_provider=...)` を渡した場合は、その provider がグローバルデフォルトではなくトランスポート選択を制御します。 -#### プロバイダーまたは実行レベル設定 +#### provider 単位または実行単位の設定 -websocket トランスポートはプロバイダー単位または実行単位でも設定できます。 +websocket トランスポートは provider 単位または実行単位でも設定できます。 ```python from agents import Agent, OpenAIProvider, RunConfig, Runner @@ -137,16 +137,16 @@ result = await Runner.run( ) ``` -#### `MultiProvider` を使った高度なルーティング +#### `MultiProvider` による高度なルーティング -プレフィックスベースのモデルルーティングが必要な場合(例: 1 回の実行で `openai/...` と `any-llm/...` を混在)、 [`MultiProvider`][agents.MultiProvider] を使い、そこで `openai_use_responses_websocket=True` を設定します。 +接頭辞ベースのモデルルーティング(例: 1 回の実行で `openai/...` と `any-llm/...` を混在)を行う必要がある場合は、 [`MultiProvider`][agents.MultiProvider] を使い、そこで `openai_use_responses_websocket=True` を設定してください。 -`MultiProvider` は 2 つの従来既定を維持します。 +`MultiProvider` には 2 つの歴史的デフォルトがあります。 -- `openai/...` は OpenAI プロバイダーのエイリアスとして扱われるため、 `openai/gpt-4.1` はモデル `gpt-4.1` としてルーティングされます。 -- 不明なプレフィックスはそのまま渡されず、 `UserError` を発生させます。 +- `openai/...` は OpenAI provider の別名として扱われるため、 `openai/gpt-4.1` はモデル `gpt-4.1` としてルーティングされます。 +- 未知の接頭辞はそのまま渡されず、 `UserError` が発生します。 -OpenAI 互換エンドポイントが名前空間付きモデル ID の文字列そのものを期待する場合は、明示的にパススルー動作を有効化してください。 websocket 有効構成では、 `MultiProvider` 側でも `openai_use_responses_websocket=True` を維持してください。 +OpenAI provider を、リテラルな名前空間付きモデル ID を期待する OpenAI 互換 endpoint に向ける場合は、明示的に pass-through 動作を有効化してください。 websocket 有効構成でも、 `MultiProvider` 上で `openai_use_responses_websocket=True` を維持してください。 ```python from agents import Agent, MultiProvider, RunConfig, Runner @@ -172,52 +172,63 @@ result = await Runner.run( ) ``` -バックエンドが文字列 `openai/...` をそのまま期待する場合は `openai_prefix_mode="model_id"` を使います。バックエンドが `openrouter/openai/gpt-4.1-mini` のような他の名前空間付きモデル ID を期待する場合は `unknown_prefix_mode="model_id"` を使います。これらのオプションは websocket 以外の `MultiProvider` でも利用可能です。この例では本セクションのトランスポート設定の一部として websocket を有効のままにしています。同じオプションは [`responses_websocket_session()`][agents.responses_websocket_session] でも利用できます。 +バックエンドがリテラルの `openai/...` 文字列を期待する場合は `openai_prefix_mode="model_id"` を使います。バックエンドが `openrouter/openai/gpt-4.1-mini` のような他の名前空間付きモデル ID を期待する場合は `unknown_prefix_mode="model_id"` を使います。これらのオプションは websocket トランスポート外の `MultiProvider` でも機能します。この例では本セクションのトランスポート設定の一部として websocket を有効のままにしています。同じオプションは [`responses_websocket_session()`][agents.responses_websocket_session] でも利用できます。 -カスタム OpenAI 互換エンドポイントまたはプロキシを使う場合、 websocket トランスポートには互換 websocket `/responses` エンドポイントも必要です。その構成では `websocket_base_url` を明示設定する必要がある場合があります。 +カスタムの OpenAI 互換 endpoint や proxy を使う場合、 websocket トランスポートには互換 websocket `/responses` endpoint も必要です。これらの構成では `websocket_base_url` を明示設定する必要がある場合があります。 #### 注意事項 -- これは websocket トランスポート上の Responses API であり、 [Realtime API](../realtime/guide.md) ではありません。 Chat Completions や、 Responses websocket `/responses` エンドポイントをサポートしない非 OpenAI プロバイダーには適用されません。 -- 環境で未導入の場合は `websockets` パッケージをインストールしてください。 -- websocket トランスポート有効化後は [`Runner.run_streamed()`][agents.run.Runner.run_streamed] を直接使用できます。同じ websocket 接続をターン間(ネストした agent-as-tool 呼び出しを含む)で再利用したいマルチターンワークフローでは、 [`responses_websocket_session()`][agents.responses_websocket_session] ヘルパーを推奨します。[Running agents](../running_agents.md) ガイドおよび [`examples/basic/stream_ws.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/stream_ws.py) を参照してください。 +- これは websocket トランスポート上の Responses API であり、 [Realtime API](../realtime/guide.md) ではありません。 Chat Completions や非 OpenAI provider には、 Responses websocket `/responses` endpoint をサポートしない限り適用されません。 +- 環境に未導入の場合は `websockets` パッケージをインストールしてください。 +- websocket トランスポート有効化後は [`Runner.run_streamed()`][agents.run.Runner.run_streamed] を直接使えます。複数ターンのワークフローで、ターン間(およびネストされた agent-as-tool 呼び出し間)で同じ websocket 接続を再利用したい場合は、 [`responses_websocket_session()`][agents.responses_websocket_session] ヘルパーを推奨します。[Running agents](../running_agents.md) ガイドと [`examples/basic/stream_ws.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/stream_ws.py) を参照してください。 ## 非 OpenAI モデル -非 OpenAI プロバイダーが必要な場合は、 SDK の組み込みプロバイダー統合ポイントから始めてください。多くの構成では、サードパーティーアダプターを追加せずに十分対応できます。各パターンの例は [examples/model_providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/) にあります。 +非 OpenAI provider が必要な場合は、 SDK の組み込み provider 統合ポイントから始めてください。多くの構成では、サードパーティアダプターを追加しなくても十分です。各パターンの例は [examples/model_providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/) にあります。 -### 非 OpenAI プロバイダー統合方法 +### 非 OpenAI provider の統合方法 | アプローチ | 使用する場面 | スコープ | | --- | --- | --- | -| [`set_default_openai_client`][agents.set_default_openai_client] | 1 つの OpenAI 互換エンドポイントをほとんどまたはすべてのエージェントの既定にしたい | グローバル既定 | -| [`ModelProvider`][agents.models.interface.ModelProvider] | 1 つのカスタムプロバイダーを単一実行に適用したい | 実行単位 | -| [`Agent.model`][agents.agent.Agent.model] | 異なるエージェントに異なるプロバイダーまたは具体的モデルオブジェクトが必要 | エージェント単位 | -| サードパーティーアダプター | 組み込みパスで提供されない、アダプター管理のプロバイダー対応またはルーティングが必要 | [サードパーティーアダプター](#third-party-adapters) を参照 | +| [`set_default_openai_client`][agents.set_default_openai_client] | 1 つの OpenAI 互換 endpoint をほとんどまたはすべてのエージェントのデフォルトにしたい | グローバルデフォルト | +| [`ModelProvider`][agents.models.interface.ModelProvider] | 1 つのカスタム provider を 1 回の実行に適用したい | 実行単位 | +| [`Agent.model`][agents.agent.Agent.model] | 異なるエージェントで異なる provider または具体的なモデルオブジェクトが必要 | エージェント単位 | +| サードパーティアダプター | 組み込みパスでは提供されない、アダプター管理の provider カバレッジやルーティングが必要 | [サードパーティアダプター](#third-party-adapters) を参照 | -これらの組み込みパスで他の LLM プロバイダーを統合できます。 +これらの組み込みパスで他の LLM provider を統合できます。 -1. [`set_default_openai_client`][agents.set_default_openai_client] は、 `AsyncOpenAI` インスタンスを LLM クライアントとしてグローバルに使用したい場合に有用です。これは、 LLM プロバイダーが OpenAI 互換 API エンドポイントを持ち、 `base_url` と `api_key` を設定できるケース向けです。設定可能な例は [examples/model_providers/custom_example_global.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_global.py) を参照してください。 -2. [`ModelProvider`][agents.models.interface.ModelProvider] は `Runner.run` レベルです。これにより「この実行のすべてのエージェントでカスタムモデルプロバイダーを使用する」と指定できます。設定可能な例は [examples/model_providers/custom_example_provider.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_provider.py) を参照してください。 -3. [`Agent.model`][agents.agent.Agent.model] は特定の Agent インスタンスでモデルを指定できます。これにより、異なるエージェントに対して異なるプロバイダーを組み合わせられます。設定可能な例は [examples/model_providers/custom_example_agent.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_agent.py) を参照してください。 +1. [`set_default_openai_client`][agents.set_default_openai_client] は、 `AsyncOpenAI` のインスタンスを LLM クライアントとしてグローバルに使いたい場合に有用です。これは、 LLM provider が OpenAI 互換 API endpoint を持ち、 `base_url` と `api_key` を設定できるケース向けです。設定可能な例は [examples/model_providers/custom_example_global.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_global.py) を参照してください。 +2. [`ModelProvider`][agents.models.interface.ModelProvider] は `Runner.run` レベルです。これにより「この実行の全エージェントでカスタムモデル provider を使う」と指定できます。設定可能な例は [examples/model_providers/custom_example_provider.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_provider.py) を参照してください。 +3. [`Agent.model`][agents.agent.Agent.model] は特定の Agent インスタンスでモデルを指定できます。これにより、異なるエージェントで異なる provider を組み合わせられます。設定可能な例は [examples/model_providers/custom_example_agent.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_agent.py) を参照してください。 -`platform.openai.com` の API キーがない場合は、 `set_tracing_disabled()` でトレーシングを無効化するか、 [別のトレーシングプロセッサー](../tracing.md) の設定を推奨します。 +`platform.openai.com` の API キーを持っていない場合は、 `set_tracing_disabled()` でトレーシングを無効化するか、 [別のトレーシングプロセッサー](../tracing.md) の設定を推奨します。 + +``` python +from agents import Agent, AsyncOpenAI, OpenAIChatCompletionsModel, set_tracing_disabled + +set_tracing_disabled(disabled=True) + +provider = AsyncOpenAI(api_key="Api_Key", base_url="Base URL of Provider") +model = OpenAIChatCompletionsModel(model="Model_Name", openai_client=provider) + +agent= Agent(name="Helping Agent", instructions="You are a Helping Agent", model=model) +``` !!! note - これらの例では、多くの LLM プロバイダーがまだ Responses API をサポートしていないため、 Chat Completions API / モデルを使用しています。お使いの LLM プロバイダーが対応している場合は、 Responses の使用を推奨します。 + これらの例では、多くの LLM provider がまだ Responses API をサポートしていないため、 Chat Completions API / model を使っています。 LLM provider がサポートしている場合は、 Responses の使用を推奨します。 ## 1 つのワークフローでのモデル混在 -1 つのワークフロー内で、エージェントごとに異なるモデルを使いたい場合があります。たとえば、トリアージには小さく高速なモデルを使い、複雑なタスクにはより大規模で高機能なモデルを使えます。[`Agent`][agents.Agent] を設定する際、特定モデルは次のいずれかで選択できます。 +1 つのワークフロー内で、エージェントごとに異なるモデルを使いたい場合があります。たとえば、トリアージには小さく高速なモデルを使い、複雑なタスクには大きく高性能なモデルを使う、といった構成です。[`Agent`][agents.Agent] を設定する際は、次のいずれかで特定モデルを選択できます。 1. モデル名を渡す。 -2. 任意のモデル名 + その名前を Model インスタンスにマップ可能な [`ModelProvider`][agents.models.interface.ModelProvider] を渡す。 -3. [`Model`][agents.models.interface.Model] 実装を直接提供する。 +2. 任意のモデル名 + その名前を Model インスタンスへマッピングできる [`ModelProvider`][agents.models.interface.ModelProvider] を渡す。 +3. [`Model`][agents.models.interface.Model] 実装を直接渡す。 !!! note - SDK は [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] と [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] の両方の形をサポートしますが、 2 つは対応機能とツールが異なるため、ワークフローごとに 1 つのモデル形に統一することを推奨します。ワークフローでモデル形の混在が必要な場合は、使用するすべての機能が両方で利用可能であることを確認してください。 + SDK は [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] と [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] の両方をサポートしますが、両者は対応機能やツールが異なるため、ワークフローごとに 1 つのモデル形状を使うことを推奨します。モデル形状を混在させる必要がある場合は、利用するすべての機能が両方で利用可能であることを確認してください。 ```python from agents import Agent, Runner, AsyncOpenAI, OpenAIChatCompletionsModel @@ -253,7 +264,7 @@ async def main(): 1. OpenAI モデル名を直接設定します。 2. [`Model`][agents.models.interface.Model] 実装を提供します。 -エージェントで使うモデルをさらに設定したい場合は、 temperature などの任意パラメーターを提供する [`ModelSettings`][agents.models.interface.ModelSettings] を渡せます。 +エージェントで使うモデルをさらに設定したい場合は、温度などの任意設定パラメーターを提供する [`ModelSettings`][agents.models.interface.ModelSettings] を渡せます。 ```python from agents import Agent, ModelSettings @@ -266,21 +277,21 @@ english_agent = Agent( ) ``` -## 高度な OpenAI Responses 設定 +## OpenAI Responses の高度な設定 -OpenAI Responses パスでより細かな制御が必要な場合は、まず `ModelSettings` を使用してください。 +OpenAI Responses パスでより細かい制御が必要な場合は、まず `ModelSettings` を使ってください。 -### 一般的な高度 `ModelSettings` オプション +### よく使う高度な `ModelSettings` オプション -OpenAI Responses API 使用時は、いくつかのリクエストフィールドに対応する `ModelSettings` フィールドが既にあるため、それらには `extra_args` は不要です。 +OpenAI Responses API を使う場合、いくつかのリクエストフィールドにはすでに直接対応する `ModelSettings` フィールドがあるため、それらに `extra_args` は不要です。 -- `parallel_tool_calls`: 同一ターンでの複数ツール呼び出しを許可または禁止します。 -- `truncation`: コンテキストあふれ時に失敗する代わりに、 Responses API に最古の会話項目を削除させるには `"auto"` を設定します。 -- `store`: 生成された応答を後で取得できるようサーバー側に保存するかを制御します。これは、 response ID に依存するフォローアップワークフローや、 `store=False` 時にローカル入力へフォールバックが必要なセッション圧縮フローで重要です。 -- `prompt_cache_retention`: たとえば `"24h"` のように、キャッシュされたプロンプト接頭辞をより長く保持します。 -- `response_include`: `web_search_call.action.sources` 、 `file_search_call.results` 、 `reasoning.encrypted_content` など、より豊富な応答 payload を要求します。 -- `top_logprobs`: 出力テキストの上位トークン logprobs を要求します。 SDK は `message.output_text.logprobs` も自動追加します。 -- `retry`: モデル呼び出しにランナー管理リトライ設定を有効化します。[ランナー管理リトライ](#runner-managed-retries) を参照してください。 +- `parallel_tool_calls`: 同一ターンで複数のツール呼び出しを許可または禁止します。 +- `truncation`: `"auto"` を設定すると、コンテキスト超過時に失敗せず、 Responses API が最も古い会話項目を削除します。 +- `store`: 生成レスポンスを後で取得できるようサーバー側に保存するかを制御します。これは response ID に依存するフォローアップワークフローや、 `store=False` 時にローカル入力へフォールバックする可能性があるセッション圧縮フローで重要です。 +- `prompt_cache_retention`: たとえば `"24h"` のように、キャッシュ済み prompt prefix をより長く保持します。 +- `response_include`: `web_search_call.action.sources` 、 `file_search_call.results` 、 `reasoning.encrypted_content` など、よりリッチな response payload を要求します。 +- `top_logprobs`: 出力テキストの top-token logprobs を要求します。 SDK は `message.output_text.logprobs` も自動追加します。 +- `retry`: モデル呼び出しに対する runner 管理のリトライ設定を有効化します。 [Runner 管理リトライ](#runner-managed-retries) を参照してください。 ```python from agents import Agent, ModelSettings @@ -299,13 +310,13 @@ research_agent = Agent( ) ``` -`store=False` を設定すると、 Responses API はその応答を後でサーバー側取得可能な状態に保持しません。これはステートレスまたはゼロデータ保持スタイルのフローで有用ですが、通常 response ID を再利用する機能は、代わりにローカル管理状態に依存する必要があります。たとえば、 [`OpenAIResponsesCompactionSession`][agents.memory.openai_responses_compaction_session.OpenAIResponsesCompactionSession] は、最後の応答が保存されていない場合、既定の `"auto"` 圧縮パスを入力ベース圧縮に切り替えます。[Sessions ガイド](../sessions/index.md#openai-responses-compaction-sessions) を参照してください。 +`store=False` を設定すると、 Responses API はそのレスポンスを後でサーバー側取得可能な状態で保持しません。これはステートレスまたはゼロデータ保持型フローに有用ですが、通常 response ID を再利用する機能は、代わりにローカル管理状態に依存する必要があります。たとえば、 [`OpenAIResponsesCompactionSession`][agents.memory.openai_responses_compaction_session.OpenAIResponsesCompactionSession] は、最後のレスポンスが保存されていない場合、デフォルトの `"auto"` 圧縮パスを入力ベース圧縮へ切り替えます。[Sessions ガイド](../sessions/index.md#openai-responses-compaction-sessions) を参照してください。 ### `extra_args` の受け渡し -SDK がまだトップレベルで直接公開していない、プロバイダー固有または新しいリクエストフィールドが必要な場合は `extra_args` を使います。 +SDK がまだトップレベルで直接公開していない provider 固有または新しいリクエストフィールドが必要な場合は、 `extra_args` を使ってください。 -また OpenAI の Responses API 使用時には、[その他の任意パラメーター](https://platform.openai.com/docs/api-reference/responses/create)(例: `user` 、 `service_tier` など)があります。トップレベルにない場合、これらも `extra_args` で渡せます。 +また OpenAI の Responses API では、[ほかにも任意パラメーター](https://platform.openai.com/docs/api-reference/responses/create)(例: `user` 、 `service_tier` など)があります。トップレベルで利用できない場合は、これらも `extra_args` で渡せます。 ```python from agents import Agent, ModelSettings @@ -321,9 +332,9 @@ english_agent = Agent( ) ``` -## ランナー管理リトライ +## Runner 管理リトライ -リトライは実行時専用で、明示的に有効化する必要があります。 SDK は `ModelSettings(retry=...)` を設定し、かつリトライポリシーがリトライを選択した場合を除き、一般的なモデルリクエストをリトライしません。 +リトライは実行時専用で、明示的な有効化が必要です。 SDK は、 `ModelSettings(retry=...)` を設定し、かつリトライポリシーがリトライを選択しない限り、一般的なモデルリクエストをリトライしません。 ```python from agents import Agent, ModelRetrySettings, ModelSettings, retry_policies @@ -355,81 +366,81 @@ agent = Agent(
-| フィールド | 型 | 注記 | +| フィールド | 型 | 備考 | | --- | --- | --- | -| `max_retries` | `int | None` | 初回リクエスト後に許可されるリトライ試行回数。 | -| `backoff` | `ModelRetryBackoffSettings | dict | None` | ポリシーが明示遅延を返さずにリトライする場合の既定遅延戦略。 | +| `max_retries` | `int | None` | 初回リクエスト後に許可されるリトライ回数。 | +| `backoff` | `ModelRetryBackoffSettings | dict | None` | ポリシーが明示的な遅延を返さずにリトライする際のデフォルト遅延戦略。 | | `policy` | `RetryPolicy | None` | リトライ可否を判断するコールバック。このフィールドは実行時専用でシリアライズされません。 |
-リトライポリシーは、 [`RetryPolicyContext`][agents.retry.RetryPolicyContext] を受け取ります。内容は次のとおりです。 +リトライポリシーは [`RetryPolicyContext`][agents.retry.RetryPolicyContext] を受け取ります。内容は次の通りです。 -- `attempt` と `max_retries` (試行回数を考慮した判断用) -- `stream` (ストリーミング / 非ストリーミング分岐用) -- `error` (raw 検査用) -- `normalized` 情報( `status_code` 、 `retry_after` 、 `error_code` 、 `is_network_error` 、 `is_timeout` 、 `is_abort` など) -- 基盤モデルアダプターがリトライ指針を提供できる場合の `provider_advice` +- `attempt` と `max_retries`(試行回数に応じた判断用) +- `stream`(ストリーミング / 非ストリーミングの分岐用) +- `error`( raw 検査用) +- `status_code` 、 `retry_after` 、 `error_code` 、 `is_network_error` 、 `is_timeout` 、 `is_abort` などの `normalized` 情報 +- 基盤モデルアダプターがリトライ指針を返せる場合の `provider_advice` -ポリシーは次のいずれかを返せます。 +ポリシーの戻り値は次のいずれかです。 -- 単純なリトライ判断として `True` / `False` -- 遅延上書きや診断理由付与をしたい場合の [`RetryDecision`][agents.retry.RetryDecision] +- 単純なリトライ判定の `True` / `False` +- 遅延上書きや診断理由の付与を行いたい場合の [`RetryDecision`][agents.retry.RetryDecision] -SDK は `retry_policies` に既製ヘルパーを提供しています。 +SDK は `retry_policies` に既製ヘルパーを用意しています。 | ヘルパー | 動作 | | --- | --- | -| `retry_policies.never()` | 常に無効化します。 | -| `retry_policies.provider_suggested()` | 利用可能な場合、プロバイダーのリトライ助言に従います。 | -| `retry_policies.network_error()` | 一時的なトランスポート / タイムアウト失敗に一致します。 | -| `retry_policies.http_status([...])` | 指定した HTTP ステータスコードに一致します。 | -| `retry_policies.retry_after()` | retry-after ヒントがある場合のみ、その遅延でリトライします。 | -| `retry_policies.any(...)` | ネストしたポリシーのいずれかが有効化したらリトライします。 | -| `retry_policies.all(...)` | ネストしたポリシーのすべてが有効化した場合のみリトライします。 | +| `retry_policies.never()` | 常に無効化。 | +| `retry_policies.provider_suggested()` | 利用可能な場合、 provider のリトライ助言に従う。 | +| `retry_policies.network_error()` | 一時的なトランスポート障害とタイムアウトに一致。 | +| `retry_policies.http_status([...])` | 指定した HTTP ステータスコードに一致。 | +| `retry_policies.retry_after()` | retry-after ヒントがある場合のみリトライし、その遅延を使用。 | +| `retry_policies.any(...)` | ネストポリシーのいずれかが有効化した場合にリトライ。 | +| `retry_policies.all(...)` | ネストポリシーのすべてが有効化した場合のみリトライ。 | -ポリシーを組み合わせる場合、 `provider_suggested()` は最も安全な最初の構成要素です。プロバイダーが判別可能な場合、プロバイダー側の拒否や再実行安全性承認を維持できるためです。 +ポリシーを組み合わせる場合、 `provider_suggested()` は最初の構成要素として最も安全です。 provider が識別可能な場合に、 provider の拒否やリプレイ安全性承認を維持できるためです。 ##### 安全境界 一部の失敗は自動リトライされません。 -- 中断エラー。 -- プロバイダー助言で再実行が unsafe とされたリクエスト。 -- 出力開始後で再実行が unsafe になるストリーミング実行。 +- Abort エラー。 +- provider 助言でリプレイが unsafe とされるリクエスト。 +- 出力開始後で、リプレイが unsafe になるストリーミング実行。 -`previous_response_id` または `conversation_id` を使う状態保持フォローアップリクエストも、より保守的に扱われます。これらでは `network_error()` や `http_status([500])` のような非プロバイダー述語だけでは不十分です。リトライポリシーには通常 `retry_policies.provider_suggested()` を通じた、プロバイダー由来の replay-safe 承認を含める必要があります。 +`previous_response_id` や `conversation_id` を使う状態保持のフォローアップリクエストも、より保守的に扱われます。これらでは `network_error()` や `http_status([500])` のような非 provider 述語だけでは不十分です。通常は `retry_policies.provider_suggested()` による provider の replay-safe 承認を含める必要があります。 -##### ランナーとエージェントのマージ動作 +##### Runner とエージェントのマージ動作 -`retry` はランナーレベルとエージェントレベルの `ModelSettings` 間でディープマージされます。 +`retry` は runner レベルとエージェントレベルの `ModelSettings` 間で deep-merge されます。 -- エージェントは `retry.max_retries` のみ上書きし、ランナーの `policy` を継承できます。 -- エージェントは `retry.backoff` の一部のみ上書きし、兄弟 backoff フィールドをランナーから維持できます。 -- `policy` は実行時専用のため、シリアライズされた `ModelSettings` では `max_retries` と `backoff` は保持されますが、コールバック自体は除外されます。 +- エージェントは `retry.max_retries` だけを上書きし、 runner の `policy` を継承できます。 +- エージェントは `retry.backoff` の一部だけを上書きし、残りの backoff フィールドを runner から維持できます。 +- `policy` は実行時専用のため、シリアライズされた `ModelSettings` には `max_retries` と `backoff` は残りますが、コールバック自体は含まれません。 -より完全な例は [`examples/basic/retry.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry.py) と [adapter-backed retry example](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry_litellm.py) を参照してください。 +より完全な例は [`examples/basic/retry.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry.py) と [adapter-backed retry 例](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry_litellm.py) を参照してください。 -## 非 OpenAI プロバイダーのトラブルシューティング +## 非 OpenAI provider のトラブルシューティング ### トレーシングクライアントエラー 401 -トレーシング関連エラーが出る場合、トレースは OpenAI サーバーへアップロードされる一方で OpenAI API キーがないことが原因です。解決策は 3 つあります。 +トレーシング関連エラーが出る場合、トレースが OpenAI サーバーへアップロードされる一方、 OpenAI API キーがないことが原因です。解決策は 3 つあります。 1. トレーシングを完全に無効化する: [`set_tracing_disabled(True)`][agents.set_tracing_disabled]。 -2. トレーシング用 OpenAI キーを設定する: [`set_tracing_export_api_key(...)`][agents.set_tracing_export_api_key]。この API キーはトレースアップロード専用で、 [platform.openai.com](https://platform.openai.com/) 発行である必要があります。 -3. 非 OpenAI のトレースプロセッサーを使う。[tracing docs](../tracing.md#custom-tracing-processors) を参照してください。 +2. トレーシング用に OpenAI キーを設定する: [`set_tracing_export_api_key(...)`][agents.set_tracing_export_api_key]。この API キーはトレースアップロード専用で、 [platform.openai.com](https://platform.openai.com/) のキーである必要があります。 +3. 非 OpenAI のトレースプロセッサーを使う。 [トレーシングドキュメント](../tracing.md#custom-tracing-processors) を参照してください。 ### Responses API サポート -SDK は既定で Responses API を使いますが、他の多くの LLM プロバイダーはまだ対応していません。その結果として 404 などの問題が発生する場合があります。解決策は 2 つあります。 +SDK はデフォルトで Responses API を使いますが、多くの他 LLM provider はまだ未対応です。その結果、 404 などの問題が発生することがあります。解決策は 2 つあります。 1. [`set_default_openai_api("chat_completions")`][agents.set_default_openai_api] を呼び出す。これは環境変数で `OPENAI_API_KEY` と `OPENAI_BASE_URL` を設定している場合に機能します。 2. [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] を使う。例は [こちら](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/) にあります。 ### structured outputs サポート -一部のモデルプロバイダーは [structured outputs](https://platform.openai.com/docs/guides/structured-outputs) をサポートしていません。この場合、次のようなエラーになることがあります。 +一部モデル provider は [structured outputs](https://platform.openai.com/docs/guides/structured-outputs) をサポートしていません。これにより、次のようなエラーが発生することがあります。 ``` @@ -437,34 +448,34 @@ BadRequestError: Error code: 400 - {'error': {'message': "'response_format.type' ``` -これは一部モデルプロバイダーの制約です。 JSON 出力はサポートしていても、出力に使用する `json_schema` の指定を許可していません。現在修正に取り組んでいますが、 JSON schema 出力をサポートするプロバイダーの利用を推奨します。そうでない場合、不正な JSON によりアプリが頻繁に壊れる可能性があります。 +これは一部モデル provider の制約です。 JSON 出力はサポートしていても、出力に使う `json_schema` を指定できません。この修正に取り組んでいますが、 JSON schema 出力をサポートする provider を使うことを推奨します。そうでない場合、アプリは不正形式 JSON により頻繁に壊れる可能性があります。 -## プロバイダー間でのモデル混在 +## provider 間でのモデル混在 -モデルプロバイダー間の機能差を把握しておく必要があります。そうしないとエラーになる可能性があります。たとえば OpenAI は structured outputs、マルチモーダル入力、ホスト型ファイル検索と Web 検索をサポートしますが、多くの他プロバイダーはこれらをサポートしません。次の制約に注意してください。 +モデル provider 間の機能差を把握しておく必要があります。把握していないとエラーになる可能性があります。たとえば OpenAI は structured outputs、マルチモーダル入力、ホスト型ファイル検索と Web 検索をサポートしますが、多くの他 provider はこれらをサポートしません。次の制限に注意してください。 -- 非対応プロバイダーへ、対応していない `tools` を送らない -- テキスト専用モデル呼び出し前にマルチモーダル入力を除外する -- structured JSON 出力非対応プロバイダーは無効 JSON を時々生成する可能性があることを理解する +- 未対応の `tools` を、理解しない provider に送らない +- テキスト専用モデルを呼ぶ前に、マルチモーダル入力を除外する +- structured JSON 出力未対応の provider は、ときどき無効な JSON を生成する可能性があることを理解する -## サードパーティーアダプター +## サードパーティアダプター -SDK の組み込みプロバイダー統合ポイントで不足する場合のみ、サードパーティーアダプターを選択してください。この SDK で OpenAI モデルのみを使用する場合、 Any-LLM や LiteLLM ではなく組み込みの [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] パスを優先してください。サードパーティーアダプターは、 OpenAI モデルと非 OpenAI プロバイダーを組み合わせる必要がある場合、または組み込みパスで提供されないアダプター管理のプロバイダー対応 / ルーティングが必要な場合向けです。アダプターは SDK と上流モデルプロバイダーの間に別の互換レイヤーを追加するため、機能サポートやリクエスト意味論はプロバイダーごとに異なります。 SDK には現在、 Any-LLM と LiteLLM がベストエフォートのベータ統合として含まれています。 +SDK の組み込み provider 統合ポイントで不足する場合にのみ、サードパーティアダプターを使ってください。この SDK で OpenAI モデルのみを使う場合、 Any-LLM や LiteLLM ではなく、組み込み [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] パスを優先してください。サードパーティアダプターは、 OpenAI モデルと非 OpenAI provider を組み合わせる必要がある場合、または組み込みパスで提供されないアダプター管理の provider カバレッジ / ルーティングが必要な場合向けです。アダプターは SDK と上流モデル provider の間に互換レイヤーを 1 つ追加するため、機能サポートやリクエストセマンティクスは provider により異なることがあります。 SDK には現在、 Any-LLM と LiteLLM の best-effort な beta 統合が含まれます。 ### Any-LLM -Any-LLM サポートは、 Any-LLM 管理のプロバイダー対応またはルーティングが必要な場合向けに、ベストエフォートのベータとして含まれています。 +Any-LLM サポートは、 Any-LLM 管理の provider カバレッジやルーティングが必要な場合向けに、 best-effort な beta として提供されています。 -上流プロバイダーパスに応じて、 Any-LLM は Responses API、 Chat Completions 互換 API、またはプロバイダー固有互換レイヤーを使う場合があります。 +上流 provider パスに応じて、 Any-LLM は Responses API、 Chat Completions 互換 API、または provider 固有互換レイヤーを使う場合があります。 -Any-LLM が必要な場合は `openai-agents[any-llm]` をインストールし、 [`examples/model_providers/any_llm_auto.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/any_llm_auto.py) または [`examples/model_providers/any_llm_provider.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/any_llm_provider.py) から始めてください。[`MultiProvider`][agents.MultiProvider] で `any-llm/...` モデル名を使うか、 `AnyLLMModel` を直接インスタンス化するか、実行スコープで `AnyLLMProvider` を使えます。モデルサーフェスを明示固定したい場合は、 `AnyLLMModel` 構築時に `api="responses"` または `api="chat_completions"` を渡してください。 +Any-LLM が必要な場合は `openai-agents[any-llm]` をインストールし、 [`examples/model_providers/any_llm_auto.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/any_llm_auto.py) または [`examples/model_providers/any_llm_provider.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/any_llm_provider.py) から始めてください。 [`MultiProvider`][agents.MultiProvider] で `any-llm/...` モデル名を使う、 `AnyLLMModel` を直接生成する、または実行スコープで `AnyLLMProvider` を使うことができます。モデル surface を明示的に固定したい場合は、 `AnyLLMModel` 構築時に `api="responses"` または `api="chat_completions"` を渡してください。 -Any-LLM はサードパーティーアダプターレイヤーであるため、プロバイダー依存関係と機能差分は SDK ではなく Any-LLM 側で定義されます。使用量メトリクスは上流プロバイダーが返す場合に自動伝播されますが、ストリーミング Chat Completions バックエンドでは usage チャンク出力前に `ModelSettings(include_usage=True)` が必要な場合があります。structured outputs、ツール呼び出し、 usage レポート、 Responses 固有動作に依存する場合は、デプロイ予定の正確なプロバイダーバックエンドを検証してください。 +Any-LLM はサードパーティアダプターレイヤーのままなので、 provider 依存関係や機能ギャップは SDK ではなく Any-LLM 側で定義されます。利用量メトリクスは上流 provider が返す場合に自動伝播されますが、ストリーミング Chat Completions バックエンドでは利用量チャンク出力前に `ModelSettings(include_usage=True)` が必要なことがあります。 structured outputs、 tool 呼び出し、利用量レポート、 Responses 固有動作に依存する場合は、デプロイ予定の provider バックエンドを正確に検証してください。 ### LiteLLM -LiteLLM サポートは、 LiteLLM 固有のプロバイダー対応またはルーティングが必要な場合向けに、ベストエフォートのベータとして含まれています。 +LiteLLM サポートは、 LiteLLM 固有の provider カバレッジやルーティングが必要な場合向けに、 best-effort な beta として提供されています。 -LiteLLM が必要な場合は `openai-agents[litellm]` をインストールし、 [`examples/model_providers/litellm_auto.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/litellm_auto.py) または [`examples/model_providers/litellm_provider.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/litellm_provider.py) から始めてください。 `litellm/...` モデル名を使うか、 [`LitellmModel`][agents.extensions.models.litellm_model.LitellmModel] を直接インスタンス化できます。 +LiteLLM が必要な場合は `openai-agents[litellm]` をインストールし、 [`examples/model_providers/litellm_auto.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/litellm_auto.py) または [`examples/model_providers/litellm_provider.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/litellm_provider.py) から始めてください。 `litellm/...` モデル名を使うか、 [`LitellmModel`][agents.extensions.models.litellm_model.LitellmModel] を直接生成できます。 -一部の LiteLLM バックエンドプロバイダーは既定で SDK usage メトリクスを埋めません。 usage レポートが必要な場合は `ModelSettings(include_usage=True)` を渡し、 structured outputs、ツール呼び出し、 usage レポート、またはアダプター固有ルーティング動作に依存する場合は、デプロイ予定の正確なプロバイダーバックエンドを検証してください。 \ No newline at end of file +一部の LiteLLM バック provider は、デフォルトでは SDK の利用量メトリクスを埋めません。利用量レポートが必要な場合は `ModelSettings(include_usage=True)` を渡し、 structured outputs、 tool 呼び出し、利用量レポート、またはアダプター固有ルーティング動作に依存する場合は、デプロイ予定の provider バックエンドを正確に検証してください。 \ No newline at end of file diff --git a/docs/ko/models/index.md b/docs/ko/models/index.md index 6e932e5bb0..6f04b4aa95 100644 --- a/docs/ko/models/index.md +++ b/docs/ko/models/index.md @@ -4,42 +4,42 @@ search: --- # 모델 -Agents SDK 는 즉시 사용할 수 있는 OpenAI 모델 지원을 두 가지 방식으로 제공합니다: +Agents SDK는 OpenAI 모델을 즉시 사용할 수 있도록 두 가지 방식으로 지원합니다 -- **권장**: 새로운 [Responses API](https://platform.openai.com/docs/api-reference/responses)를 사용해 OpenAI API 를 호출하는 [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] -- [Chat Completions API](https://platform.openai.com/docs/api-reference/chat)를 사용해 OpenAI API 를 호출하는 [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] +- **권장**: 새로운 [Responses API](https://platform.openai.com/docs/api-reference/responses)를 사용해 OpenAI API를 호출하는 [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] +- [Chat Completions API](https://platform.openai.com/docs/api-reference/chat)를 사용해 OpenAI API를 호출하는 [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] ## 모델 설정 선택 -사용 중인 설정에 맞는 가장 단순한 경로부터 시작하세요: +현재 설정에 맞는 가장 간단한 경로부터 시작하세요 -| 다음을 하려는 경우 | 권장 경로 | 더 읽기 | +| 다음을 하려는 경우... | 권장 경로 | 자세히 보기 | | --- | --- | --- | -| OpenAI 모델만 사용 | 기본 OpenAI provider 와 Responses 모델 경로 사용 | [OpenAI 모델](#openai-models) | +| OpenAI 모델만 사용 | 기본 OpenAI provider와 Responses 모델 경로 사용 | [OpenAI 모델](#openai-models) | | websocket 전송으로 OpenAI Responses API 사용 | Responses 모델 경로를 유지하고 websocket 전송 활성화 | [Responses WebSocket 전송](#responses-websocket-transport) | -| OpenAI 가 아닌 provider 하나 사용 | 내장된 provider 통합 지점부터 시작 | [OpenAI 가 아닌 모델](#non-openai-models) | -| 에이전트 간에 모델 또는 provider 혼합 | 실행 단위 또는 에이전트 단위로 provider 선택 후 기능 차이 검토 | [하나의 워크플로에서 모델 혼합](#mixing-models-in-one-workflow) 및 [provider 간 모델 혼합](#mixing-models-across-providers) | +| OpenAI 이외 provider 하나 사용 | 내장 provider 통합 지점부터 시작 | [OpenAI 이외 모델](#non-openai-models) | +| 에이전트 간 모델 또는 provider 혼합 | 실행 단위 또는 에이전트 단위로 provider 선택 후 기능 차이 검토 | [하나의 워크플로에서 모델 혼합](#mixing-models-in-one-workflow) 및 [provider 간 모델 혼합](#mixing-models-across-providers) | | 고급 OpenAI Responses 요청 설정 조정 | OpenAI Responses 경로에서 `ModelSettings` 사용 | [고급 OpenAI Responses 설정](#advanced-openai-responses-settings) | -| OpenAI 가 아닌 경로 또는 혼합 provider 라우팅에 서드파티 adapter 사용 | 지원되는 베타 adapter 비교 후 배포할 provider 경로 검증 | [서드파티 adapter](#third-party-adapters) | +| OpenAI 이외 또는 혼합 provider 라우팅에 서드파티 어댑터 사용 | 지원되는 베타 어댑터를 비교하고 배포 예정 provider 경로 검증 | [서드파티 어댑터](#third-party-adapters) | ## OpenAI 모델 -대부분의 OpenAI 전용 앱에서는 기본 OpenAI provider 와 문자열 모델 이름을 사용하고, Responses 모델 경로를 유지하는 것을 권장합니다 +대부분의 OpenAI 전용 앱에서는 기본 OpenAI provider와 문자열 모델 이름을 사용하고 Responses 모델 경로를 유지하는 것을 권장합니다 -`Agent` 를 초기화할 때 모델을 지정하지 않으면 기본 모델이 사용됩니다. 현재 기본값은 호환성과 낮은 지연 시간을 위해 [`gpt-4.1`](https://developers.openai.com/api/docs/models/gpt-4.1)입니다. 사용 권한이 있다면, 명시적인 `model_settings` 를 유지하면서 더 높은 품질을 위해 에이전트를 [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4)로 설정하는 것을 권장합니다 +`Agent`를 초기화할 때 모델을 지정하지 않으면 기본 모델이 사용됩니다. 현재 기본값은 호환성과 낮은 지연 시간을 위해 [`gpt-4.1`](https://developers.openai.com/api/docs/models/gpt-4.1)입니다. 접근 권한이 있다면, 명시적인 `model_settings`를 유지하면서 더 높은 품질을 위해 에이전트를 [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4)로 설정하는 것을 권장합니다 [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4) 같은 다른 모델로 전환하려면 에이전트를 구성하는 방법이 두 가지 있습니다 ### 기본 모델 -첫째, 사용자 지정 모델을 설정하지 않는 모든 에이전트에서 일관되게 특정 모델을 사용하려면 에이전트를 실행하기 전에 `OPENAI_DEFAULT_MODEL` 환경 변수를 설정하세요 +먼저, 커스텀 모델을 설정하지 않은 모든 에이전트에 특정 모델을 일관되게 사용하려면 에이전트를 실행하기 전에 `OPENAI_DEFAULT_MODEL` 환경 변수를 설정하세요 ```bash export OPENAI_DEFAULT_MODEL=gpt-5.4 python3 my_awesome_agent.py ``` -둘째, `RunConfig` 를 통해 실행 단위 기본 모델을 설정할 수 있습니다. 에이전트에 모델을 설정하지 않으면 이 실행의 모델이 사용됩니다 +둘째, `RunConfig`를 통해 실행 단위 기본 모델을 설정할 수 있습니다. 에이전트에 모델을 설정하지 않으면 이 실행의 모델이 사용됩니다 ```python from agents import Agent, RunConfig, Runner @@ -58,7 +58,7 @@ result = await Runner.run( #### GPT-5 모델 -이 방식으로 [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4) 같은 GPT-5 모델을 사용하면 SDK 가 기본 `ModelSettings` 를 적용합니다. 대부분의 사용 사례에서 가장 잘 동작하는 값이 설정됩니다. 기본 모델의 reasoning effort 를 조정하려면 사용자 정의 `ModelSettings` 를 전달하세요: +이 방식으로 [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4) 같은 GPT-5 모델을 사용할 때 SDK는 기본 `ModelSettings`를 적용합니다. 대부분의 사용 사례에 가장 잘 맞는 값을 설정합니다. 기본 모델의 reasoning effort를 조정하려면 사용자 정의 `ModelSettings`를 전달하세요 ```python from openai.types.shared import Reasoning @@ -74,35 +74,35 @@ my_agent = Agent( ) ``` -더 낮은 지연 시간을 위해 `gpt-5.4` 에서 `reasoning.effort="none"` 사용을 권장합니다. gpt-4.1 계열( mini 및 nano 변형 포함)도 대화형 에이전트 앱 구축에 여전히 좋은 선택입니다 +더 낮은 지연 시간을 위해 `gpt-5.4`와 `reasoning.effort="none"` 조합을 권장합니다. gpt-4.1 계열(미니 및 나노 변형 포함)도 인터랙티브 에이전트 앱 구축에 여전히 좋은 선택입니다 #### ComputerTool 모델 선택 -에이전트에 [`ComputerTool`][agents.tool.ComputerTool] 이 포함된 경우, 실제 Responses 요청에서의 유효 모델이 SDK 가 전송하는 컴퓨터 도구 payload 를 결정합니다. 명시적인 `gpt-5.4` 요청은 GA 내장 `computer` 도구를 사용하고, 명시적인 `computer-use-preview` 요청은 이전 `computer_use_preview` payload 를 유지합니다 +에이전트에 [`ComputerTool`][agents.tool.ComputerTool]이 포함되어 있으면 실제 Responses 요청에서의 유효 모델이 SDK가 전송할 컴퓨터 도구 payload를 결정합니다. 명시적 `gpt-5.4` 요청은 GA 내장 `computer` 도구를 사용하고, 명시적 `computer-use-preview` 요청은 기존 `computer_use_preview` payload를 유지합니다 -주요 예외는 프롬프트 관리 호출입니다. 프롬프트 템플릿이 모델을 소유하고 SDK 가 요청에서 `model` 을 생략하는 경우, SDK 는 프롬프트가 어떤 모델에 고정됐는지 추측하지 않기 위해 preview 호환 컴퓨터 payload 를 기본으로 사용합니다. 이 흐름에서 GA 경로를 유지하려면 요청에 `model="gpt-5.4"` 를 명시하거나 `ModelSettings(tool_choice="computer")` 또는 `ModelSettings(tool_choice="computer_use")` 로 GA 선택자를 강제하세요 +주요 예외는 프롬프트 관리 호출입니다. 프롬프트 템플릿이 모델을 소유하고 SDK가 요청에서 `model`을 생략하면, SDK는 프롬프트가 고정한 모델을 추측하지 않기 위해 preview 호환 컴퓨터 payload를 기본으로 사용합니다. 이 흐름에서 GA 경로를 유지하려면 요청에 `model="gpt-5.4"`를 명시하거나 `ModelSettings(tool_choice="computer")` 또는 `ModelSettings(tool_choice="computer_use")`로 GA selector를 강제하세요 -등록된 [`ComputerTool`][agents.tool.ComputerTool] 이 있는 경우 `tool_choice="computer"`, `"computer_use"`, `"computer_use_preview"` 는 유효 요청 모델과 일치하는 내장 선택자로 정규화됩니다. `ComputerTool` 이 등록되지 않은 경우 이 문자열은 일반 함수 이름처럼 계속 동작합니다 +등록된 [`ComputerTool`][agents.tool.ComputerTool]이 있을 때 `tool_choice="computer"`, `"computer_use"`, `"computer_use_preview"`는 유효 요청 모델에 맞는 내장 selector로 정규화됩니다. `ComputerTool`이 등록되지 않은 경우 이 문자열들은 일반 함수 이름처럼 계속 동작합니다 -preview 호환 요청은 `environment` 와 디스플레이 크기를 미리 직렬화해야 하므로 [`ComputerProvider`][agents.tool.ComputerProvider] 팩토리를 사용하는 프롬프트 관리 흐름에서는 구체적인 `Computer` 또는 `AsyncComputer` 인스턴스를 전달하거나 요청 전송 전에 GA 선택자를 강제해야 합니다. 전체 마이그레이션 세부 사항은 [Tools](../tools.md#computertool-and-the-responses-computer-tool)를 참조하세요 +preview 호환 요청은 `environment`와 디스플레이 크기를 사전에 직렬화해야 하므로, [`ComputerProvider`][agents.tool.ComputerProvider] 팩토리를 사용하는 프롬프트 관리 흐름은 구체적인 `Computer` 또는 `AsyncComputer` 인스턴스를 전달하거나 요청 전송 전에 GA selector를 강제해야 합니다. 전체 마이그레이션 세부 사항은 [Tools](../tools.md#computertool-and-the-responses-computer-tool)를 참고하세요 -#### GPT-5 가 아닌 모델 +#### GPT-5 이외 모델 -사용자 지정 `model_settings` 없이 GPT-5 가 아닌 모델 이름을 전달하면 SDK 는 모든 모델과 호환되는 일반 `ModelSettings` 로 되돌아갑니다 +사용자 지정 `model_settings` 없이 GPT-5 이외 모델 이름을 전달하면 SDK는 모든 모델과 호환되는 일반 `ModelSettings`로 되돌아갑니다 ### Responses 전용 도구 검색 기능 -다음 도구 기능은 OpenAI Responses 모델에서만 지원됩니다: +다음 도구 기능은 OpenAI Responses 모델에서만 지원됩니다 - [`ToolSearchTool`][agents.tool.ToolSearchTool] - [`tool_namespace()`][agents.tool.tool_namespace] - `@function_tool(defer_loading=True)` 및 기타 지연 로딩 Responses 도구 표면 -이 기능들은 Chat Completions 모델 및 Responses 가 아닌 backend 에서 거부됩니다. 지연 로딩 도구를 사용할 때는 에이전트에 `ToolSearchTool()` 을 추가하고, 빈 namespace 이름이나 지연 전용 함수 이름을 강제하는 대신 모델이 `auto` 또는 `required` tool choice 를 통해 도구를 로드하도록 하세요. 설정 세부 사항과 현재 제약은 [Tools](../tools.md#hosted-tool-search)를 참조하세요 +이 기능들은 Chat Completions 모델과 Responses 이외 백엔드에서 거부됩니다. 지연 로딩 도구를 사용할 때는 에이전트에 `ToolSearchTool()`을 추가하고, 네임스페이스 이름 또는 지연 전용 함수 이름을 강제하지 말고 `auto` 또는 `required` tool choice를 통해 모델이 도구를 로드하도록 하세요. 설정 세부 사항과 현재 제약은 [Tools](../tools.md#hosted-tool-search)를 참고하세요 ### Responses WebSocket 전송 -기본적으로 OpenAI Responses API 요청은 HTTP 전송을 사용합니다. OpenAI 기반 모델 사용 시 websocket 전송을 선택적으로 활성화할 수 있습니다 +기본적으로 OpenAI Responses API 요청은 HTTP 전송을 사용합니다. OpenAI 기반 모델을 사용할 때 websocket 전송을 선택적으로 활성화할 수 있습니다 #### 기본 설정 @@ -112,13 +112,13 @@ from agents import set_default_openai_responses_transport set_default_openai_responses_transport("websocket") ``` -이는 기본 OpenAI provider 로 해석되는 OpenAI Responses 모델(`"gpt-5.4"` 같은 문자열 모델 이름 포함)에 적용됩니다 +이 설정은 기본 OpenAI provider가 해석하는 OpenAI Responses 모델(예: `"gpt-5.4"` 같은 문자열 모델 이름)에 적용됩니다 -전송 선택은 SDK 가 모델 이름을 모델 인스턴스로 해석할 때 이루어집니다. 구체적인 [`Model`][agents.models.interface.Model] 객체를 전달하면 해당 전송은 이미 고정됩니다: [`OpenAIResponsesWSModel`][agents.models.openai_responses.OpenAIResponsesWSModel] 은 websocket, [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] 은 HTTP, [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] 은 Chat Completions 를 유지합니다. `RunConfig(model_provider=...)` 를 전달하면 전역 기본값 대신 해당 provider 가 전송 선택을 제어합니다 +전송 선택은 SDK가 모델 이름을 모델 인스턴스로 해석할 때 이루어집니다. 구체적인 [`Model`][agents.models.interface.Model] 객체를 전달하면 해당 객체의 전송은 이미 고정됩니다: [`OpenAIResponsesWSModel`][agents.models.openai_responses.OpenAIResponsesWSModel]은 websocket, [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel]은 HTTP, [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel]은 Chat Completions를 유지합니다. `RunConfig(model_provider=...)`를 전달하면 전역 기본값 대신 해당 provider가 전송 선택을 제어합니다 #### provider 또는 실행 단위 설정 -websocket 전송은 provider 단위 또는 실행 단위로도 구성할 수 있습니다: +websocket 전송은 provider 단위 또는 실행 단위로도 구성할 수 있습니다 ```python from agents import Agent, OpenAIProvider, RunConfig, Runner @@ -137,16 +137,16 @@ result = await Runner.run( ) ``` -#### `MultiProvider` 를 사용한 고급 라우팅 +#### `MultiProvider`를 사용한 고급 라우팅 -접두사 기반 모델 라우팅이 필요하다면(예: 한 번의 실행에서 `openai/...` 와 `any-llm/...` 모델 이름 혼합) [`MultiProvider`][agents.MultiProvider] 를 사용하고, 그곳에서 `openai_use_responses_websocket=True` 를 설정하세요 +접두사 기반 모델 라우팅이 필요하다면(예: 한 실행에서 `openai/...`와 `any-llm/...` 모델 이름 혼합) [`MultiProvider`][agents.MultiProvider]를 사용하고 여기서 `openai_use_responses_websocket=True`를 설정하세요 -`MultiProvider` 는 두 가지 기존 기본값을 유지합니다: +`MultiProvider`는 두 가지 기존 기본 동작을 유지합니다 -- `openai/...` 는 OpenAI provider 의 별칭으로 처리되어 `openai/gpt-4.1` 은 모델 `gpt-4.1` 로 라우팅됩니다 -- 알 수 없는 접두사는 그대로 전달되지 않고 `UserError` 를 발생시킵니다 +- `openai/...`는 OpenAI provider의 별칭으로 처리되어 `openai/gpt-4.1`은 모델 `gpt-4.1`로 라우팅됩니다 +- 알 수 없는 접두사는 전달되지 않고 `UserError`를 발생시킵니다 -OpenAI provider 를 리터럴 네임스페이스 모델 ID 를 기대하는 OpenAI 호환 endpoint 로 지정하는 경우, pass-through 동작을 명시적으로 활성화하세요. websocket 활성화 설정에서는 `MultiProvider` 에서도 `openai_use_responses_websocket=True` 를 유지하세요: +OpenAI provider를 OpenAI 호환 엔드포인트로 지정했고 해당 엔드포인트가 리터럴 네임스페이스 모델 ID를 기대한다면, pass-through 동작을 명시적으로 활성화하세요. websocket 사용 설정에서는 `MultiProvider`에도 `openai_use_responses_websocket=True`를 유지하세요 ```python from agents import Agent, MultiProvider, RunConfig, Runner @@ -172,52 +172,63 @@ result = await Runner.run( ) ``` -backend 가 리터럴 `openai/...` 문자열을 기대하면 `openai_prefix_mode="model_id"` 를 사용하세요. backend 가 `openrouter/openai/gpt-4.1-mini` 같은 다른 네임스페이스 모델 ID 를 기대하면 `unknown_prefix_mode="model_id"` 를 사용하세요. 이 옵션들은 websocket 전송 외부의 `MultiProvider` 에서도 동작합니다. 이 예시는 이 섹션에서 설명한 전송 설정의 일부이므로 websocket 을 활성화한 상태를 유지합니다. 동일한 옵션은 [`responses_websocket_session()`][agents.responses_websocket_session] 에서도 사용할 수 있습니다 +백엔드가 리터럴 `openai/...` 문자열을 기대하면 `openai_prefix_mode="model_id"`를 사용하세요. 백엔드가 `openrouter/openai/gpt-4.1-mini` 같은 다른 네임스페이스 모델 ID를 기대하면 `unknown_prefix_mode="model_id"`를 사용하세요. 이 옵션들은 websocket 전송 외부의 `MultiProvider`에서도 동작합니다. 이 예시는 이 섹션의 전송 설정 일부이므로 websocket을 활성화한 상태를 유지합니다. 동일한 옵션은 [`responses_websocket_session()`][agents.responses_websocket_session]에서도 사용할 수 있습니다 -사용자 지정 OpenAI 호환 endpoint 또는 proxy 를 사용하는 경우 websocket 전송에도 호환되는 websocket `/responses` endpoint 가 필요합니다. 이런 설정에서는 `websocket_base_url` 을 명시적으로 설정해야 할 수 있습니다 +커스텀 OpenAI 호환 엔드포인트 또는 프록시를 사용하는 경우 websocket 전송에도 호환되는 websocket `/responses` 엔드포인트가 필요합니다. 이 경우 `websocket_base_url`을 명시적으로 설정해야 할 수 있습니다 #### 참고 사항 -- 이것은 websocket 전송의 Responses API 이며 [Realtime API](../realtime/guide.md)가 아닙니다. Chat Completions 또는 Responses websocket `/responses` endpoint 를 지원하지 않는 OpenAI 가 아닌 provider 에는 적용되지 않습니다 -- 환경에 `websockets` 패키지가 아직 없다면 설치하세요 -- websocket 전송을 활성화한 뒤 [`Runner.run_streamed()`][agents.run.Runner.run_streamed] 를 직접 사용할 수 있습니다. 여러 턴 워크플로에서 턴 간(중첩된 agent-as-tool 호출 포함) 동일 websocket 연결을 재사용하려면 [`responses_websocket_session()`][agents.responses_websocket_session] 헬퍼를 권장합니다. [Running agents](../running_agents.md) 가이드와 [`examples/basic/stream_ws.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/stream_ws.py)를 참조하세요 +- 이는 websocket 전송 기반 Responses API이며 [Realtime API](../realtime/guide.md)가 아닙니다. Chat Completions 또는 Responses websocket `/responses` 엔드포인트를 지원하지 않는 OpenAI 이외 provider에는 적용되지 않습니다 +- 환경에 `websockets` 패키지가 없으면 설치하세요 +- websocket 전송을 활성화한 뒤 [`Runner.run_streamed()`][agents.run.Runner.run_streamed]를 바로 사용할 수 있습니다. 여러 턴 워크플로에서 동일 websocket 연결을 턴 간(및 중첩된 agent-as-tool 호출 간) 재사용하려면 [`responses_websocket_session()`][agents.responses_websocket_session] 헬퍼를 권장합니다. [에이전트 실행](../running_agents.md) 가이드와 [`examples/basic/stream_ws.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/stream_ws.py)를 참고하세요 -## OpenAI 가 아닌 모델 +## OpenAI 이외 모델 -OpenAI 가 아닌 provider 가 필요하면 SDK 의 내장 provider 통합 지점부터 시작하세요. 많은 설정에서 서드파티 adapter 없이도 충분합니다. 각 패턴의 예시는 [examples/model_providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/)에 있습니다 +OpenAI 이외 provider가 필요하면 SDK의 내장 provider 통합 지점부터 시작하세요. 많은 설정에서 서드파티 어댑터 없이도 충분합니다. 각 패턴의 예시는 [examples/model_providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/)에 있습니다 -### OpenAI 가 아닌 provider 통합 방법 +### OpenAI 이외 provider 통합 방법 -| 접근 방식 | 사용하는 경우 | 범위 | +| 접근 방식 | 이런 경우 사용 | 범위 | | --- | --- | --- | -| [`set_default_openai_client`][agents.set_default_openai_client] | 하나의 OpenAI 호환 endpoint 를 대부분 또는 모든 에이전트의 기본값으로 사용해야 하는 경우 | 전역 기본값 | -| [`ModelProvider`][agents.models.interface.ModelProvider] | 하나의 사용자 지정 provider 를 단일 실행에 적용해야 하는 경우 | 실행 단위 | -| [`Agent.model`][agents.agent.Agent.model] | 서로 다른 에이전트에 서로 다른 provider 또는 구체적 모델 객체가 필요한 경우 | 에이전트 단위 | -| 서드파티 adapter | 내장 경로가 제공하지 않는 adapter 관리 provider 범위 또는 라우팅이 필요한 경우 | [서드파티 adapters](#third-party-adapters) 참조 | +| [`set_default_openai_client`][agents.set_default_openai_client] | 하나의 OpenAI 호환 엔드포인트를 대부분 또는 모든 에이전트의 기본값으로 사용해야 할 때 | 전역 기본값 | +| [`ModelProvider`][agents.models.interface.ModelProvider] | 하나의 커스텀 provider를 단일 실행에 적용해야 할 때 | 실행 단위 | +| [`Agent.model`][agents.agent.Agent.model] | 서로 다른 에이전트에 서로 다른 provider 또는 구체적인 모델 객체가 필요할 때 | 에이전트 단위 | +| 서드파티 어댑터 | 내장 경로가 제공하지 않는 어댑터 관리 provider 범위 또는 라우팅이 필요할 때 | [서드파티 어댑터](#third-party-adapters) 참고 | -이 내장 경로들로 다른 LLM provider 를 통합할 수 있습니다: +이 내장 경로로 다른 LLM provider를 통합할 수 있습니다 -1. [`set_default_openai_client`][agents.set_default_openai_client] 는 `AsyncOpenAI` 인스턴스를 LLM 클라이언트로 전역 사용하려는 경우에 유용합니다. LLM provider 가 OpenAI 호환 API endpoint 를 제공하고 `base_url` 및 `api_key` 를 설정할 수 있는 경우에 해당합니다. 구성 가능한 예시는 [examples/model_providers/custom_example_global.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_global.py)를 참조하세요 -2. [`ModelProvider`][agents.models.interface.ModelProvider] 는 `Runner.run` 수준에서 동작합니다. 이를 통해 "이 실행의 모든 에이전트에 사용자 지정 모델 provider 를 사용"할 수 있습니다. 구성 가능한 예시는 [examples/model_providers/custom_example_provider.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_provider.py)를 참조하세요 -3. [`Agent.model`][agents.agent.Agent.model] 은 특정 Agent 인스턴스에 모델을 지정할 수 있게 해줍니다. 이를 통해 서로 다른 에이전트에 서로 다른 provider 를 혼합해 사용할 수 있습니다. 구성 가능한 예시는 [examples/model_providers/custom_example_agent.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_agent.py)를 참조하세요 +1. [`set_default_openai_client`][agents.set_default_openai_client]는 LLM 클라이언트로 `AsyncOpenAI` 인스턴스를 전역적으로 사용하려는 경우 유용합니다. LLM provider가 OpenAI 호환 API 엔드포인트를 제공하고 `base_url` 및 `api_key`를 설정할 수 있는 경우에 해당합니다. 구성 가능한 예시는 [examples/model_providers/custom_example_global.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_global.py)를 참고하세요 +2. [`ModelProvider`][agents.models.interface.ModelProvider]는 `Runner.run` 수준에서 적용됩니다. 이를 통해 "이 실행의 모든 에이전트에 커스텀 model provider를 사용"하도록 지정할 수 있습니다. 구성 가능한 예시는 [examples/model_providers/custom_example_provider.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_provider.py)를 참고하세요 +3. [`Agent.model`][agents.agent.Agent.model]은 특정 Agent 인스턴스에 모델을 지정할 수 있게 해줍니다. 이를 통해 서로 다른 에이전트에 서로 다른 provider를 조합해 사용할 수 있습니다. 구성 가능한 예시는 [examples/model_providers/custom_example_agent.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_agent.py)를 참고하세요 -`platform.openai.com` 의 API 키가 없는 경우에는 `set_tracing_disabled()` 로 트레이싱을 비활성화하거나 [다른 트레이싱 프로세서](../tracing.md)를 설정하는 것을 권장합니다 +`platform.openai.com`의 API 키가 없는 경우에는 `set_tracing_disabled()`로 트레이싱을 비활성화하거나 [다른 트레이싱 프로세서](../tracing.md)를 설정하는 것을 권장합니다 + +``` python +from agents import Agent, AsyncOpenAI, OpenAIChatCompletionsModel, set_tracing_disabled + +set_tracing_disabled(disabled=True) + +provider = AsyncOpenAI(api_key="Api_Key", base_url="Base URL of Provider") +model = OpenAIChatCompletionsModel(model="Model_Name", openai_client=provider) + +agent= Agent(name="Helping Agent", instructions="You are a Helping Agent", model=model) +``` !!! note - 이 예시들에서는 많은 LLM provider 가 아직 Responses API 를 지원하지 않기 때문에 Chat Completions API/모델을 사용합니다. LLM provider 가 이를 지원한다면 Responses 사용을 권장합니다 + 이 예시들에서는 많은 LLM provider가 아직 Responses API를 지원하지 않기 때문에 Chat Completions API/모델을 사용합니다. LLM provider가 이를 지원한다면 Responses 사용을 권장합니다 ## 하나의 워크플로에서 모델 혼합 -단일 워크플로 내에서 각 에이전트마다 서로 다른 모델을 사용하고 싶을 수 있습니다. 예를 들어, 분류에는 더 작고 빠른 모델을 사용하고 복잡한 작업에는 더 크고 성능이 높은 모델을 사용할 수 있습니다. [`Agent`][agents.Agent] 를 구성할 때는 다음 중 하나로 특정 모델을 선택할 수 있습니다: +단일 워크플로 내에서 에이전트별로 다른 모델을 사용하고 싶을 수 있습니다. 예를 들어 분류에는 더 작고 빠른 모델을, 복잡한 작업에는 더 크고 성능이 높은 모델을 사용할 수 있습니다. [`Agent`][agents.Agent]를 구성할 때는 다음 중 하나로 특정 모델을 선택할 수 있습니다 1. 모델 이름 전달 -2. 임의의 모델 이름 + 해당 이름을 Model 인스턴스로 매핑할 수 있는 [`ModelProvider`][agents.models.interface.ModelProvider] 전달 +2. 모델 이름 + 해당 이름을 Model 인스턴스로 매핑할 수 있는 [`ModelProvider`][agents.models.interface.ModelProvider] 전달 3. [`Model`][agents.models.interface.Model] 구현을 직접 제공 !!! note - SDK 는 [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] 과 [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] 형태를 모두 지원하지만, 두 형태는 지원 기능과 도구 집합이 다르므로 워크플로마다 단일 모델 형태를 사용하는 것을 권장합니다. 워크플로에서 모델 형태를 혼합해야 한다면 사용 중인 모든 기능이 양쪽 모두에서 사용 가능한지 확인하세요 + SDK는 [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel]과 [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] 형태를 모두 지원하지만, 두 형태는 지원 기능 및 도구 집합이 다르므로 워크플로마다 단일 모델 형태를 사용하는 것을 권장합니다. 워크플로에서 모델 형태를 혼합해야 한다면 사용 중인 모든 기능이 양쪽 모두에서 사용 가능한지 확인하세요 ```python from agents import Agent, Runner, AsyncOpenAI, OpenAIChatCompletionsModel @@ -253,7 +264,7 @@ async def main(): 1. OpenAI 모델 이름을 직접 설정합니다 2. [`Model`][agents.models.interface.Model] 구현을 제공합니다 -에이전트에 사용되는 모델을 추가로 구성하려면 temperature 같은 선택적 모델 구성 매개변수를 제공하는 [`ModelSettings`][agents.models.interface.ModelSettings] 를 전달할 수 있습니다 +에이전트에 사용할 모델을 추가로 구성하려면 temperature 같은 선택적 모델 구성 매개변수를 제공하는 [`ModelSettings`][agents.models.interface.ModelSettings]를 전달할 수 있습니다 ```python from agents import Agent, ModelSettings @@ -268,19 +279,19 @@ english_agent = Agent( ## 고급 OpenAI Responses 설정 -OpenAI Responses 경로에서 더 많은 제어가 필요하면 `ModelSettings` 부터 시작하세요 +OpenAI Responses 경로에서 더 많은 제어가 필요하면 `ModelSettings`부터 시작하세요 ### 일반적인 고급 `ModelSettings` 옵션 -OpenAI Responses API 를 사용할 때는 여러 요청 필드가 이미 `ModelSettings` 필드로 직접 제공되므로 이를 위해 `extra_args` 가 필요하지 않습니다 +OpenAI Responses API를 사용할 때 여러 요청 필드는 이미 `ModelSettings`에 직접 대응하는 필드가 있으므로 `extra_args`가 필요하지 않습니다 -- `parallel_tool_calls`: 같은 턴에서 여러 도구 호출 허용 또는 금지 -- `truncation`: 컨텍스트가 넘칠 때 실패하는 대신 Responses API 가 가장 오래된 대화 항목을 제거하도록 `"auto"` 설정 -- `store`: 생성된 응답을 이후 조회를 위해 서버 측에 저장할지 제어. 이는 response ID 에 의존하는 후속 워크플로 및 `store=False` 일 때 로컬 입력으로 대체해야 할 수 있는 세션 압축 흐름에 중요합니다 -- `prompt_cache_retention`: 예를 들어 `"24h"` 로 캐시된 프롬프트 접두사를 더 오래 유지 +- `parallel_tool_calls`: 동일 턴에서 여러 도구 호출 허용 또는 금지 +- `truncation`: 컨텍스트가 넘칠 때 실패 대신 가장 오래된 대화 항목을 Responses API가 제거하도록 `"auto"` 설정 +- `store`: 생성된 응답을 이후 조회를 위해 서버 측에 저장할지 제어. 이는 응답 ID에 의존하는 후속 워크플로 및 `store=False`일 때 로컬 입력으로 폴백해야 할 수 있는 세션 압축 흐름에 중요합니다 +- `prompt_cache_retention`: 예를 들어 `"24h"`처럼 캐시된 프롬프트 접두사를 더 오래 유지 - `response_include`: `web_search_call.action.sources`, `file_search_call.results`, `reasoning.encrypted_content` 같은 더 풍부한 응답 payload 요청 -- `top_logprobs`: 출력 텍스트의 상위 토큰 logprobs 요청. SDK 는 `message.output_text.logprobs` 도 자동으로 추가합니다 -- `retry`: 모델 호출에 대해 runner 관리 재시도 설정 선택 적용. [Runner 관리 재시도](#runner-managed-retries) 참조 +- `top_logprobs`: 출력 텍스트의 상위 토큰 logprobs 요청. SDK는 `message.output_text.logprobs`도 자동 추가 +- `retry`: 모델 호출에 runner 관리 재시도 설정 사용. [Runner 관리 재시도](#runner-managed-retries) 참고 ```python from agents import Agent, ModelSettings @@ -299,13 +310,13 @@ research_agent = Agent( ) ``` -`store=False` 를 설정하면 Responses API 는 이후 서버 측 조회를 위해 해당 응답을 보관하지 않습니다. 이는 무상태 또는 zero-data-retention 스타일 흐름에 유용하지만, 응답 ID 를 재사용하던 기능은 대신 로컬 관리 상태에 의존해야 함을 의미합니다. 예를 들어 [`OpenAIResponsesCompactionSession`][agents.memory.openai_responses_compaction_session.OpenAIResponsesCompactionSession] 은 마지막 응답이 저장되지 않았을 때 기본 `"auto"` 압축 경로를 입력 기반 압축으로 전환합니다. [Sessions 가이드](../sessions/index.md#openai-responses-compaction-sessions)를 참조하세요 +`store=False`를 설정하면 Responses API는 해당 응답을 이후 서버 측 조회에 사용할 수 있도록 보관하지 않습니다. 이는 상태 비저장 또는 zero-data-retention 스타일 흐름에 유용하지만, 응답 ID를 재사용하던 기능이 대신 로컬 관리 상태에 의존해야 함을 의미합니다. 예를 들어 [`OpenAIResponsesCompactionSession`][agents.memory.openai_responses_compaction_session.OpenAIResponsesCompactionSession]은 마지막 응답이 저장되지 않았을 때 기본 `"auto"` 압축 경로를 입력 기반 압축으로 전환합니다. [세션 가이드](../sessions/index.md#openai-responses-compaction-sessions)를 참고하세요 ### `extra_args` 전달 -SDK 가 아직 최상위에 직접 노출하지 않는 provider 전용 또는 최신 요청 필드가 필요할 때 `extra_args` 를 사용하세요 +SDK가 아직 최상위에서 직접 노출하지 않는 provider별 또는 최신 요청 필드가 필요할 때 `extra_args`를 사용하세요 -또한 OpenAI 의 Responses API 사용 시 [추가 선택 매개변수](https://platform.openai.com/docs/api-reference/responses/create) (예: `user`, `service_tier` 등)도 있습니다. 최상위에 없다면 `extra_args` 로 전달할 수 있습니다 +또한 OpenAI Responses API를 사용할 때 [기타 선택적 매개변수](https://platform.openai.com/docs/api-reference/responses/create)(예: `user`, `service_tier` 등)가 있습니다. 이들이 최상위에 없다면 `extra_args`로 전달할 수 있습니다 ```python from agents import Agent, ModelSettings @@ -323,7 +334,7 @@ english_agent = Agent( ## Runner 관리 재시도 -재시도는 런타임 전용이며 옵트인입니다. `ModelSettings(retry=...)` 를 설정하고 재시도 정책이 재시도를 선택하지 않는 한 SDK 는 일반 모델 요청을 재시도하지 않습니다 +재시도는 런타임 전용이며 옵트인 방식입니다. `ModelSettings(retry=...)`를 설정하고 재시도 정책이 재시도를 선택하지 않는 한 SDK는 일반 모델 요청을 재시도하지 않습니다 ```python from agents import Agent, ModelRetrySettings, ModelSettings, retry_policies @@ -351,7 +362,7 @@ agent = Agent( ) ``` -`ModelRetrySettings` 에는 세 가지 필드가 있습니다: +`ModelRetrySettings`에는 세 필드가 있습니다
@@ -363,73 +374,73 @@ agent = Agent(
-재시도 정책은 [`RetryPolicyContext`][agents.retry.RetryPolicyContext] 를 받으며 다음을 포함합니다: +재시도 정책은 [`RetryPolicyContext`][agents.retry.RetryPolicyContext]를 전달받습니다 -- `attempt` 와 `max_retries` 로 시도 횟수 인지 의사결정 가능 -- `stream` 으로 스트리밍/비스트리밍 동작 분기 가능 -- 원시 검사 용도의 `error` -- `status_code`, `retry_after`, `error_code`, `is_network_error`, `is_timeout`, `is_abort` 같은 `normalized` 정보 -- 하위 모델 adapter 가 재시도 가이드를 제공할 수 있을 때의 `provider_advice` +- `attempt`와 `max_retries`로 시도 횟수 기반 의사결정 가능 +- `stream`으로 스트리밍/비스트리밍 동작 분기 가능 +- `error` 원문 점검 +- `status_code`, `retry_after`, `error_code`, `is_network_error`, `is_timeout`, `is_abort` 같은 `normalized` 사실 +- 기본 모델 어댑터가 재시도 가이드를 제공할 수 있을 때 `provider_advice` -정책은 다음 중 하나를 반환할 수 있습니다: +정책은 다음 중 하나를 반환할 수 있습니다 - 단순 재시도 결정을 위한 `True` / `False` -- 지연을 재정의하거나 진단 이유를 첨부하려는 경우 [`RetryDecision`][agents.retry.RetryDecision] +- 지연 재정의 또는 진단 사유 첨부가 필요할 때 [`RetryDecision`][agents.retry.RetryDecision] -SDK 는 `retry_policies` 에 준비된 헬퍼를 제공합니다: +SDK는 `retry_policies`에 즉시 사용 가능한 헬퍼를 제공합니다 | 헬퍼 | 동작 | | --- | --- | -| `retry_policies.never()` | 항상 사용 안 함 | +| `retry_policies.never()` | 항상 비활성화 | | `retry_policies.provider_suggested()` | 가능할 때 provider 재시도 권고를 따름 | -| `retry_policies.network_error()` | 일시적 전송 및 timeout 실패와 매칭 | -| `retry_policies.http_status([...])` | 선택한 HTTP 상태 코드와 매칭 | +| `retry_policies.network_error()` | 일시적 전송/타임아웃 실패 매칭 | +| `retry_policies.http_status([...])` | 선택한 HTTP 상태 코드 매칭 | | `retry_policies.retry_after()` | retry-after 힌트가 있을 때만 해당 지연으로 재시도 | -| `retry_policies.any(...)` | 중첩 정책 중 하나라도 선택하면 재시도 | -| `retry_policies.all(...)` | 중첩 정책 모두가 선택할 때만 재시도 | +| `retry_policies.any(...)` | 중첩 정책 중 하나라도 활성화하면 재시도 | +| `retry_policies.all(...)` | 중첩 정책 모두 활성화할 때만 재시도 | -정책 조합 시 `provider_suggested()` 가 가장 안전한 첫 구성 요소입니다. provider 가 이를 구분할 수 있을 때 provider veto 와 replay-safety 승인을 보존하기 때문입니다 +정책을 조합할 때 `provider_suggested()`는 provider가 이를 구분할 수 있을 때 provider 거부 및 replay 안전 승인 정보를 보존하므로 가장 안전한 첫 구성 요소입니다 ##### 안전 경계 -일부 실패는 자동 재시도되지 않습니다: +일부 실패는 자동 재시도되지 않습니다 - Abort 오류 -- provider 권고에서 replay 가 안전하지 않다고 표시된 요청 -- replay 를 안전하지 않게 만드는 방식으로 출력이 이미 시작된 스트리밍 실행 +- provider 권고가 replay를 안전하지 않다고 표시한 요청 +- replay를 안전하지 않게 만드는 방식으로 출력이 이미 시작된 이후의 스트리밍 실행 -`previous_response_id` 또는 `conversation_id` 를 사용하는 상태 저장형 후속 요청도 더 보수적으로 처리됩니다. 이런 요청에서는 `network_error()` 또는 `http_status([500])` 같은 비provider predicate 만으로는 충분하지 않습니다. 재시도 정책에는 일반적으로 `retry_policies.provider_suggested()` 를 통한 provider 의 replay-safe 승인이 포함되어야 합니다 +`previous_response_id` 또는 `conversation_id`를 사용하는 상태 저장 후속 요청도 더 보수적으로 처리됩니다. 이러한 요청에서는 `network_error()`나 `http_status([500])` 같은 비provider 조건만으로는 충분하지 않습니다. 재시도 정책에는 일반적으로 `retry_policies.provider_suggested()`를 통한 provider의 replay-safe 승인 포함이 필요합니다 -##### Runner 와 에이전트 병합 동작 +##### Runner 및 에이전트 병합 동작 -`retry` 는 runner 수준과 에이전트 수준 `ModelSettings` 사이에서 deep-merge 됩니다: +`retry`는 runner 수준과 에이전트 수준 `ModelSettings` 사이에서 deep merge됩니다 -- 에이전트는 `retry.max_retries` 만 재정의하고 runner 의 `policy` 는 상속할 수 있습니다 +- 에이전트는 `retry.max_retries`만 재정의하고 runner의 `policy`를 상속할 수 있습니다 - 에이전트는 `retry.backoff` 일부만 재정의하고 나머지 backoff 필드는 runner 값을 유지할 수 있습니다 -- `policy` 는 런타임 전용이므로 직렬화된 `ModelSettings` 에는 `max_retries` 와 `backoff` 는 남고 콜백 자체는 제외됩니다 +- `policy`는 런타임 전용이므로 직렬화된 `ModelSettings`에는 `max_retries`와 `backoff`는 남고 콜백 자체는 제외됩니다 -더 자세한 예시는 [`examples/basic/retry.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry.py) 및 [adapter 기반 재시도 예시](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry_litellm.py)를 참조하세요 +더 자세한 예시는 [`examples/basic/retry.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry.py) 및 [어댑터 기반 재시도 예시](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry_litellm.py)를 참고하세요 -## OpenAI 가 아닌 provider 문제 해결 +## OpenAI 이외 provider 문제 해결 ### 트레이싱 클라이언트 오류 401 -트레이싱 관련 오류가 발생한다면, 트레이스가 OpenAI 서버로 업로드되는데 OpenAI API 키가 없기 때문입니다. 해결 방법은 세 가지입니다: +트레이싱 관련 오류가 발생하는 이유는 트레이스가 OpenAI 서버로 업로드되는데 OpenAI API 키가 없기 때문입니다. 해결 방법은 세 가지입니다 1. 트레이싱 완전 비활성화: [`set_tracing_disabled(True)`][agents.set_tracing_disabled] -2. 트레이싱용 OpenAI 키 설정: [`set_tracing_export_api_key(...)`][agents.set_tracing_export_api_key]. 이 API 키는 트레이스 업로드에만 사용되며 [platform.openai.com](https://platform.openai.com/) 키여야 합니다 -3. OpenAI 가 아닌 트레이스 프로세서 사용. [트레이싱 문서](../tracing.md#custom-tracing-processors) 참조 +2. 트레이싱용 OpenAI 키 설정: [`set_tracing_export_api_key(...)`][agents.set_tracing_export_api_key]. 이 API 키는 트레이스 업로드에만 사용되며 [platform.openai.com](https://platform.openai.com/) 발급 키여야 합니다 +3. OpenAI 이외 트레이스 프로세서 사용. [트레이싱 문서](../tracing.md#custom-tracing-processors) 참고 ### Responses API 지원 -SDK 는 기본적으로 Responses API 를 사용하지만, 다른 많은 LLM provider 는 아직 이를 지원하지 않습니다. 그 결과 404 또는 유사한 이슈를 볼 수 있습니다. 해결 방법은 두 가지입니다: +SDK는 기본적으로 Responses API를 사용하지만, 다른 많은 LLM provider는 아직 이를 지원하지 않습니다. 그 결과 404 또는 유사한 문제가 발생할 수 있습니다. 해결 방법은 두 가지입니다 -1. [`set_default_openai_api("chat_completions")`][agents.set_default_openai_api] 호출. 환경 변수로 `OPENAI_API_KEY` 와 `OPENAI_BASE_URL` 을 설정하는 경우 동작합니다 +1. [`set_default_openai_api("chat_completions")`][agents.set_default_openai_api] 호출. 환경 변수로 `OPENAI_API_KEY` 및 `OPENAI_BASE_URL`을 설정하는 경우 동작합니다 2. [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] 사용. 예시는 [여기](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/)에 있습니다 ### structured outputs 지원 -일부 모델 provider 는 [structured outputs](https://platform.openai.com/docs/guides/structured-outputs)를 지원하지 않습니다. 이 경우 때때로 다음과 같은 오류가 발생합니다: +일부 모델 provider는 [structured outputs](https://platform.openai.com/docs/guides/structured-outputs)를 지원하지 않습니다. 때로는 다음과 같은 오류가 발생합니다 ``` @@ -437,34 +448,34 @@ BadRequestError: Error code: 400 - {'error': {'message': "'response_format.type' ``` -이는 일부 모델 provider 의 한계입니다. JSON 출력은 지원하지만 출력에 사용할 `json_schema` 지정은 허용하지 않습니다. 이 문제에 대한 수정 작업을 진행 중이지만, JSON schema 출력을 지원하는 provider 에 의존하는 것을 권장합니다. 그렇지 않으면 잘못된 JSON 때문에 앱이 자주 중단될 수 있습니다 +이는 일부 모델 provider의 한계입니다. JSON 출력은 지원하지만 출력에 사용할 `json_schema` 지정은 허용하지 않습니다. 이를 위한 수정 작업을 진행 중이지만, JSON schema 출력을 지원하는 provider에 의존하는 것을 권장합니다. 그렇지 않으면 잘못된 형식의 JSON 때문에 앱이 자주 깨질 수 있습니다 ## provider 간 모델 혼합 -모델 provider 간 기능 차이를 인지하지 않으면 오류가 발생할 수 있습니다. 예를 들어 OpenAI 는 structured outputs, 멀티모달 입력, 호스티드 file search 와 web search 를 지원하지만 다른 많은 provider 는 이러한 기능을 지원하지 않습니다. 다음 제한 사항에 유의하세요: +모델 provider 간 기능 차이를 인지해야 하며, 그렇지 않으면 오류가 발생할 수 있습니다. 예를 들어 OpenAI는 structured outputs, 멀티모달 입력, 호스티드 file search 및 web search를 지원하지만 많은 다른 provider는 이러한 기능을 지원하지 않습니다. 다음 제한 사항에 유의하세요 -- 지원하지 않는 `tools` 를 이를 이해하지 못하는 provider 에 보내지 마세요 -- 텍스트 전용 모델 호출 전 멀티모달 입력을 필터링하세요 -- structured JSON 출력을 지원하지 않는 provider 는 때때로 유효하지 않은 JSON 을 생성할 수 있음을 유의하세요 +- 지원하지 않는 `tools`를 해당 provider에 보내지 않기 +- 텍스트 전용 모델 호출 전 멀티모달 입력 필터링 +- structured JSON 출력을 지원하지 않는 provider는 때때로 유효하지 않은 JSON을 생성할 수 있음에 유의 -## 서드파티 adapters +## 서드파티 어댑터 -SDK 의 내장 provider 통합 지점만으로 부족할 때만 서드파티 adapter 를 사용하세요. 이 SDK 에서 OpenAI 모델만 사용하는 경우 Any-LLM 이나 LiteLLM 대신 내장 [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] 경로를 우선하세요. 서드파티 adapter 는 OpenAI 모델과 OpenAI 가 아닌 provider 를 함께 사용해야 하거나, 내장 경로가 제공하지 않는 adapter 관리 provider 범위 또는 라우팅이 필요한 경우를 위한 것입니다. adapter 는 SDK 와 업스트림 모델 provider 사이에 추가 호환성 계층을 더하므로 기능 지원과 요청 의미가 provider 별로 달라질 수 있습니다. SDK 는 현재 Any-LLM 과 LiteLLM 을 best-effort 베타 adapter 통합으로 포함합니다 +SDK의 내장 provider 통합 지점만으로 충분하지 않을 때만 서드파티 어댑터를 사용하세요. 이 SDK에서 OpenAI 모델만 사용하는 경우 Any-LLM 또는 LiteLLM 대신 내장 [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] 경로를 선호하세요. 서드파티 어댑터는 OpenAI 모델과 OpenAI 이외 provider를 결합해야 하거나, 내장 경로가 제공하지 않는 어댑터 관리 provider 범위/라우팅이 필요할 때를 위한 것입니다. 어댑터는 SDK와 업스트림 모델 provider 사이에 또 하나의 호환성 계층을 추가하므로 기능 지원과 요청 의미가 provider마다 다를 수 있습니다. SDK는 현재 Any-LLM 및 LiteLLM을 best-effort 베타 어댑터 통합으로 포함합니다 ### Any-LLM -Any-LLM 지원은 Any-LLM 관리 provider 범위 또는 라우팅이 필요한 경우를 위해 best-effort 베타 형태로 제공됩니다 +Any-LLM 지원은 Any-LLM 관리 provider 범위 또는 라우팅이 필요한 경우를 위해 best-effort 베타 방식으로 제공됩니다 -업스트림 provider 경로에 따라 Any-LLM 은 Responses API, Chat Completions 호환 API 또는 provider 전용 호환성 계층을 사용할 수 있습니다 +업스트림 provider 경로에 따라 Any-LLM은 Responses API, Chat Completions 호환 API 또는 provider별 호환성 계층을 사용할 수 있습니다 -Any-LLM 이 필요하면 `openai-agents[any-llm]` 을 설치한 뒤 [`examples/model_providers/any_llm_auto.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/any_llm_auto.py) 또는 [`examples/model_providers/any_llm_provider.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/any_llm_provider.py)부터 시작하세요. [`MultiProvider`][agents.MultiProvider] 와 함께 `any-llm/...` 모델 이름을 사용하거나, `AnyLLMModel` 을 직접 인스턴스화하거나, 실행 범위에서 `AnyLLMProvider` 를 사용할 수 있습니다. 모델 표면을 명시적으로 고정해야 하면 `AnyLLMModel` 생성 시 `api="responses"` 또는 `api="chat_completions"` 를 전달하세요 +Any-LLM이 필요하면 `openai-agents[any-llm]`를 설치한 뒤 [`examples/model_providers/any_llm_auto.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/any_llm_auto.py) 또는 [`examples/model_providers/any_llm_provider.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/any_llm_provider.py)부터 시작하세요. [`MultiProvider`][agents.MultiProvider]와 함께 `any-llm/...` 모델 이름을 사용하거나, `AnyLLMModel`을 직접 인스턴스화하거나, 실행 범위에서 `AnyLLMProvider`를 사용할 수 있습니다. 모델 표면을 명시적으로 고정해야 하면 `AnyLLMModel` 구성 시 `api="responses"` 또는 `api="chat_completions"`를 전달하세요 -Any-LLM 은 여전히 서드파티 adapter 계층이므로 provider 의존성과 기능 격차는 SDK 가 아니라 Any-LLM 업스트림에서 정의됩니다. 사용량 지표는 업스트림 provider 가 반환할 때 자동 전파되지만, 스트리밍 Chat Completions backend 는 사용량 청크를 내보내기 전에 `ModelSettings(include_usage=True)` 가 필요할 수 있습니다. structured outputs, 도구 호출, 사용량 보고, Responses 전용 동작에 의존한다면 배포하려는 정확한 provider backend 를 검증하세요 +Any-LLM은 여전히 서드파티 어댑터 계층이므로 provider 종속성과 기능 격차는 SDK가 아니라 업스트림 Any-LLM이 정의합니다. 사용량 지표는 업스트림 provider가 반환할 때 자동 전파되지만, 스트리밍 Chat Completions 백엔드는 사용량 청크를 내보내기 전에 `ModelSettings(include_usage=True)`가 필요할 수 있습니다. structured outputs, tool calling, 사용량 보고 또는 Responses 전용 동작에 의존한다면 배포 예정인 정확한 provider 백엔드를 검증하세요 ### LiteLLM -LiteLLM 지원은 LiteLLM 전용 provider 범위 또는 라우팅이 필요한 경우를 위해 best-effort 베타 형태로 제공됩니다 +LiteLLM 지원은 LiteLLM 전용 provider 범위 또는 라우팅이 필요한 경우를 위해 best-effort 베타 방식으로 제공됩니다 -LiteLLM 이 필요하면 `openai-agents[litellm]` 을 설치한 뒤 [`examples/model_providers/litellm_auto.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/litellm_auto.py) 또는 [`examples/model_providers/litellm_provider.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/litellm_provider.py)부터 시작하세요. `litellm/...` 모델 이름을 사용하거나 [`LitellmModel`][agents.extensions.models.litellm_model.LitellmModel] 을 직접 인스턴스화할 수 있습니다 +LiteLLM이 필요하면 `openai-agents[litellm]`를 설치한 뒤 [`examples/model_providers/litellm_auto.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/litellm_auto.py) 또는 [`examples/model_providers/litellm_provider.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/litellm_provider.py)부터 시작하세요. `litellm/...` 모델 이름을 사용하거나 [`LitellmModel`][agents.extensions.models.litellm_model.LitellmModel]을 직접 인스턴스화할 수 있습니다 -일부 LiteLLM 기반 provider 는 기본적으로 SDK 사용량 지표를 채우지 않습니다. 사용량 보고가 필요하면 `ModelSettings(include_usage=True)` 를 전달하고 structured outputs, 도구 호출, 사용량 보고 또는 adapter 전용 라우팅 동작에 의존한다면 배포하려는 정확한 provider backend 를 검증하세요 \ No newline at end of file +일부 LiteLLM 기반 provider는 기본적으로 SDK 사용량 지표를 채우지 않습니다. 사용량 보고가 필요하면 `ModelSettings(include_usage=True)`를 전달하고, structured outputs, tool calling, 사용량 보고 또는 어댑터별 라우팅 동작에 의존한다면 배포 예정인 정확한 provider 백엔드를 검증하세요 \ No newline at end of file diff --git a/docs/zh/models/index.md b/docs/zh/models/index.md index 02a98a3acb..0dade618fa 100644 --- a/docs/zh/models/index.md +++ b/docs/zh/models/index.md @@ -4,31 +4,31 @@ search: --- # 模型 -Agents SDK 对 OpenAI 模型提供开箱即用的两种支持方式: +Agents SDK 开箱即用支持两种形式的 OpenAI 模型: -- **推荐**:[`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel],通过新的[Responses API](https://platform.openai.com/docs/api-reference/responses)调用 OpenAI API。 -- [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel],通过[Chat Completions API](https://platform.openai.com/docs/api-reference/chat)调用 OpenAI API。 +- **推荐**:[`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel],使用新的 [Responses API](https://platform.openai.com/docs/api-reference/responses) 调用 OpenAI API。 +- [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel],使用 [Chat Completions API](https://platform.openai.com/docs/api-reference/chat) 调用 OpenAI API。 ## 模型设置选择 -从最适合你当前设置的最简单路径开始: +从最符合你当前设置的最简单路径开始: -| 如果你想要... | 推荐路径 | 了解更多 | +| 如果你想要…… | 推荐路径 | 了解更多 | | --- | --- | --- | -| 仅使用 OpenAI 模型 | 使用默认 OpenAI provider 与 Responses 模型路径 | [OpenAI 模型](#openai-models) | +| 仅使用 OpenAI 模型 | 使用默认 OpenAI provider 和 Responses 模型路径 | [OpenAI 模型](#openai-models) | | 通过 websocket 传输使用 OpenAI Responses API | 保持 Responses 模型路径并启用 websocket 传输 | [Responses WebSocket 传输](#responses-websocket-transport) | | 使用一个非 OpenAI provider | 从内置 provider 集成点开始 | [非 OpenAI 模型](#non-openai-models) | -| 在智能体之间混合模型或 providers | 按每次运行或每个智能体选择 provider,并检查功能差异 | [在单个工作流中混合模型](#mixing-models-in-one-workflow) 和 [跨 providers 混合模型](#mixing-models-across-providers) | +| 在多个智能体之间混合模型或 provider | 按每次运行或每个智能体选择 provider,并查看功能差异 | [在一个工作流中混合模型](#mixing-models-in-one-workflow) 和 [跨 provider 混合模型](#mixing-models-across-providers) | | 调整高级 OpenAI Responses 请求设置 | 在 OpenAI Responses 路径上使用 `ModelSettings` | [高级 OpenAI Responses 设置](#advanced-openai-responses-settings) | -| 使用第三方适配器进行非 OpenAI 或混合 provider 路由 | 比较受支持的 beta 适配器,并验证你计划上线的 provider 路径 | [第三方适配器](#third-party-adapters) | +| 使用第三方适配器进行非 OpenAI 或混合 provider 路由 | 比较支持的 beta 适配器,并验证你计划上线的 provider 路径 | [第三方适配器](#third-party-adapters) | ## OpenAI 模型 -对于大多数仅使用 OpenAI 的应用,推荐路径是使用字符串模型名与默认 OpenAI provider,并保持在 Responses 模型路径上。 +对于大多数仅使用 OpenAI 的应用,推荐路径是使用字符串模型名、默认 OpenAI provider,并保持在 Responses 模型路径上。 -当你在初始化 `Agent` 时未指定模型,将使用默认模型。当前默认是 [`gpt-4.1`](https://developers.openai.com/api/docs/models/gpt-4.1),以兼顾兼容性与低延迟。如果你有权限,我们建议将智能体设置为 [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4) 以获得更高质量,同时显式设置 `model_settings`。 +当你在初始化 `Agent` 时未指定模型,将使用默认模型。当前默认是 [`gpt-4.1`](https://developers.openai.com/api/docs/models/gpt-4.1),以兼顾兼容性和低延迟。若你有访问权限,我们建议将智能体设置为 [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4) 以获得更高质量,同时保持显式 `model_settings`。 -如果你想切换到其他模型(如 [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4)),有两种方式配置智能体。 +如果你想切换到其他模型(例如 [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4)),有两种方式可配置智能体。 ### 默认模型 @@ -39,7 +39,7 @@ export OPENAI_DEFAULT_MODEL=gpt-5.4 python3 my_awesome_agent.py ``` -其次,你可以通过 `RunConfig` 为一次运行设置默认模型。如果某个智能体未设置模型,将使用该运行的模型。 +其次,你可以通过 `RunConfig` 为一次运行设置默认模型。如果你未为某个智能体设置模型,将使用本次运行的模型。 ```python from agents import Agent, RunConfig, Runner @@ -58,7 +58,7 @@ result = await Runner.run( #### GPT-5 模型 -当你以这种方式使用任意 GPT-5 模型(例如 [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4))时,SDK 会应用默认 `ModelSettings`。它会设置适用于大多数场景的最佳选项。若要调整默认模型的推理强度,请传入你自己的 `ModelSettings`: +当你以这种方式使用任意 GPT-5 模型(例如 [`gpt-5.4`](https://developers.openai.com/api/docs/models/gpt-5.4))时,SDK 会应用默认 `ModelSettings`。它会设置在大多数场景下效果最佳的选项。要调整默认模型的推理力度,请传入你自己的 `ModelSettings`: ```python from openai.types.shared import Reasoning @@ -78,33 +78,33 @@ my_agent = Agent( #### ComputerTool 模型选择 -如果智能体包含 [`ComputerTool`][agents.tool.ComputerTool],实际 Responses 请求中的生效模型将决定 SDK 发送哪种 computer-tool 负载。显式的 `gpt-5.4` 请求会使用 GA 内置 `computer` 工具;显式的 `computer-use-preview` 请求则保持旧版 `computer_use_preview` 负载。 +如果一个智能体包含 [`ComputerTool`][agents.tool.ComputerTool],实际 Responses 请求中的有效模型将决定 SDK 发送哪种 computer-tool 负载。显式 `gpt-5.4` 请求会使用 GA 内置 `computer` 工具,而显式 `computer-use-preview` 请求会保留旧的 `computer_use_preview` 负载。 -由提示词管理的调用是主要例外。如果提示模板拥有模型且 SDK 在请求中省略 `model`,SDK 会默认使用与 preview 兼容的 computer 负载,以避免猜测提示绑定了哪个模型。要在该流程中保持 GA 路径,可在请求中显式设置 `model="gpt-5.4"`,或通过 `ModelSettings(tool_choice="computer")` 或 `ModelSettings(tool_choice="computer_use")` 强制 GA 选择器。 +由提示词管理的调用是主要例外。如果提示模板拥有模型且 SDK 在请求中省略 `model`,SDK 会默认使用与 preview 兼容的 computer 负载,以避免猜测提示固定了哪个模型。要在该流程中保持 GA 路径,可在请求中显式设置 `model="gpt-5.4"`,或使用 `ModelSettings(tool_choice="computer")` 或 `ModelSettings(tool_choice="computer_use")` 强制 GA 选择器。 -在已注册 [`ComputerTool`][agents.tool.ComputerTool] 的情况下,`tool_choice="computer"`、`"computer_use"` 和 `"computer_use_preview"` 会被标准化为与生效请求模型匹配的内置选择器。如果未注册 `ComputerTool`,这些字符串会继续按普通函数名处理。 +当已注册 [`ComputerTool`][agents.tool.ComputerTool] 时,`tool_choice="computer"`、`"computer_use"` 和 `"computer_use_preview"` 会被规范化为与有效请求模型匹配的内置选择器。若未注册 `ComputerTool`,这些字符串仍按普通函数名处理。 -与 preview 兼容的请求必须预先序列化 `environment` 和显示尺寸,因此使用 [`ComputerProvider`][agents.tool.ComputerProvider] 工厂的提示词管理流程应传入具体的 `Computer` 或 `AsyncComputer` 实例,或在发送请求前强制使用 GA 选择器。完整迁移细节见 [Tools](../tools.md#computertool-and-the-responses-computer-tool)。 +与 preview 兼容的请求必须提前序列化 `environment` 和显示尺寸,因此使用 [`ComputerProvider`][agents.tool.ComputerProvider] 工厂的提示词管理流程应传入具体的 `Computer` 或 `AsyncComputer` 实例,或在发送请求前强制使用 GA 选择器。完整迁移细节见 [工具](../tools.md#computertool-and-the-responses-computer-tool)。 #### 非 GPT-5 模型 如果你传入非 GPT-5 模型名且未提供自定义 `model_settings`,SDK 会回退到与任意模型兼容的通用 `ModelSettings`。 -### 仅 Responses 的工具检索功能 +### 仅限 Responses 的工具搜索功能 -以下工具功能仅在 OpenAI Responses 模型中受支持: +以下工具功能仅在 OpenAI Responses 模型下受支持: - [`ToolSearchTool`][agents.tool.ToolSearchTool] - [`tool_namespace()`][agents.tool.tool_namespace] -- `@function_tool(defer_loading=True)` 以及其他延迟加载的 Responses 工具接口 +- `@function_tool(defer_loading=True)` 及其他延迟加载的 Responses 工具接口 -这些功能在 Chat Completions 模型和非 Responses 后端上会被拒绝。当你使用延迟加载工具时,请将 `ToolSearchTool()` 添加到智能体,并让模型通过 `auto` 或 `required` 的 tool choice 加载工具,而不是强制使用裸命名空间名或仅延迟加载的函数名。设置细节与当前限制见 [Tools](../tools.md#hosted-tool-search)。 +这些功能在 Chat Completions 模型和非 Responses 后端上会被拒绝。当你使用延迟加载工具时,请将 `ToolSearchTool()` 添加到智能体,并让模型通过 `auto` 或 `required` 的工具选择来加载工具,而不是强制使用裸命名空间名称或仅延迟加载的函数名。配置细节和当前限制见 [工具](../tools.md#hosted-tool-search)。 ### Responses WebSocket 传输 默认情况下,OpenAI Responses API 请求使用 HTTP 传输。使用 OpenAI 支持的模型时,你可以选择启用 websocket 传输。 -#### 基础设置 +#### 基本设置 ```python from agents import set_default_openai_responses_transport @@ -112,11 +112,11 @@ from agents import set_default_openai_responses_transport set_default_openai_responses_transport("websocket") ``` -这会影响由默认 OpenAI provider 解析出的 OpenAI Responses 模型(包括如 `"gpt-5.4"` 的字符串模型名)。 +这会影响由默认 OpenAI provider 解析的 OpenAI Responses 模型(包括 `"gpt-5.4"` 这类字符串模型名)。 -传输方式的选择发生在 SDK 将模型名解析为模型实例时。如果你传入具体的 [`Model`][agents.models.interface.Model] 对象,其传输方式已固定:[`OpenAIResponsesWSModel`][agents.models.openai_responses.OpenAIResponsesWSModel] 使用 websocket,[`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] 使用 HTTP,而 [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] 保持使用 Chat Completions。若传入 `RunConfig(model_provider=...)`,则由该 provider 控制传输选择而非全局默认值。 +传输方式的选择发生在 SDK 将模型名解析为模型实例时。如果你传入具体的 [`Model`][agents.models.interface.Model] 对象,其传输方式已固定:[`OpenAIResponsesWSModel`][agents.models.openai_responses.OpenAIResponsesWSModel] 使用 websocket,[`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] 使用 HTTP,而 [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] 保持在 Chat Completions。若你传入 `RunConfig(model_provider=...)`,则由该 provider 控制传输选择,而非全局默认值。 -#### Provider 或运行级设置 +#### provider 或运行级设置 你也可以按 provider 或按运行配置 websocket 传输: @@ -139,14 +139,14 @@ result = await Runner.run( #### 使用 `MultiProvider` 的高级路由 -如果你需要基于前缀的模型路由(例如在一次运行中混用 `openai/...` 和 `any-llm/...` 模型名),请使用 [`MultiProvider`][agents.MultiProvider],并在其中设置 `openai_use_responses_websocket=True`。 +如果你需要基于前缀的模型路由(例如在一次运行中混用 `openai/...` 和 `any-llm/...` 模型名),请改用 [`MultiProvider`][agents.MultiProvider],并在其中设置 `openai_use_responses_websocket=True`。 `MultiProvider` 保留两个历史默认行为: - `openai/...` 被视为 OpenAI provider 的别名,因此 `openai/gpt-4.1` 会按模型 `gpt-4.1` 路由。 - 未知前缀会抛出 `UserError`,而不是透传。 -当你将 OpenAI provider 指向一个期望字面量命名空间模型 ID 的 OpenAI 兼容端点时,请显式启用透传行为。在启用 websocket 的设置中,也请在 `MultiProvider` 上保持 `openai_use_responses_websocket=True`: +当你将 OpenAI provider 指向一个期望字面命名空间模型 ID 的 OpenAI 兼容端点时,请显式启用透传行为。在启用 websocket 的设置中,也请在 `MultiProvider` 上保持 `openai_use_responses_websocket=True`: ```python from agents import Agent, MultiProvider, RunConfig, Runner @@ -172,52 +172,63 @@ result = await Runner.run( ) ``` -当后端期望字面量 `openai/...` 字符串时,使用 `openai_prefix_mode="model_id"`。当后端期望其他命名空间模型 ID(如 `openrouter/openai/gpt-4.1-mini`)时,使用 `unknown_prefix_mode="model_id"`。这些选项在非 websocket 传输的 `MultiProvider` 上同样适用;本示例保持 websocket 启用,因为它属于本节描述的传输设置。相同选项也可用于 [`responses_websocket_session()`][agents.responses_websocket_session]。 +当后端期望字面 `openai/...` 字符串时,使用 `openai_prefix_mode="model_id"`。当后端期望其他命名空间模型 ID(如 `openrouter/openai/gpt-4.1-mini`)时,使用 `unknown_prefix_mode="model_id"`。这些选项也可用于 websocket 传输之外的 `MultiProvider`;本示例保持 websocket 启用是因为它属于本节描述的传输设置。相同选项也可用于 [`responses_websocket_session()`][agents.responses_websocket_session]。 -如果你使用自定义 OpenAI 兼容端点或代理,websocket 传输还要求有兼容的 websocket `/responses` 端点。在这些设置下,你可能需要显式设置 `websocket_base_url`。 +如果你使用自定义 OpenAI 兼容端点或代理,websocket 传输还需要兼容的 websocket `/responses` 端点。在这些设置中,你可能需要显式设置 `websocket_base_url`。 #### 说明 -- 这是基于 websocket 传输的 Responses API,不是[Realtime API](../realtime/guide.md)。除非支持 Responses websocket `/responses` 端点,否则不适用于 Chat Completions 或非 OpenAI providers。 -- 如果你的环境中尚未安装,请安装 `websockets` 包。 -- 启用 websocket 传输后,你可以直接使用 [`Runner.run_streamed()`][agents.run.Runner.run_streamed]。对于希望在多轮流程中跨轮次(以及嵌套 agent-as-tool 调用)复用同一 websocket 连接的场景,推荐使用 [`responses_websocket_session()`][agents.responses_websocket_session] 辅助方法。参见[运行智能体](../running_agents.md)指南和 [`examples/basic/stream_ws.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/stream_ws.py)。 +- 这是通过 websocket 传输的 Responses API,不是 [Realtime API](../realtime/guide.md)。除非支持 Responses websocket `/responses` 端点,否则不适用于 Chat Completions 或非 OpenAI provider。 +- 如果你的环境中还没有 `websockets` 包,请安装它。 +- 启用 websocket 传输后,你可以直接使用 [`Runner.run_streamed()`][agents.run.Runner.run_streamed]。对于希望在多轮工作流(以及嵌套的 Agents-as-tools 调用)中复用同一 websocket 连接的场景,推荐使用 [`responses_websocket_session()`][agents.responses_websocket_session] 辅助函数。请参阅 [运行智能体](../running_agents.md) 指南和 [`examples/basic/stream_ws.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/stream_ws.py)。 ## 非 OpenAI 模型 -如果你需要非 OpenAI provider,请先使用 SDK 的内置 provider 集成点。在许多设置中,这已经足够,无需增加第三方适配器。每种模式的示例见 [examples/model_providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/)。 +如果你需要非 OpenAI provider,先从 SDK 内置的 provider 集成点开始。在许多设置中,无需增加第三方适配器即可满足需求。各模式示例见 [examples/model_providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/)。 ### 非 OpenAI provider 集成方式 | 方式 | 适用场景 | 范围 | | --- | --- | --- | -| [`set_default_openai_client`][agents.set_default_openai_client] | 一个 OpenAI 兼容端点应作为大多数或所有智能体的默认值 | 全局默认 | -| [`ModelProvider`][agents.models.interface.ModelProvider] | 一个自定义 provider 应用于单次运行 | 每次运行 | +| [`set_default_openai_client`][agents.set_default_openai_client] | 一个 OpenAI 兼容端点应作为大多数或全部智能体的默认值 | 全局默认 | +| [`ModelProvider`][agents.models.interface.ModelProvider] | 一个自定义 provider 仅应用于单次运行 | 每次运行 | | [`Agent.model`][agents.agent.Agent.model] | 不同智能体需要不同 provider 或具体模型对象 | 每个智能体 | -| 第三方适配器 | 你需要内置路径未提供的适配器管理 provider 覆盖或路由 | 见[第三方适配器](#third-party-adapters) | +| 第三方适配器 | 你需要内置路径未提供的适配器管理 provider 覆盖或路由 | 见 [第三方适配器](#third-party-adapters) | -你可以通过这些内置路径集成其他 LLM providers: +你可以通过以下内置路径集成其他 LLM provider: -1. [`set_default_openai_client`][agents.set_default_openai_client] 适用于你希望全局使用 `AsyncOpenAI` 实例作为 LLM 客户端的场景。这适用于 LLM provider 提供 OpenAI 兼容 API 端点,并且你可以设置 `base_url` 与 `api_key` 的情况。可配置示例见 [examples/model_providers/custom_example_global.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_global.py)。 -2. [`ModelProvider`][agents.models.interface.ModelProvider] 位于 `Runner.run` 级别。这使你可以声明“本次运行中的所有智能体都使用一个自定义模型 provider”。可配置示例见 [examples/model_providers/custom_example_provider.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_provider.py)。 -3. [`Agent.model`][agents.agent.Agent.model] 允许你在特定 Agent 实例上指定模型。这使你可以为不同智能体混合搭配不同 providers。可配置示例见 [examples/model_providers/custom_example_agent.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_agent.py)。 +1. [`set_default_openai_client`][agents.set_default_openai_client] 适用于你希望全局使用 `AsyncOpenAI` 实例作为 LLM 客户端的场景。这适用于 LLM provider 提供 OpenAI 兼容 API 端点,且你可设置 `base_url` 与 `api_key`。可配置示例见 [examples/model_providers/custom_example_global.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_global.py)。 +2. [`ModelProvider`][agents.models.interface.ModelProvider] 作用于 `Runner.run` 级别。你可以指定“本次运行中所有智能体都使用自定义模型 provider”。可配置示例见 [examples/model_providers/custom_example_provider.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_provider.py)。 +3. [`Agent.model`][agents.agent.Agent.model] 允许你在特定 Agent 实例上指定模型。这使你可以为不同智能体混用不同 provider。可配置示例见 [examples/model_providers/custom_example_agent.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_agent.py)。 -在你没有 `platform.openai.com` API key 的情况下,我们建议通过 `set_tracing_disabled()` 禁用追踪,或设置[其他追踪进程](../tracing.md)。 +当你没有来自 `platform.openai.com` 的 API key 时,我们建议通过 `set_tracing_disabled()` 禁用追踪,或配置[其他追踪进程](../tracing.md)。 + +``` python +from agents import Agent, AsyncOpenAI, OpenAIChatCompletionsModel, set_tracing_disabled + +set_tracing_disabled(disabled=True) + +provider = AsyncOpenAI(api_key="Api_Key", base_url="Base URL of Provider") +model = OpenAIChatCompletionsModel(model="Model_Name", openai_client=provider) + +agent= Agent(name="Helping Agent", instructions="You are a Helping Agent", model=model) +``` !!! note - 在这些示例中,我们使用 Chat Completions API/模型,因为许多 LLM providers 仍不支持 Responses API。如果你的 LLM provider 支持,我们建议使用 Responses。 + 在这些示例中,我们使用 Chat Completions API/模型,因为许多 LLM provider 仍不支持 Responses API。如果你的 LLM provider 支持它,我们建议使用 Responses。 -## 在单个工作流中混合模型 +## 单一工作流中的模型混用 -在单个工作流内,你可能希望每个智能体使用不同模型。例如,你可以在分流阶段使用更小、更快的模型,在复杂任务中使用更大、更强的模型。配置 [`Agent`][agents.Agent] 时,你可以通过以下方式选择特定模型: +在单个工作流中,你可能希望为每个智能体使用不同模型。例如,你可以在分诊中使用更小、更快的模型,而在复杂任务中使用更大、更强的模型。配置 [`Agent`][agents.Agent] 时,你可通过以下任一方式选择特定模型: 1. 传入模型名称。 -2. 传入任意模型名 + 一个可将该名称映射为 Model 实例的 [`ModelProvider`][agents.models.interface.ModelProvider]。 -3. 直接提供一个 [`Model`][agents.models.interface.Model] 实现。 +2. 传入任意模型名称 + 一个可将该名称映射到模型实例的 [`ModelProvider`][agents.models.interface.ModelProvider]。 +3. 直接提供 [`Model`][agents.models.interface.Model] 实现。 !!! note - 虽然我们的 SDK 同时支持 [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] 和 [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] 两种形态,但我们建议每个工作流只使用一种模型形态,因为两者支持的功能和工具集合不同。如果你的工作流必须混用模型形态,请确保你使用的所有功能在两者上都可用。 + 虽然我们的 SDK 同时支持 [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] 与 [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel] 两种形态,但我们建议每个工作流只使用一种模型形态,因为两者支持的功能和工具集合不同。如果你的工作流必须混用模型形态,请确保你使用的全部功能在两者上都可用。 ```python from agents import Agent, Runner, AsyncOpenAI, OpenAIChatCompletionsModel @@ -253,7 +264,7 @@ async def main(): 1. 直接设置 OpenAI 模型名称。 2. 提供一个 [`Model`][agents.models.interface.Model] 实现。 -当你希望进一步配置智能体所用模型时,可传入 [`ModelSettings`][agents.models.interface.ModelSettings],它提供如 temperature 等可选模型配置参数。 +当你希望进一步配置智能体使用的模型时,可以传入 [`ModelSettings`][agents.models.interface.ModelSettings],它提供如 temperature 等可选模型配置参数。 ```python from agents import Agent, ModelSettings @@ -268,19 +279,19 @@ english_agent = Agent( ## 高级 OpenAI Responses 设置 -当你走 OpenAI Responses 路径并需要更多控制时,请从 `ModelSettings` 开始。 +当你使用 OpenAI Responses 路径并需要更多控制时,请先从 `ModelSettings` 开始。 ### 常见高级 `ModelSettings` 选项 -当你使用 OpenAI Responses API 时,多个请求字段已经有对应的 `ModelSettings` 直接字段,因此不需要用 `extra_args` 传入。 +当你使用 OpenAI Responses API 时,多个请求字段已在 `ModelSettings` 中有直接对应字段,因此无需为其使用 `extra_args`。 - `parallel_tool_calls`:允许或禁止同一轮中的多个工具调用。 -- `truncation`:设为 `"auto"` 可让 Responses API 在上下文将溢出时丢弃最旧会话项,而不是直接失败。 -- `store`:控制生成的响应是否存储在服务端供后续检索。这会影响依赖响应 ID 的后续工作流,以及在 `store=False` 时可能需要回退到本地输入的会话压缩流程。 -- `prompt_cache_retention`:延长提示词缓存前缀保留时间,例如设为 `"24h"`。 -- `response_include`:请求更丰富的响应负载,如 `web_search_call.action.sources`、`file_search_call.results` 或 `reasoning.encrypted_content`。 -- `top_logprobs`:请求输出文本的 top-token logprobs。SDK 也会自动加入 `message.output_text.logprobs`。 -- `retry`:为模型调用启用由 runner 管理的重试设置。见[Runner 管理的重试](#runner-managed-retries)。 +- `truncation`:设置为 `"auto"`,让 Responses API 在上下文将溢出时丢弃最旧对话项,而不是失败。 +- `store`:控制生成响应是否在服务端存储以供后续检索。这对依赖响应 ID 的后续工作流,以及在 `store=False` 时可能需要回退到本地输入的会话压缩流程都很重要。 +- `prompt_cache_retention`:让缓存的提示词前缀保留更久,例如 `"24h"`。 +- `response_include`:请求更丰富的响应负载,例如 `web_search_call.action.sources`、`file_search_call.results` 或 `reasoning.encrypted_content`。 +- `top_logprobs`:请求输出文本的 top-token logprobs。SDK 也会自动添加 `message.output_text.logprobs`。 +- `retry`:为模型调用启用由 runner 管理的重试设置。见 [Runner 管理的重试](#runner-managed-retries)。 ```python from agents import Agent, ModelSettings @@ -299,13 +310,13 @@ research_agent = Agent( ) ``` -当你设置 `store=False` 时,Responses API 不会保留该响应以供后续服务端检索。这适用于无状态或零数据保留风格流程,但也意味着原本可复用响应 ID 的功能需要依赖本地管理状态。例如,当上一条响应未存储时,[`OpenAIResponsesCompactionSession`][agents.memory.openai_responses_compaction_session.OpenAIResponsesCompactionSession] 会将其默认 `"auto"` 压缩路径切换为基于输入的压缩。参见[Sessions 指南](../sessions/index.md#openai-responses-compaction-sessions)。 +当你设置 `store=False` 时,Responses API 不会保留该响应以供之后服务端检索。这对无状态或零数据保留风格流程很有用,但也意味着原本会复用响应 ID 的功能需要依赖本地管理状态。例如,[`OpenAIResponsesCompactionSession`][agents.memory.openai_responses_compaction_session.OpenAIResponsesCompactionSession] 会在上次响应未存储时,将默认 `"auto"` 压缩路径切换为基于输入的压缩。见[会话指南](../sessions/index.md#openai-responses-compaction-sessions)。 ### 传递 `extra_args` 当你需要 SDK 尚未直接在顶层暴露的 provider 特定字段或较新请求字段时,使用 `extra_args`。 -另外,当你使用 OpenAI 的 Responses API 时,[还有一些其他可选参数](https://platform.openai.com/docs/api-reference/responses/create)(例如 `user`、`service_tier` 等)。如果它们未在顶层提供,你也可以通过 `extra_args` 传递。 +此外,当你使用 OpenAI 的 Responses API 时,[还有一些其他可选参数](https://platform.openai.com/docs/api-reference/responses/create)(如 `user`、`service_tier` 等)。若它们未在顶层可用,也可通过 `extra_args` 传递。 ```python from agents import Agent, ModelSettings @@ -323,7 +334,7 @@ english_agent = Agent( ## Runner 管理的重试 -重试是仅运行时生效且需显式启用的。除非你设置 `ModelSettings(retry=...)` 且重试策略选择重试,否则 SDK 不会重试普通模型请求。 +重试仅在运行时生效,且需主动启用。除非你设置 `ModelSettings(retry=...)` 且重试策略决定重试,否则 SDK 不会重试一般模型请求。 ```python from agents import Agent, ModelRetrySettings, ModelSettings, retry_policies @@ -357,79 +368,79 @@ agent = Agent( | 字段 | 类型 | 说明 | | --- | --- | --- | -| `max_retries` | `int | None` | 初次请求之后允许的重试次数。 | -| `backoff` | `ModelRetryBackoffSettings | dict | None` | 当策略选择重试但未返回显式延迟时使用的默认延迟策略。 | -| `policy` | `RetryPolicy | None` | 决定是否重试的回调。此字段仅在运行时生效,不会被序列化。 | +| `max_retries` | `int | None` | 初始请求之后允许的重试次数。 | +| `backoff` | `ModelRetryBackoffSettings | dict | None` | 当策略决定重试但未返回显式延迟时使用的默认延迟策略。 | +| `policy` | `RetryPolicy | None` | 决定是否重试的回调。该字段仅在运行时使用,不会被序列化。 | -重试策略会收到一个 [`RetryPolicyContext`][agents.retry.RetryPolicyContext],其中包含: +重试策略会接收一个 [`RetryPolicyContext`][agents.retry.RetryPolicyContext],其中包含: -- `attempt` 和 `max_retries`,便于做与尝试次数相关的决策。 -- `stream`,便于区分流式与非流式行为。 -- `error`,用于原始错误检查。 +- `attempt` 和 `max_retries`,便于你基于尝试次数作决策。 +- `stream`,便于你区分流式与非流式行为。 +- `error`,用于原始检查。 - `normalized` 事实,如 `status_code`、`retry_after`、`error_code`、`is_network_error`、`is_timeout` 和 `is_abort`。 - 当底层模型适配器可提供重试建议时的 `provider_advice`。 -策略可返回: +策略可以返回: - `True` / `False`,用于简单重试决策。 - [`RetryDecision`][agents.retry.RetryDecision],用于覆盖延迟或附加诊断原因。 -SDK 在 `retry_policies` 中导出了现成辅助函数: +SDK 在 `retry_policies` 中导出可直接使用的辅助函数: | 辅助函数 | 行为 | | --- | --- | | `retry_policies.never()` | 始终不重试。 | | `retry_policies.provider_suggested()` | 在可用时遵循 provider 的重试建议。 | | `retry_policies.network_error()` | 匹配临时传输错误与超时失败。 | -| `retry_policies.http_status([...])` | 匹配指定的 HTTP 状态码。 | +| `retry_policies.http_status([...])` | 匹配选定的 HTTP 状态码。 | | `retry_policies.retry_after()` | 仅在存在 retry-after 提示时重试,并使用该延迟。 | -| `retry_policies.any(...)` | 任一嵌套策略选择重试时即重试。 | -| `retry_policies.all(...)` | 仅当所有嵌套策略都选择重试时才重试。 | +| `retry_policies.any(...)` | 当任一嵌套策略选择重试时重试。 | +| `retry_policies.all(...)` | 仅当每个嵌套策略都选择重试时重试。 | -组合策略时,`provider_suggested()` 是最安全的首个构建块,因为当 provider 可区分时,它能保留 provider 否决与重放安全批准。 +组合策略时,`provider_suggested()` 是最安全的首个构建块,因为当 provider 能区分否决与可重放安全批准时,它会保留这些信息。 ##### 安全边界 某些失败永远不会自动重试: -- Abort 错误。 -- provider 建议将重放标记为不安全的请求。 -- 在输出已开始且重放会不安全的流式运行中发生的错误。 +- 中止错误。 +- provider 建议标记为重放不安全的请求。 +- 在输出已开始且重放不安全的情况下进行的流式运行。 -使用 `previous_response_id` 或 `conversation_id` 的有状态后续请求也会更保守处理。对这类请求,仅有 `network_error()` 或 `http_status([500])` 等非 provider 谓词还不够。重试策略应包含来自 provider 的重放安全批准,通常通过 `retry_policies.provider_suggested()`。 +使用 `previous_response_id` 或 `conversation_id` 的有状态后续请求也会被更保守地处理。对这类请求,仅有 `network_error()` 或 `http_status([500])` 这类非 provider 谓词本身并不足够。重试策略应包含来自 provider 的可重放安全批准,通常通过 `retry_policies.provider_suggested()`。 -##### Runner 与智能体的合并行为 +##### Runner 与智能体合并行为 -`retry` 会在 runner 级和智能体级 `ModelSettings` 之间进行深度合并: +`retry` 会在 runner 级与智能体级 `ModelSettings` 间进行深度合并: -- 智能体可以只覆盖 `retry.max_retries`,并继续继承 runner 的 `policy`。 -- 智能体可以只覆盖 `retry.backoff` 的一部分,并保留 runner 中同级其他 backoff 字段。 -- `policy` 仅在运行时生效,因此序列化后的 `ModelSettings` 会保留 `max_retries` 与 `backoff`,但省略回调本身。 +- 智能体可以只覆盖 `retry.max_retries`,并继承 runner 的 `policy`。 +- 智能体可以只覆盖 `retry.backoff` 的部分字段,并保留 runner 中其余同级字段。 +- `policy` 仅运行时有效,因此序列化后的 `ModelSettings` 会保留 `max_retries` 与 `backoff`,但省略回调本身。 -更完整示例见 [`examples/basic/retry.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry.py) 和[基于适配器的重试示例](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry_litellm.py)。 +更完整示例见 [`examples/basic/retry.py`](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry.py) 和[由适配器支持的重试示例](https://github.com/openai/openai-agents-python/tree/main/examples/basic/retry_litellm.py)。 -## 非 OpenAI providers 故障排查 +## 非 OpenAI provider 故障排查 ### 追踪客户端错误 401 -如果你遇到与追踪相关的错误,这是因为 trace 会上传到 OpenAI 服务端,而你没有 OpenAI API key。你有三种解决方式: +如果你遇到与追踪相关的错误,这是因为 trace 会上传到 OpenAI 服务,而你没有 OpenAI API key。你有三种解决方式: 1. 完全禁用追踪:[`set_tracing_disabled(True)`][agents.set_tracing_disabled]。 -2. 为追踪设置 OpenAI key:[`set_tracing_export_api_key(...)`][agents.set_tracing_export_api_key]。该 API key 仅用于上传 trace,且必须来自 [platform.openai.com](https://platform.openai.com/)。 -3. 使用非 OpenAI 的 trace 进程。见[追踪文档](../tracing.md#custom-tracing-processors)。 +2. 为追踪设置 OpenAI key:[`set_tracing_export_api_key(...)`][agents.set_tracing_export_api_key]。此 API key 仅用于上传 trace,且必须来自 [platform.openai.com](https://platform.openai.com/)。 +3. 使用非 OpenAI 的追踪进程。见[追踪文档](../tracing.md#custom-tracing-processors)。 ### Responses API 支持 -SDK 默认使用 Responses API,但许多其他 LLM providers 仍不支持它。因此你可能会看到 404 或类似问题。你有两种解决方式: +SDK 默认使用 Responses API,但许多其他 LLM provider 仍不支持。你可能因此看到 404 或类似问题。可通过两种方式解决: -1. 调用 [`set_default_openai_api("chat_completions")`][agents.set_default_openai_api]。这适用于你通过环境变量设置 `OPENAI_API_KEY` 和 `OPENAI_BASE_URL` 的情况。 -2. 使用 [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel]。示例在[这里](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/)。 +1. 调用 [`set_default_openai_api("chat_completions")`][agents.set_default_openai_api]。当你通过环境变量设置 `OPENAI_API_KEY` 和 `OPENAI_BASE_URL` 时,此方式有效。 +2. 使用 [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel]。示例见[这里](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/)。 ### structured outputs 支持 -一些模型 provider 不支持 [structured outputs](https://platform.openai.com/docs/guides/structured-outputs)。这有时会导致如下错误: +某些模型 provider 不支持 [structured outputs](https://platform.openai.com/docs/guides/structured-outputs)。这有时会导致类似如下错误: ``` @@ -437,34 +448,34 @@ BadRequestError: Error code: 400 - {'error': {'message': "'response_format.type' ``` -这是一些模型 provider 的短板——它们支持 JSON 输出,但不允许你指定输出使用的 `json_schema`。我们正在修复此问题,但建议你依赖支持 JSON schema 输出的 provider,否则应用会经常因 JSON 格式错误而中断。 +这是某些模型 provider 的不足——它们支持 JSON 输出,但不允许你指定输出所用的 `json_schema`。我们正在修复这一点,但建议你依赖支持 JSON schema 输出的 provider,否则应用经常会因 JSON 格式错误而中断。 -## 跨 providers 混合模型 +## 跨 provider 混合模型 -你需要了解不同模型 provider 的功能差异,否则可能会遇到错误。例如,OpenAI 支持 structured outputs、多模态输入以及托管文件检索和网络检索,但许多其他 providers 不支持这些功能。请注意以下限制: +你需要了解各模型 provider 的功能差异,否则可能会遇到错误。例如,OpenAI 支持 structured outputs、多模态输入,以及托管的文件检索和网络检索,但许多其他 provider 不支持这些功能。请注意以下限制: - 不要向不支持的 provider 发送其无法理解的 `tools` -- 在调用纯文本模型前过滤掉多模态输入 -- 注意不支持结构化 JSON 输出的 providers 偶尔会产生无效 JSON。 +- 在调用仅文本模型前,过滤掉多模态输入 +- 注意不支持结构化 JSON 输出的 provider 偶尔会生成无效 JSON ## 第三方适配器 -仅当 SDK 的内置 provider 集成点不足时再使用第三方适配器。如果你在此 SDK 中仅使用 OpenAI 模型,优先使用内置 [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] 路径,而不是 Any-LLM 或 LiteLLM。第三方适配器适用于你需要将 OpenAI 模型与非 OpenAI providers 组合使用,或需要内置路径未提供的适配器管理 provider 覆盖或路由的场景。适配器在 SDK 与上游模型 provider 之间增加了一层兼容层,因此功能支持和请求语义会因 provider 而异。SDK 当前将 Any-LLM 和 LiteLLM 作为尽力支持的 beta 适配器集成。 +仅当 SDK 内置 provider 集成点不足时,再考虑第三方适配器。如果你仅在本 SDK 中使用 OpenAI 模型,优先选择内置 [`OpenAIResponsesModel`][agents.models.openai_responses.OpenAIResponsesModel] 路径,而非 Any-LLM 或 LiteLLM。第三方适配器适用于你需要将 OpenAI 模型与非 OpenAI provider 组合,或需要内置路径未提供的适配器管理 provider 覆盖或路由的情况。适配器会在 SDK 与上游模型 provider 之间增加一层兼容层,因此功能支持与请求语义会因 provider 而异。SDK 当前以尽力而为的 beta 形式集成了 Any-LLM 和 LiteLLM。 ### Any-LLM -Any-LLM 支持以尽力支持的 beta 形式提供,适用于你需要 Any-LLM 管理的 provider 覆盖或路由的场景。 +Any-LLM 支持以尽力而为的 beta 形式提供,适用于你需要 Any-LLM 管理的 provider 覆盖或路由的场景。 -根据上游 provider 路径,Any-LLM 可能使用 Responses API、兼容 Chat Completions 的 API,或 provider 特定兼容层。 +根据上游 provider 路径,Any-LLM 可能使用 Responses API、与 Chat Completions 兼容的 API,或 provider 特定的兼容层。 -如果你需要 Any-LLM,请安装 `openai-agents[any-llm]`,然后从 [`examples/model_providers/any_llm_auto.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/any_llm_auto.py) 或 [`examples/model_providers/any_llm_provider.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/any_llm_provider.py) 开始。你可以在 [`MultiProvider`][agents.MultiProvider] 中使用 `any-llm/...` 模型名,直接实例化 `AnyLLMModel`,或在运行范围使用 `AnyLLMProvider`。若需显式固定模型接口,可在构造 `AnyLLMModel` 时传入 `api="responses"` 或 `api="chat_completions"`。 +如果你需要 Any-LLM,请安装 `openai-agents[any-llm]`,然后从 [`examples/model_providers/any_llm_auto.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/any_llm_auto.py) 或 [`examples/model_providers/any_llm_provider.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/any_llm_provider.py) 开始。你可以在 [`MultiProvider`][agents.MultiProvider] 中使用 `any-llm/...` 模型名,直接实例化 `AnyLLMModel`,或在运行范围使用 `AnyLLMProvider`。如果你需要显式固定模型接口,请在构造 `AnyLLMModel` 时传入 `api="responses"` 或 `api="chat_completions"`。 -Any-LLM 仍是第三方适配器层,因此 provider 依赖与能力缺口由 Any-LLM 上游定义,而非 SDK 定义。当上游 provider 返回使用量指标时会自动透传;但流式 Chat Completions 后端可能需要先设置 `ModelSettings(include_usage=True)` 才会输出使用量分片。若你依赖 structured outputs、工具调用、使用量上报或 Responses 特定行为,请验证你计划部署的具体 provider 后端。 +Any-LLM 仍是第三方适配器层,因此 provider 依赖和能力缺口由 Any-LLM 上游定义,而非 SDK 定义。当上游 provider 返回使用量指标时会自动透传;但流式 Chat Completions 后端在发出使用量分块前可能需要 `ModelSettings(include_usage=True)`。如果你依赖 structured outputs、工具调用、使用量报告或 Responses 特定行为,请验证你计划部署的确切 provider 后端。 ### LiteLLM -LiteLLM 支持以尽力支持的 beta 形式提供,适用于你需要 LiteLLM 特定 provider 覆盖或路由的场景。 +LiteLLM 支持以尽力而为的 beta 形式提供,适用于你需要 LiteLLM 特定 provider 覆盖或路由的场景。 如果你需要 LiteLLM,请安装 `openai-agents[litellm]`,然后从 [`examples/model_providers/litellm_auto.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/litellm_auto.py) 或 [`examples/model_providers/litellm_provider.py`](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/litellm_provider.py) 开始。你可以使用 `litellm/...` 模型名,或直接实例化 [`LitellmModel`][agents.extensions.models.litellm_model.LitellmModel]。 -一些 LiteLLM 支持的 providers 默认不会填充 SDK 使用量指标。如果你需要使用量上报,请传入 `ModelSettings(include_usage=True)`,并在依赖 structured outputs、工具调用、使用量上报或适配器特定路由行为时,验证你计划部署的具体 provider 后端。 \ No newline at end of file +部分由 LiteLLM 支持的 provider 默认不会填充 SDK 使用量指标。如果你需要使用量报告,请传入 `ModelSettings(include_usage=True)`,并在你依赖 structured outputs、工具调用、使用量报告或适配器特定路由行为时,验证计划部署的确切 provider 后端。 \ No newline at end of file From ce0d7923b837edf07077900f4ac3a5791888288e Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Fri, 27 Mar 2026 16:58:52 +0900 Subject: [PATCH 19/40] docs: tweak for #2792 changes --- docs/ja/models/index.md | 4 ++-- docs/ko/models/index.md | 4 ++-- docs/models/index.md | 4 ++-- docs/zh/models/index.md | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/ja/models/index.md b/docs/ja/models/index.md index 981e501dd8..fff3d3dce7 100644 --- a/docs/ja/models/index.md +++ b/docs/ja/models/index.md @@ -208,8 +208,8 @@ from agents import Agent, AsyncOpenAI, OpenAIChatCompletionsModel, set_tracing_d set_tracing_disabled(disabled=True) -provider = AsyncOpenAI(api_key="Api_Key", base_url="Base URL of Provider") -model = OpenAIChatCompletionsModel(model="Model_Name", openai_client=provider) +client = AsyncOpenAI(api_key="Api_Key", base_url="Base URL of Provider") +model = OpenAIChatCompletionsModel(model="Model_Name", openai_client=client) agent= Agent(name="Helping Agent", instructions="You are a Helping Agent", model=model) ``` diff --git a/docs/ko/models/index.md b/docs/ko/models/index.md index 6f04b4aa95..a3f56d35ec 100644 --- a/docs/ko/models/index.md +++ b/docs/ko/models/index.md @@ -208,8 +208,8 @@ from agents import Agent, AsyncOpenAI, OpenAIChatCompletionsModel, set_tracing_d set_tracing_disabled(disabled=True) -provider = AsyncOpenAI(api_key="Api_Key", base_url="Base URL of Provider") -model = OpenAIChatCompletionsModel(model="Model_Name", openai_client=provider) +client = AsyncOpenAI(api_key="Api_Key", base_url="Base URL of Provider") +model = OpenAIChatCompletionsModel(model="Model_Name", openai_client=client) agent= Agent(name="Helping Agent", instructions="You are a Helping Agent", model=model) ``` diff --git a/docs/models/index.md b/docs/models/index.md index 021da1fb24..a838c9802c 100644 --- a/docs/models/index.md +++ b/docs/models/index.md @@ -204,8 +204,8 @@ from agents import Agent, AsyncOpenAI, OpenAIChatCompletionsModel, set_tracing_d set_tracing_disabled(disabled=True) -provider = AsyncOpenAI(api_key="Api_Key", base_url="Base URL of Provider") -model = OpenAIChatCompletionsModel(model="Model_Name", openai_client=provider) +client = AsyncOpenAI(api_key="Api_Key", base_url="Base URL of Provider") +model = OpenAIChatCompletionsModel(model="Model_Name", openai_client=client) agent= Agent(name="Helping Agent", instructions="You are a Helping Agent", model=model) ``` diff --git a/docs/zh/models/index.md b/docs/zh/models/index.md index 0dade618fa..f0acc191df 100644 --- a/docs/zh/models/index.md +++ b/docs/zh/models/index.md @@ -208,8 +208,8 @@ from agents import Agent, AsyncOpenAI, OpenAIChatCompletionsModel, set_tracing_d set_tracing_disabled(disabled=True) -provider = AsyncOpenAI(api_key="Api_Key", base_url="Base URL of Provider") -model = OpenAIChatCompletionsModel(model="Model_Name", openai_client=provider) +client = AsyncOpenAI(api_key="Api_Key", base_url="Base URL of Provider") +model = OpenAIChatCompletionsModel(model="Model_Name", openai_client=client) agent= Agent(name="Helping Agent", instructions="You are a Helping Agent", model=model) ``` From 9ed6dadb412b164da2055dcbd2121846f807c5d6 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Fri, 27 Mar 2026 21:04:49 +0900 Subject: [PATCH 20/40] chore: add Codex Stop hook for targeted Ruff tidy (#2795) --- .codex/config.toml | 4 + .codex/hooks.json | 15 +++ .codex/hooks/stop_repo_tidy.py | 222 +++++++++++++++++++++++++++++++++ 3 files changed, 241 insertions(+) create mode 100644 .codex/config.toml create mode 100644 .codex/hooks.json create mode 100644 .codex/hooks/stop_repo_tidy.py diff --git a/.codex/config.toml b/.codex/config.toml new file mode 100644 index 0000000000..b75aa36adb --- /dev/null +++ b/.codex/config.toml @@ -0,0 +1,4 @@ +#:schema https://developers.openai.com/codex/config-schema.json + +[features] +codex_hooks = true diff --git a/.codex/hooks.json b/.codex/hooks.json new file mode 100644 index 0000000000..082dde5ba9 --- /dev/null +++ b/.codex/hooks.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "uv run python \"$(git rev-parse --show-toplevel)/.codex/hooks/stop_repo_tidy.py\"", + "timeout": 20 + } + ] + } + ] + } +} diff --git a/.codex/hooks/stop_repo_tidy.py b/.codex/hooks/stop_repo_tidy.py new file mode 100644 index 0000000000..67e11d603a --- /dev/null +++ b/.codex/hooks/stop_repo_tidy.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import hashlib +import json +import subprocess +import sys +import tempfile +from dataclasses import asdict, dataclass +from pathlib import Path + +MAX_RUFF_FIX_FILES = 20 +PYTHON_SUFFIXES = {".py", ".pyi"} + + +@dataclass +class HookState: + last_tidy_fingerprint: str | None = None + + +def write_stop_block(reason: str, system_message: str) -> None: + sys.stdout.write( + json.dumps( + { + "decision": "block", + "reason": reason, + "systemMessage": system_message, + } + ) + ) + + +def run_command(cwd: str, *args: str) -> subprocess.CompletedProcess[str]: + try: + return subprocess.run( + args, + cwd=cwd, + capture_output=True, + check=False, + text=True, + ) + except FileNotFoundError as exc: + return subprocess.CompletedProcess(args, returncode=127, stdout="", stderr=str(exc)) + + +def run_git(cwd: str, *args: str) -> subprocess.CompletedProcess[str]: + return run_command(cwd, "git", *args) + + +def git_root(cwd: str) -> str: + result = run_git(cwd, "rev-parse", "--show-toplevel") + if result.returncode != 0: + raise RuntimeError(result.stderr.strip() or "git root lookup failed") + return result.stdout.strip() + + +def parse_status_paths(repo_root: str) -> list[str]: + unstaged = run_git(repo_root, "diff", "--name-only", "--diff-filter=ACMR") + untracked = run_git(repo_root, "ls-files", "--others", "--exclude-standard") + if unstaged.returncode != 0 or untracked.returncode != 0: + return [] + + paths = { + line.strip() + for result in (unstaged, untracked) + for line in result.stdout.splitlines() + if line.strip() + } + return sorted(paths) + + +def untracked_paths(repo_root: str, paths: list[str]) -> set[str]: + if not paths: + return set() + + result = run_git(repo_root, "ls-files", "--others", "--exclude-standard", "--", *paths) + if result.returncode != 0: + return set() + + return {line.strip() for line in result.stdout.splitlines() if line.strip()} + + +def fingerprint_for_paths(repo_root: str, paths: list[str]) -> str | None: + if not paths: + return None + + repo_root_path = Path(repo_root) + untracked = untracked_paths(repo_root, paths) + tracked_paths = [file_path for file_path in paths if file_path not in untracked] + diff_parts: list[str] = [] + + if tracked_paths: + diff = run_git(repo_root, "diff", "--no-ext-diff", "--binary", "--", *tracked_paths) + if diff.returncode == 0: + diff_parts.append(diff.stdout) + + for file_path in sorted(untracked): + try: + digest = hashlib.sha256((repo_root_path / file_path).read_bytes()).hexdigest() + except OSError: + continue + diff_parts.append(f"untracked:{file_path}:{digest}") + + if not diff_parts: + return None + + return hashlib.sha256("\n".join(diff_parts).encode("utf-8")).hexdigest() + + +def state_dir() -> Path: + return Path(tempfile.gettempdir()) / "openai-agents-python-codex-hooks" + + +def state_path(session_id: str, repo_root: str) -> Path: + root_hash = hashlib.sha256(repo_root.encode("utf-8")).hexdigest()[:12] + safe_session_id = "".join( + ch if ch.isascii() and (ch.isalnum() or ch in "._-") else "_" for ch in session_id + ) + return state_dir() / f"{safe_session_id}-{root_hash}.json" + + +def load_state(session_id: str, repo_root: str) -> HookState: + file_path = state_path(session_id, repo_root) + if not file_path.exists(): + return HookState() + + try: + payload = json.loads(file_path.read_text()) + except (OSError, json.JSONDecodeError): + return HookState() + + return HookState(last_tidy_fingerprint=payload.get("last_tidy_fingerprint")) + + +def save_state(session_id: str, repo_root: str, state: HookState) -> None: + file_path = state_path(session_id, repo_root) + file_path.parent.mkdir(parents=True, exist_ok=True) + file_path.write_text(json.dumps(asdict(state), indent=2)) + + +def lint_fix_paths(repo_root: str) -> list[str]: + return [ + file_path + for file_path in parse_status_paths(repo_root) + if Path(file_path).suffix in PYTHON_SUFFIXES + ] + + +def main() -> None: + try: + payload = json.loads(sys.stdin.read() or "null") + except json.JSONDecodeError: + return + + if not isinstance(payload, dict): + return + + session_id = payload.get("session_id") + cwd = payload.get("cwd") + if not isinstance(session_id, str) or not isinstance(cwd, str): + return + + if payload.get("stop_hook_active"): + return + + repo_root = git_root(cwd) + current_paths = lint_fix_paths(repo_root) + if not current_paths or len(current_paths) > MAX_RUFF_FIX_FILES: + return + + state = load_state(session_id, repo_root) + current_fingerprint = fingerprint_for_paths(repo_root, current_paths) + if current_fingerprint is None or state.last_tidy_fingerprint == current_fingerprint: + return + + format_result = run_command(repo_root, "uv", "run", "ruff", "format", "--", *current_paths) + check_result: subprocess.CompletedProcess[str] | None = None + if format_result.returncode == 0: + check_result = run_command( + repo_root, + "uv", + "run", + "ruff", + "check", + "--fix", + "--", + *current_paths, + ) + + if format_result.returncode != 0: + write_stop_block( + "`uv run ruff format -- ...` failed for the touched Python files. " + "Review the formatting step before wrapping up.", + "Repo hook: targeted Ruff format failed.", + ) + return + + if check_result and check_result.returncode != 0: + write_stop_block( + "`uv run ruff check --fix -- ...` failed for the touched Python files. " + "Review the lint output before wrapping up.", + "Repo hook: targeted Ruff lint fix failed.", + ) + return + + updated_paths = lint_fix_paths(repo_root) + updated_fingerprint = fingerprint_for_paths(repo_root, updated_paths) + state.last_tidy_fingerprint = updated_fingerprint + save_state(session_id, repo_root, state) + + if updated_fingerprint != current_fingerprint: + write_stop_block( + "I ran targeted tidy steps on the touched Python files " + "(`ruff format` and `ruff check --fix`). Review the updated diff, " + "then continue or wrap up.", + "Repo hook: ran targeted Ruff tidy on touched files.", + ) + + +if __name__ == "__main__": + main() From a7b4851e69c3e4de908b2858b792a58b0c316ce1 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Sat, 28 Mar 2026 11:02:39 +0900 Subject: [PATCH 21/40] fix: #2797 accept raw image_url content parts on chat completions input (#2799) --- src/agents/models/chatcmpl_converter.py | 36 ++++++++ src/agents/models/openai_chatcompletions.py | 6 +- tests/test_openai_chatcompletions.py | 84 +++++++++++++++++++ .../test_openai_chatcompletions_converter.py | 43 ++++++++++ 4 files changed, 168 insertions(+), 1 deletion(-) diff --git a/src/agents/models/chatcmpl_converter.py b/src/agents/models/chatcmpl_converter.py index 87e4afd7a7..60fa10b6ad 100644 --- a/src/agents/models/chatcmpl_converter.py +++ b/src/agents/models/chatcmpl_converter.py @@ -329,6 +329,41 @@ def extract_text_content( raise UserError(f"Only text content is supported here, got: {c}") return out + @classmethod + def _normalize_input_content_part_alias( + cls, + content_part: ResponseInputContentWithAudioParam, + ) -> ResponseInputContentWithAudioParam: + """Accept raw Chat Completions parts by mapping them to SDK canonical shapes.""" + if not isinstance(content_part, dict): + return content_part + + content_type = content_part.get("type") + if content_type == "text": + text = content_part.get("text") + if not isinstance(text, str): + raise UserError(f"Only text content is supported here, got: {content_part}") + # Cast the normalized dict because we are constructing a TypedDict alias by hand. + return cast(ResponseInputTextParam, {"type": "input_text", "text": text}) + + if content_type != "image_url": + return content_part + + image_payload = content_part.get("image_url") + if not isinstance(image_payload, dict): + raise UserError(f"Only image URLs are supported for image_url {content_part}") + + image_url = image_payload.get("url") + if not isinstance(image_url, str) or not image_url: + raise UserError(f"Only image URLs are supported for image_url {content_part}") + + normalized: dict[str, Any] = {"type": "input_image", "image_url": image_url} + detail = image_payload.get("detail") + if detail is not None: + normalized["detail"] = detail + # Cast the normalized dict because we are constructing a TypedDict alias by hand. + return cast(ResponseInputImageParam, normalized) + @classmethod def extract_all_content( cls, content: str | Iterable[ResponseInputContentWithAudioParam] @@ -338,6 +373,7 @@ def extract_all_content( out: list[ChatCompletionContentPartParam] = [] for c in content: + c = cls._normalize_input_content_part_alias(c) if isinstance(c, dict) and c.get("type") == "input_text": casted_text_param = cast(ResponseInputTextParam, c) out.append( diff --git a/src/agents/models/openai_chatcompletions.py b/src/agents/models/openai_chatcompletions.py index 5b48cb1707..454bd7afda 100644 --- a/src/agents/models/openai_chatcompletions.py +++ b/src/agents/models/openai_chatcompletions.py @@ -87,7 +87,11 @@ def _validate_official_openai_input_content_types( if not isinstance(part, dict): continue - content_type = part.get("type") + normalized_part = Converter._normalize_input_content_part_alias(part) + if not isinstance(normalized_part, dict): + continue + + content_type = normalized_part.get("type") if content_type in self._OFFICIAL_OPENAI_SUPPORTED_INPUT_CONTENT_TYPES: continue diff --git a/tests/test_openai_chatcompletions.py b/tests/test_openai_chatcompletions.py index 5a87efedb7..42fce10d1a 100644 --- a/tests/test_openai_chatcompletions.py +++ b/tests/test_openai_chatcompletions.py @@ -384,6 +384,90 @@ def __init__(self, completions: DummyCompletions) -> None: assert kwargs["stream_options"] is omit +@pytest.mark.allow_call_model_methods +@pytest.mark.asyncio +async def test_get_response_accepts_raw_chat_completions_image_content() -> None: + """ + Raw Chat Completions content parts should be accepted on the SDK input path + when using the Chat Completions backend. + """ + + class DummyCompletions: + def __init__(self) -> None: + self.kwargs: dict[str, Any] = {} + + async def create(self, **kwargs: Any) -> Any: + self.kwargs = kwargs + return chat + + class DummyClient: + def __init__(self, completions: DummyCompletions) -> None: + self.chat = type("_Chat", (), {"completions": completions})() + self.base_url = httpx.URL("https://api.openai.com/v1/") + + msg = ChatCompletionMessage(role="assistant", content="ok") + choice = Choice(index=0, finish_reason="stop", message=msg) + chat = ChatCompletion( + id="resp-id", + created=0, + model="fake", + object="chat.completion", + choices=[choice], + usage=None, + ) + completions = DummyCompletions() + dummy_client = DummyClient(completions) + model = OpenAIChatCompletionsModel(model="gpt-4", openai_client=dummy_client) # type: ignore[arg-type] + + await model.get_response( + system_instructions=None, + input=[ + # Cast the fixture because the raw chat-style alias is intentionally outside the + # canonical TypedDict shape that mypy expects for ordinary SDK inputs. + cast( + Any, + { + "role": "user", + "content": [ + {"type": "text", "text": "What is in this image?"}, + { + "type": "image_url", + "image_url": { + "url": "data:image/png;base64,AAAA", + "detail": "high", + }, + }, + ], + }, + ) + ], + model_settings=ModelSettings(), + tools=[], + output_schema=None, + handoffs=[], + tracing=ModelTracing.DISABLED, + previous_response_id=None, + conversation_id=None, + prompt=None, + ) + + assert completions.kwargs["messages"] == [ + { + "role": "user", + "content": [ + {"type": "text", "text": "What is in this image?"}, + { + "type": "image_url", + "image_url": { + "url": "data:image/png;base64,AAAA", + "detail": "high", + }, + }, + ], + } + ] + + @pytest.mark.asyncio async def test_fetch_response_stream(monkeypatch) -> None: """ diff --git a/tests/test_openai_chatcompletions_converter.py b/tests/test_openai_chatcompletions_converter.py index a00960b168..116a6e0767 100644 --- a/tests/test_openai_chatcompletions_converter.py +++ b/tests/test_openai_chatcompletions_converter.py @@ -140,6 +140,49 @@ def test_items_to_messages_with_easy_input_message(): assert out["content"] == "How are you?" +def test_items_to_messages_accepts_raw_chat_completions_user_content_parts(): + """ + Raw Chat Completions content parts should be accepted as aliases for the SDK's + canonical input content shapes. + """ + items: list[TResponseInputItem] = [ + # Cast the fixture because mypy cannot infer this raw chat-style dict as a specific + # member of the TResponseInputItem TypedDict union on its own. + cast( + TResponseInputItem, + { + "role": "user", + "content": [ + {"type": "text", "text": "What is in this image?"}, + { + "type": "image_url", + "image_url": { + "url": "https://example.com/image.png", + "detail": "high", + }, + }, + ], + }, + ) + ] + + messages = Converter.items_to_messages(items) + + assert len(messages) == 1 + message = messages[0] + assert message["role"] == "user" + assert message["content"] == [ + {"type": "text", "text": "What is in this image?"}, + { + "type": "image_url", + "image_url": { + "url": "https://example.com/image.png", + "detail": "high", + }, + }, + ] + + def test_items_to_messages_with_output_message_and_function_call(): """ Given a sequence of one ResponseOutputMessageParam followed by a From 9f5575ada4e7852a182bbe76f4ec21bb6e88268c Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Sat, 28 Mar 2026 11:06:03 +0900 Subject: [PATCH 22/40] fix: #2798 avoid stale hydrated input ids in server conversation tracker (#2800) --- src/agents/run_internal/oai_conversation.py | 3 +- tests/test_server_conversation_tracker.py | 35 +++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/agents/run_internal/oai_conversation.py b/src/agents/run_internal/oai_conversation.py index 44d0d1465b..0f6a9b1a6a 100644 --- a/src/agents/run_internal/oai_conversation.py +++ b/src/agents/run_internal/oai_conversation.py @@ -156,10 +156,11 @@ def hydrate_from_state( if isinstance(original_input, list): normalized_input = prepare_model_input_items(original_input) + # Hydrated initial input is reconstructed during resume, so object identity is not a + # stable dedupe key and can later collide with unrelated freshly allocated items. for item in ItemHelpers.input_to_new_input_list(normalized_input): if item is None: continue - self.sent_items.add(id(item)) item_id = _normalize_server_item_id( item.get("id") if isinstance(item, dict) else getattr(item, "id", None) ) diff --git a/tests/test_server_conversation_tracker.py b/tests/test_server_conversation_tracker.py index baafac6fda..408f0446c0 100644 --- a/tests/test_server_conversation_tracker.py +++ b/tests/test_server_conversation_tracker.py @@ -84,6 +84,41 @@ def test_prepare_input_filters_items_seen_by_server_and_tool_calls() -> None: assert tracker.remaining_initial_input is None +def test_hydrate_from_state_does_not_track_string_initial_input_by_object_identity() -> None: + tracker = OpenAIServerConversationTracker( + conversation_id="conv-init-string", previous_response_id=None + ) + + tracker.hydrate_from_state( + original_input="hello", + generated_items=[], + model_responses=[], + ) + + assert tracker.sent_items == set() + assert tracker.sent_initial_input is True + assert tracker.remaining_initial_input is None + assert len(tracker.sent_item_fingerprints) == 1 + + +def test_hydrate_from_state_does_not_track_list_initial_input_by_object_identity() -> None: + tracker = OpenAIServerConversationTracker( + conversation_id="conv-init-list", previous_response_id=None + ) + original_input = [cast(TResponseInputItem, {"role": "user", "content": "hello"})] + + tracker.hydrate_from_state( + original_input=original_input, + generated_items=[], + model_responses=[], + ) + + assert tracker.sent_items == set() + assert tracker.sent_initial_input is True + assert tracker.remaining_initial_input is None + assert len(tracker.sent_item_fingerprints) == 1 + + def test_mark_input_as_sent_and_rewind_input_respects_remaining_initial_input() -> None: tracker = OpenAIServerConversationTracker(conversation_id="conv2", previous_response_id=None) pending_1: TResponseInputItem = cast(TResponseInputItem, {"id": "p-1", "type": "message"}) From d86c2e27d9ec4897529abe67a4aa9006c8923e93 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:32:23 +0900 Subject: [PATCH 23/40] Release 0.13.3 (#2801) --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 37edaf05b6..59c5dd716f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openai-agents" -version = "0.13.2" +version = "0.13.3" description = "OpenAI Agents SDK" readme = "README.md" requires-python = ">=3.10" diff --git a/uv.lock b/uv.lock index b718f5de26..66a6b38a6f 100644 --- a/uv.lock +++ b/uv.lock @@ -1902,7 +1902,7 @@ wheels = [ [[package]] name = "openai-agents" -version = "0.13.2" +version = "0.13.3" source = { editable = "." } dependencies = [ { name = "griffelib" }, From 5e0e6d26f0f69e0c022f70dce89e5090f5e82a97 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Tue, 31 Mar 2026 19:30:19 +0900 Subject: [PATCH 24/40] fix: map extensions changes to the feature:extensions label (#2814) --- .github/codex/prompts/pr-labels.md | 4 ++-- .github/codex/schemas/pr-labels.json | 2 +- .github/scripts/pr_labels.py | 12 +++--------- tests/test_pr_labels.py | 20 ++++++++++++++++---- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/.github/codex/prompts/pr-labels.md b/.github/codex/prompts/pr-labels.md index d1f3d73a5e..ad67e9fd28 100644 --- a/.github/codex/prompts/pr-labels.md +++ b/.github/codex/prompts/pr-labels.md @@ -22,7 +22,7 @@ Allowed labels: - dependencies - feature:chat-completions - feature:core -- feature:lite-llm +- feature:extensions - feature:mcp - feature:realtime - feature:sessions @@ -46,7 +46,7 @@ Label rules: - bug vs enhancement: Prefer exactly one of these. Include both only when the PR clearly contains two separate substantial changes and both are first-order outcomes. - feature:chat-completions: Chat Completions support or conversion is a primary deliverable of the PR. Do not add it for a small compatibility guard or parity update in `chatcmpl_converter.py`. - feature:core: Core agent loop, tool calls, run pipeline, or other central runtime behavior is a primary surface of the PR. For cross-cutting runtime changes, this is usually the single best feature label. -- feature:lite-llm: LiteLLM adapter/provider behavior is a primary deliverable of the PR. +- feature:extensions: `src/agents/extensions/` surfaces are a primary deliverable of the PR, including extension models/providers such as Any-LLM and LiteLLM. - feature:mcp: MCP-specific behavior or APIs are a primary deliverable of the PR. Do not add it for incidental hosted/deferred tool plumbing touched by broader runtime work. - feature:realtime: Realtime-specific behavior, API shape, or session semantics are a primary deliverable of the PR. Do not add it for small parity updates in realtime adapters. - feature:sessions: Session or memory behavior is a primary deliverable of the PR. Do not add it for persistence updates that merely support a broader feature. diff --git a/.github/codex/schemas/pr-labels.json b/.github/codex/schemas/pr-labels.json index 4e8ed84e97..fde06d378b 100644 --- a/.github/codex/schemas/pr-labels.json +++ b/.github/codex/schemas/pr-labels.json @@ -15,7 +15,7 @@ "dependencies", "feature:chat-completions", "feature:core", - "feature:lite-llm", + "feature:extensions", "feature:mcp", "feature:realtime", "feature:sessions", diff --git a/.github/scripts/pr_labels.py b/.github/scripts/pr_labels.py index b0da296911..306b631364 100644 --- a/.github/scripts/pr_labels.py +++ b/.github/scripts/pr_labels.py @@ -19,7 +19,7 @@ "dependencies", "feature:chat-completions", "feature:core", - "feature:lite-llm", + "feature:extensions", "feature:mcp", "feature:realtime", "feature:sessions", @@ -41,11 +41,12 @@ FEATURE_LABELS: Final[set[str]] = ALLOWED_LABELS - DETERMINISTIC_LABELS - MODEL_ONLY_LABELS SOURCE_FEATURE_PREFIXES: Final[dict[str, tuple[str, ...]]] = { + "feature:extensions": ("src/agents/extensions/",), "feature:realtime": ("src/agents/realtime/",), "feature:voice": ("src/agents/voice/",), "feature:mcp": ("src/agents/mcp/",), "feature:tracing": ("src/agents/tracing/",), - "feature:sessions": ("src/agents/memory/", "src/agents/extensions/memory/"), + "feature:sessions": ("src/agents/memory/",), } CORE_EXCLUDED_PREFIXES: Final[tuple[str, ...]] = ( @@ -192,13 +193,6 @@ def infer_specific_feature_labels(changed_files: Sequence[str]) -> set[str]: ): labels.add("feature:chat-completions") - if any( - path.startswith(("src/agents/models/", "src/agents/extensions/models/")) - and "litellm" in path - for path in source_files - ): - labels.add("feature:lite-llm") - return labels diff --git a/tests/test_pr_labels.py b/tests/test_pr_labels.py index df4ad2c0f8..3cd205088a 100644 --- a/tests/test_pr_labels.py +++ b/tests/test_pr_labels.py @@ -40,12 +40,24 @@ def test_infer_fallback_labels_marks_core_for_runtime_changes() -> None: assert labels == {"feature:core"} -def test_infer_fallback_labels_marks_sessions_for_extensions_memory_changes() -> None: +def test_infer_fallback_labels_marks_extensions_for_extensions_memory_changes() -> None: labels = pr_labels.infer_fallback_labels( ["src/agents/extensions/memory/advanced_sqlite_session.py"] ) - assert labels == {"feature:sessions"} + assert labels == {"feature:extensions"} + + +def test_infer_fallback_labels_marks_extensions_for_litellm_changes() -> None: + labels = pr_labels.infer_fallback_labels(["src/agents/extensions/models/litellm_model.py"]) + + assert labels == {"feature:extensions"} + + +def test_infer_fallback_labels_marks_extensions_for_any_llm_changes() -> None: + labels = pr_labels.infer_fallback_labels(["src/agents/extensions/models/any_llm_model.py"]) + + assert labels == {"feature:extensions"} def test_compute_desired_labels_removes_stale_fallback_labels() -> None: @@ -108,7 +120,7 @@ def test_compute_desired_labels_infers_bug_from_fix_title() -> None: assert desired == {"bug", "feature:core"} -def test_compute_desired_labels_infers_sessions_for_extensions_memory_fix() -> None: +def test_compute_desired_labels_infers_extensions_for_extensions_memory_fix() -> None: desired = pr_labels.compute_desired_labels( pr_context=pr_labels.PRContext(title="fix(memory): honor custom table names"), changed_files=[ @@ -123,7 +135,7 @@ def test_compute_desired_labels_infers_sessions_for_extensions_memory_fix() -> N head_sha=None, ) - assert desired == {"bug", "feature:sessions"} + assert desired == {"bug", "feature:extensions"} def test_compute_managed_labels_preserves_model_only_labels_without_signal() -> None: From 40aada1a8be9c57d8e0d8037dbdebc97048d8d35 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Tue, 31 Mar 2026 19:35:22 +0900 Subject: [PATCH 25/40] fix: #2806 sanitize AnyLLM responses replay input before validation (#2813) --- src/agents/extensions/models/any_llm_model.py | 54 +++++-- tests/models/test_any_llm_model.py | 147 +++++++++++++++++- 2 files changed, 182 insertions(+), 19 deletions(-) diff --git a/src/agents/extensions/models/any_llm_model.py b/src/agents/extensions/models/any_llm_model.py index 5302e49779..e135768a73 100644 --- a/src/agents/extensions/models/any_llm_model.py +++ b/src/agents/extensions/models/any_llm_model.py @@ -813,7 +813,7 @@ async def _fetch_responses_response( list_input = ItemHelpers.input_to_new_input_list(input) list_input = _to_dump_compatible(list_input) - list_input = self._remove_openai_responses_api_incompatible_fields(list_input) + list_input = self._sanitize_any_llm_responses_input(list_input) parallel_tool_calls = ( True @@ -1095,31 +1095,51 @@ def _make_any_llm_responses_params(payload: dict[str, Any]) -> Any: AnyLLMResponsesParams = any_llm_responses.ResponsesParams return AnyLLMResponsesParams(**payload) - def _remove_openai_responses_api_incompatible_fields(self, list_input: list[Any]) -> list[Any]: - has_provider_data = any( - isinstance(item, dict) and item.get("provider_data") for item in list_input - ) - if not has_provider_data: - return list_input + def _sanitize_any_llm_responses_input(self, list_input: list[Any]) -> list[Any]: + """Normalize replayed Responses input into a shape accepted by any-llm. + any-llm validates replayed items against OpenAI-style input models before the request is + handed to the underlying provider. SDK-produced replay items can legitimately carry + adapter-only fields such as provider_data or explicit nulls like status=None, which those + models reject. Strip those fields here while preserving valid replay content. + """ result: list[Any] = [] for item in list_input: - cleaned = self._clean_item_for_openai(item) + cleaned = self._sanitize_any_llm_responses_value(item) if cleaned is not None: result.append(cleaned) return result - def _clean_item_for_openai(self, item: Any) -> Any | None: - if not isinstance(item, dict): - return item + def _sanitize_any_llm_responses_value(self, value: Any) -> Any | None: + if isinstance(value, list): + sanitized_list = [] + for item in value: + cleaned_item = self._sanitize_any_llm_responses_value(item) + if cleaned_item is not None: + sanitized_list.append(cleaned_item) + return sanitized_list + + if not isinstance(value, dict): + return value - if item.get("type") == "reasoning" and item.get("provider_data"): + # Provider-specific reasoning payloads are not replay-safe across adapter boundaries. + if value.get("type") == "reasoning" and value.get("provider_data"): return None - if item.get("id") == FAKE_RESPONSES_ID: - del item["id"] - if "provider_data" in item: - del item["provider_data"] - return item + + cleaned: dict[str, Any] = {} + for key, item_value in value.items(): + if key == "provider_data": + continue + if key == "id" and item_value == FAKE_RESPONSES_ID: + continue + if item_value is None: + continue + + sanitized = self._sanitize_any_llm_responses_value(item_value) + if sanitized is not None: + cleaned[key] = sanitized + + return cleaned def _attach_logprobs_to_output(self, output_items: list[Any], logprobs: list[Any]) -> None: from openai.types.responses import ResponseOutputMessage, ResponseOutputText diff --git a/tests/models/test_any_llm_model.py b/tests/models/test_any_llm_model.py index b65b206668..69fdd8ab67 100644 --- a/tests/models/test_any_llm_model.py +++ b/tests/models/test_any_llm_model.py @@ -4,7 +4,7 @@ import sys import types as pytypes from collections.abc import AsyncIterator -from typing import Any +from typing import Any, Literal, cast import pytest from openai.types.chat import ( @@ -25,9 +25,18 @@ ) from pydantic import BaseModel -from agents import ModelSettings, ModelTracing, __version__ +from agents import ( + Agent, + Handoff, + ModelSettings, + ModelTracing, + Tool, + TResponseInputItem, + __version__, +) from agents.exceptions import UserError from agents.models.chatcmpl_helpers import HEADERS_OVERRIDE +from agents.models.fake_id import FAKE_RESPONSES_ID class FakeAnyLLMProvider: @@ -583,6 +592,140 @@ async def test_any_llm_prompt_requests_fail_fast(monkeypatch) -> None: ) +def test_any_llm_responses_input_sanitizer_strips_none_fields_from_reasoning_items() -> None: + pytest.importorskip( + "any_llm", + reason="`any-llm-sdk` is only available when the optional dependency is installed.", + ) + from agents.extensions.models.any_llm_model import AnyLLMModel + + model = AnyLLMModel(model="openai/gpt-5.4-mini") + raw_input = [ + { + "id": "rid1", + "summary": [{"text": "why", "type": "summary_text"}], + "type": "reasoning", + "content": [{"type": "reasoning_text", "text": "thinking"}], + "status": None, + "encrypted_content": None, + } + ] + + cleaned = model._sanitize_any_llm_responses_input(raw_input) + + assert cleaned == [ + { + "id": "rid1", + "summary": [{"text": "why", "type": "summary_text"}], + "type": "reasoning", + "content": [{"type": "reasoning_text", "text": "thinking"}], + } + ] + + ResponsesParams = importlib.import_module("any_llm.types.responses").ResponsesParams + params = ResponsesParams(model="dummy", input=cleaned) + assert isinstance(params.input, list) + + +@pytest.mark.allow_call_model_methods +@pytest.mark.asyncio +async def test_any_llm_responses_path_sanitizes_replayed_items_before_validation() -> None: + pytest.importorskip( + "any_llm", + reason="`any-llm-sdk` is only available when the optional dependency is installed.", + ) + from agents.extensions.models.any_llm_model import AnyLLMModel + + class ValidatingProvider: + SUPPORTS_RESPONSES = True + + def __init__(self) -> None: + self.private_responses_calls: list[dict[str, Any]] = [] + + async def aresponses(self, **kwargs: Any) -> Any: + raise AssertionError("public aresponses path should not be used in this test") + + async def _aresponses(self, params: Any, **kwargs: Any) -> Response: + self.private_responses_calls.append({"params": params, "kwargs": kwargs}) + return _response("Hello from sanitized replay") + + class TestAnyLLMModel(AnyLLMModel): + def __init__(self, provider: ValidatingProvider) -> None: + super().__init__(model="openai/gpt-5.4-mini", api="responses") + self._provider = provider + + def _get_provider(self) -> Any: + return self._provider + + provider = ValidatingProvider() + model = TestAnyLLMModel(provider) + tools: list[Tool] = [] + handoffs: list[Handoff[Any, Agent[Any]]] = [] + stream_flag: Literal[False] = False + + replay_input = cast( + list[TResponseInputItem], + [ + {"role": "user", "content": "What's the weather in Tokyo?"}, + { + "id": FAKE_RESPONSES_ID, + "summary": [ + {"text": "I should call the weather tool first.", "type": "summary_text"} + ], + "type": "reasoning", + "content": [{"type": "reasoning_text", "text": "thinking"}], + "status": None, + "provider_data": {"model": "anthropic/fake-responses-model"}, + }, + { + "id": FAKE_RESPONSES_ID, + "arguments": '{"city": "Tokyo"}', + "call_id": "call_weather_123", + "name": "get_weather", + "type": "function_call", + "status": None, + "provider_data": {"model": "anthropic/fake-responses-model"}, + }, + { + "type": "function_call_output", + "call_id": "call_weather_123", + "output": "The weather in Tokyo is sunny and 22°C.", + }, + ], + ) + + response = await model._fetch_responses_response( + system_instructions=None, + input=replay_input, + model_settings=ModelSettings(), + tools=tools, + output_schema=None, + handoffs=handoffs, + previous_response_id=None, + conversation_id=None, + stream=stream_flag, + prompt=None, + ) + + assert response.id == "resp_123" + assert len(provider.private_responses_calls) == 1 + params = provider.private_responses_calls[0]["params"] + assert params.input == [ + {"role": "user", "content": "What's the weather in Tokyo?"}, + { + "arguments": '{"city": "Tokyo"}', + "call_id": "call_weather_123", + "name": "get_weather", + "type": "function_call", + }, + { + "type": "function_call_output", + "call_id": "call_weather_123", + "output": "The weather in Tokyo is sunny and 22°C.", + }, + ] + + def test_any_llm_provider_passes_api_override() -> None: pytest.importorskip( "any_llm", From 051c2ea388fabe74e7fcea5dd7989adfb65484e4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 11:37:18 +0900 Subject: [PATCH 26/40] Release 0.13.4 (#2815) --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 59c5dd716f..015ca452e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openai-agents" -version = "0.13.3" +version = "0.13.4" description = "OpenAI Agents SDK" readme = "README.md" requires-python = ">=3.10" diff --git a/uv.lock b/uv.lock index 66a6b38a6f..f3471e2c85 100644 --- a/uv.lock +++ b/uv.lock @@ -1902,7 +1902,7 @@ wheels = [ [[package]] name = "openai-agents" -version = "0.13.3" +version = "0.13.4" source = { editable = "." } dependencies = [ { name = "griffelib" }, From 976bccedc476612ea22b39c4d1b1c9ee4bd6003f Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Wed, 1 Apr 2026 14:14:01 +0900 Subject: [PATCH 27/40] feat: #2807 support callable approval policies for local MCP servers (#2818) --- src/agents/mcp/__init__.py | 2 + src/agents/mcp/server.py | 38 +++++++++++-- tests/mcp/test_mcp_approval.py | 98 +++++++++++++++++++++++++++++++++- tests/mcp/test_mcp_util.py | 72 +++++++++++++++++++++++++ 4 files changed, 204 insertions(+), 6 deletions(-) diff --git a/src/agents/mcp/__init__.py b/src/agents/mcp/__init__.py index 49249ca545..923af01d41 100644 --- a/src/agents/mcp/__init__.py +++ b/src/agents/mcp/__init__.py @@ -1,6 +1,7 @@ try: from .manager import MCPServerManager from .server import ( + LocalMCPApprovalCallable, MCPServer, MCPServerSse, MCPServerSseParams, @@ -32,6 +33,7 @@ "MCPServerStreamableHttp", "MCPServerStreamableHttpParams", "MCPServerManager", + "LocalMCPApprovalCallable", "MCPUtil", "MCPToolMetaContext", "MCPToolMetaResolver", diff --git a/src/agents/mcp/server.py b/src/agents/mcp/server.py index abbb5e087f..b8c7a69d04 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -63,13 +63,31 @@ class RequireApprovalObject(TypedDict, total=False): RequireApprovalPolicy = Literal["always", "never"] RequireApprovalMapping = dict[str, RequireApprovalPolicy] +if TYPE_CHECKING: + LocalMCPApprovalCallable = Callable[ + [RunContextWrapper[Any], "AgentBase", MCPTool], + MaybeAwaitable[bool], + ] +else: + LocalMCPApprovalCallable = Callable[..., Any] + if TYPE_CHECKING: RequireApprovalSetting = ( - RequireApprovalPolicy | RequireApprovalObject | RequireApprovalMapping | bool | None + RequireApprovalPolicy + | RequireApprovalObject + | RequireApprovalMapping + | LocalMCPApprovalCallable + | bool + | None ) else: RequireApprovalSetting = Union[ # noqa: UP007 - RequireApprovalPolicy, RequireApprovalObject, RequireApprovalMapping, bool, None + RequireApprovalPolicy, + RequireApprovalObject, + RequireApprovalMapping, + LocalMCPApprovalCallable, + bool, + None, ] @@ -220,8 +238,10 @@ def __init__( default will cause duplicate content. You can set this to True if you know the server will not duplicate the structured content in the `tool_result.content`. require_approval: Approval policy for tools on this server. Accepts "always"/"never", - a dict of tool names to those values, a boolean, or an object with always/never - tool lists (mirroring TS requireApproval). Normalized into a needs_approval policy. + a dict of tool names to those values, a boolean, an object with always/never + tool lists (mirroring TS requireApproval), or a sync/async callable that receives + `(run_context, agent, tool)` and returns whether the tool call needs approval. + Normalized into a needs_approval policy. failure_error_function: Optional function used to convert MCP tool failures into a model-visible error message. If explicitly set to None, tool errors will be raised instead of converted. If left unset, the agent-level configuration (or @@ -408,6 +428,9 @@ def _is_tool_list_schema(value: object) -> bool: tool_mapping[str(name)] = _to_bool(value) return tool_mapping + if callable(require_approval): + return require_approval + if isinstance(require_approval, bool): return require_approval @@ -418,7 +441,12 @@ def _get_needs_approval_for_tool( tool: MCPTool, agent: AgentBase | None, ) -> bool | Callable[[RunContextWrapper[Any], dict[str, Any], str], Awaitable[bool]]: - """Return a FunctionTool.needs_approval value for a given MCP tool.""" + """Return a FunctionTool.needs_approval value for a given MCP tool. + + Legacy callers may omit ``agent`` when using ``MCPUtil.to_function_tool()`` directly. + When approval is configured with a callable policy and no agent is available, this method + returns ``True`` to preserve the historical fail-closed behavior. + """ policy = self._needs_approval_policy diff --git a/tests/mcp/test_mcp_approval.py b/tests/mcp/test_mcp_approval.py index 99f0f60a75..1e99ff795f 100644 --- a/tests/mcp/test_mcp_approval.py +++ b/tests/mcp/test_mcp_approval.py @@ -1,6 +1,9 @@ +import asyncio + import pytest +from mcp.types import Tool as MCPTool -from agents import Agent, Runner +from agents import Agent, RunContextWrapper, Runner from ..fake_model import FakeModel from ..test_responses import get_function_tool_call, get_text_message @@ -122,3 +125,96 @@ async def test_mcp_require_approval_mapping_allows_policy_keyword_tool_names(): second = await Runner.run(agent, "call never") assert not second.interruptions, "tool named 'never' should not require approval" + + +@pytest.mark.asyncio +async def test_mcp_require_approval_callable_can_allow_and_block_by_tool_name(): + """Callable policies should decide approval dynamically for each MCP tool.""" + + seen: list[str] = [] + + def require_approval( + _run_context: RunContextWrapper[object | None], + _agent: Agent, + tool: MCPTool, + ) -> bool: + seen.append(tool.name) + return tool.name == "guarded" + + server = FakeMCPServer(require_approval=require_approval) + server.add_tool("guarded", {"type": "object", "properties": {}}) + server.add_tool("safe", {"type": "object", "properties": {}}) + + model = FakeModel() + agent = Agent(name="TestAgent", model=model, mcp_servers=[server]) + + queue_function_call_and_text( + model, + get_function_tool_call("guarded", "{}"), + followup=[get_text_message("guarded done")], + ) + first = await Runner.run(agent, "call guarded") + assert first.interruptions, "guarded should require approval via callable policy" + assert first.interruptions[0].tool_name == "guarded" + + resumed = await resume_after_first_approval(agent, first, always_approve=True) + assert resumed.final_output == "guarded done" + + queue_function_call_and_text( + model, + get_function_tool_call("safe", "{}"), + followup=[get_text_message("safe done")], + ) + second = await Runner.run(agent, "call safe") + assert not second.interruptions, "safe should bypass approval via callable policy" + assert second.final_output == "safe done" + + assert seen == ["guarded", "guarded", "safe"] + + +@pytest.mark.asyncio +async def test_mcp_require_approval_async_callable_uses_run_context(): + """Async callable policies should receive the run context and be awaited.""" + + seen_contexts: list[object | None] = [] + + async def require_approval( + run_context: RunContextWrapper[dict[str, bool] | None], + _agent: Agent, + _tool, + ) -> bool: + seen_contexts.append(run_context.context) + await asyncio.sleep(0) + return bool(run_context.context and run_context.context.get("needs_approval")) + + server = FakeMCPServer(require_approval=require_approval) + server.add_tool("conditional", {"type": "object", "properties": {}}) + + model = FakeModel() + agent = Agent(name="TestAgent", model=model, mcp_servers=[server]) + + queue_function_call_and_text( + model, + get_function_tool_call("conditional", "{}"), + followup=[get_text_message("approved path")], + ) + first = await Runner.run(agent, "call conditional", context={"needs_approval": True}) + assert first.interruptions, "run context should be able to trigger approval" + + resumed = await resume_after_first_approval(agent, first, always_approve=True) + assert resumed.final_output == "approved path" + + queue_function_call_and_text( + model, + get_function_tool_call("conditional", "{}"), + followup=[get_text_message("no approval path")], + ) + second = await Runner.run(agent, "call conditional", context={"needs_approval": False}) + assert not second.interruptions, "run context should be able to skip approval" + assert second.final_output == "no approval path" + + assert seen_contexts == [ + {"needs_approval": True}, + {"needs_approval": True}, + {"needs_approval": False}, + ] diff --git a/tests/mcp/test_mcp_util.py b/tests/mcp/test_mcp_util.py index 5a9cbd140c..c992e25e03 100644 --- a/tests/mcp/test_mcp_util.py +++ b/tests/mcp/test_mcp_util.py @@ -676,6 +676,78 @@ def require_approval( assert function_tool.needs_approval is True +@pytest.mark.asyncio +async def test_to_function_tool_callable_policy_uses_agent_and_tool(): + """Callable require_approval policies should bridge into FunctionTool.needs_approval.""" + + captured: dict[str, Any] = {} + + def require_approval( + run_context: RunContextWrapper[Any], + agent: Agent, + tool: MCPTool, + ) -> bool: + captured["run_context"] = run_context + captured["agent"] = agent + captured["tool"] = tool + return tool.name == "guarded_tool" + + server = FakeMCPServer(require_approval=require_approval) + tool = MCPTool(name="guarded_tool", inputSchema={}) + agent = Agent(name="test-agent") + + function_tool = MCPUtil.to_function_tool( + tool, + server, + convert_schemas_to_strict=False, + agent=agent, + ) + + assert callable(function_tool.needs_approval) + + run_context = RunContextWrapper(context={"request_id": "req_123"}) + needs_approval = await function_tool.needs_approval(run_context, {}, "call_123") + + assert needs_approval is True + assert captured["run_context"] is run_context + assert captured["agent"] is agent + assert captured["tool"].name == "guarded_tool" + + +@pytest.mark.asyncio +async def test_to_function_tool_async_callable_policy_is_awaited(): + """Async require_approval policies should be awaited before tool execution.""" + + async def require_approval( + _run_context: RunContextWrapper[Any], + _agent: Agent, + tool: MCPTool, + ) -> bool: + await asyncio.sleep(0) + return tool.name == "async_guarded_tool" + + server = FakeMCPServer(require_approval=require_approval) + tool = MCPTool(name="async_guarded_tool", inputSchema={}) + agent = Agent(name="test-agent") + + function_tool = MCPUtil.to_function_tool( + tool, + server, + convert_schemas_to_strict=False, + agent=agent, + ) + + assert callable(function_tool.needs_approval) + + needs_approval = await function_tool.needs_approval( + RunContextWrapper(context=None), + {}, + "call_async_123", + ) + + assert needs_approval is True + + @pytest.mark.asyncio async def test_mcp_tool_failure_error_function_agent_default(): """Agent-level failure_error_function should handle MCP tool failures.""" From 70939dc73d9779421371a02496889002589664b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:14:36 +0900 Subject: [PATCH 28/40] chore(deps): bump openai/codex-action from 1.4 to 1.6 (#2819) --- .github/workflows/pr-labels.yml | 2 +- .github/workflows/release-pr-update.yml | 2 +- .github/workflows/release-pr.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index 1462c204ef..6d5b0ad511 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -124,7 +124,7 @@ jobs: - name: Run Codex labeling id: run_codex if: ${{ (github.event_name == 'workflow_dispatch' || steps.pr.outputs.is_fork != 'true') && github.actor != 'dependabot[bot]' }} - uses: openai/codex-action@086169432f1d2ab2f4057540b1754d550f6a1189 + uses: openai/codex-action@c25d10f3f498316d4b2496cc4c6dd58057a7b031 with: openai-api-key: ${{ secrets.PROD_OPENAI_API_KEY }} prompt-file: .github/codex/prompts/pr-labels.md diff --git a/.github/workflows/release-pr-update.yml b/.github/workflows/release-pr-update.yml index 5319ac9753..72333e3ea5 100644 --- a/.github/workflows/release-pr-update.yml +++ b/.github/workflows/release-pr-update.yml @@ -74,7 +74,7 @@ jobs: echo "output_file=${output_file}" >> "$GITHUB_OUTPUT" - name: Run Codex release review if: steps.find.outputs.found == 'true' - uses: openai/codex-action@086169432f1d2ab2f4057540b1754d550f6a1189 + uses: openai/codex-action@c25d10f3f498316d4b2496cc4c6dd58057a7b031 with: openai-api-key: ${{ secrets.PROD_OPENAI_API_KEY }} prompt-file: .github/codex/prompts/release-review.md diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 7a29d537de..1f8a68a036 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -101,7 +101,7 @@ jobs: mkdir -p "$output_dir" echo "output_file=${output_file}" >> "$GITHUB_OUTPUT" - name: Run Codex release review - uses: openai/codex-action@086169432f1d2ab2f4057540b1754d550f6a1189 + uses: openai/codex-action@c25d10f3f498316d4b2496cc4c6dd58057a7b031 with: openai-api-key: ${{ secrets.PROD_OPENAI_API_KEY }} prompt-file: .github/codex/prompts/release-review.md From c9400550a78369495952215007dfd0229aa2270c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:14:49 +0900 Subject: [PATCH 29/40] chore(deps): bump astral-sh/setup-uv from 7.3.1 to 8.0.0 (#2820) --- .github/workflows/docs.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/release-pr.yml | 2 +- .github/workflows/tests.yml | 8 ++++---- .github/workflows/update-docs.yml | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index cf4aa44e18..ae9cb0387e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -36,7 +36,7 @@ jobs: fi - name: Setup uv if: steps.docs-only.outputs.skip != 'true' - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 with: enable-cache: true - name: Install dependencies diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index de0dd3d592..6b8a6776ed 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,7 +23,7 @@ jobs: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Setup uv - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 with: enable-cache: true - name: Install dependencies diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 1f8a68a036..4653ebd6df 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -21,7 +21,7 @@ jobs: fetch-depth: 0 ref: main - name: Setup uv - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 with: enable-cache: true - name: Fetch tags diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 49f5821913..8f9847765e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,7 +24,7 @@ jobs: run: ./.github/scripts/detect-changes.sh code "${{ github.event.pull_request.base.sha || github.event.before }}" "${{ github.sha }}" - name: Setup uv if: steps.changes.outputs.run == 'true' - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 with: enable-cache: true - name: Install dependencies @@ -50,7 +50,7 @@ jobs: run: ./.github/scripts/detect-changes.sh code "${{ github.event.pull_request.base.sha || github.event.before }}" "${{ github.sha }}" - name: Setup uv if: steps.changes.outputs.run == 'true' - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 with: enable-cache: true - name: Install dependencies @@ -84,7 +84,7 @@ jobs: run: ./.github/scripts/detect-changes.sh code "${{ github.event.pull_request.base.sha || github.event.before }}" "${{ github.sha }}" - name: Setup uv if: steps.changes.outputs.run == 'true' - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 with: enable-cache: true python-version: ${{ matrix.python-version }} @@ -116,7 +116,7 @@ jobs: run: ./.github/scripts/detect-changes.sh docs "${{ github.event.pull_request.base.sha || github.event.before }}" "${{ github.sha }}" - name: Setup uv if: steps.changes.outputs.run == 'true' - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 with: enable-cache: true - name: Install dependencies diff --git a/.github/workflows/update-docs.yml b/.github/workflows/update-docs.yml index 7292f2d09c..717f5e39f5 100644 --- a/.github/workflows/update-docs.yml +++ b/.github/workflows/update-docs.yml @@ -48,7 +48,7 @@ jobs: with: fetch-depth: 0 - name: Setup uv - uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 with: enable-cache: true - name: Install dependencies From f75a40ebe25789705866ba2a03491a579930bd85 Mon Sep 17 00:00:00 2001 From: Abdullah Faheem Date: Thu, 2 Apr 2026 10:49:40 +0500 Subject: [PATCH 30/40] docs: sync examples.md with current examples directory (#2827) --- docs/examples.md | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/docs/examples.md b/docs/examples.md index 8c353aa3de..9fda81c382 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -9,12 +9,17 @@ Check out a variety of sample implementations of the SDK in the examples section - Deterministic workflows - Agents as tools + - Agents as tools with streaming events (`examples/agent_patterns/agents_as_tools_streaming.py`) + - Agents as tools with structured input parameters (`examples/agent_patterns/agents_as_tools_structured.py`) - Parallel agent execution - Conditional tool usage + - Forcing tool use with different behaviors (`examples/agent_patterns/forcing_tool_use.py`) - Input/output guardrails - LLM as a judge - Routing - Streaming guardrails + - Human-in-the-loop with tool approval and state serialization (`examples/agent_patterns/human_in_the_loop.py`) + - Human-in-the-loop with streaming (`examples/agent_patterns/human_in_the_loop_stream.py`) - Custom rejection messages for approval flows (`examples/agent_patterns/human_in_the_loop_custom_rejection.py`) - **[basic](https://github.com/openai/openai-agents-python/tree/main/examples/basic):** @@ -22,7 +27,11 @@ Check out a variety of sample implementations of the SDK in the examples section - Hello world examples (Default model, GPT-5, open-weight model) - Agent lifecycle management + - Run hooks and agent hooks lifecycle example (`examples/basic/lifecycle_example.py`) - Dynamic system prompts + - Basic tool usage (`examples/basic/tools.py`) + - Tool input/output guardrails (`examples/basic/tool_guardrails.py`) + - Image tool output (`examples/basic/image_tool_output.py`) - Streaming outputs (text, items, function call args) - Responses websocket transport with a shared session helper across turns (`examples/basic/stream_ws.py`) - Prompt templates @@ -40,10 +49,18 @@ Check out a variety of sample implementations of the SDK in the examples section A financial research agent that demonstrates structured research workflows with agents and tools for financial data analysis. - **[handoffs](https://github.com/openai/openai-agents-python/tree/main/examples/handoffs):** - See practical examples of agent handoffs with message filtering. + Practical examples of agent handoffs with message filtering, including: + + - Message filter example (`examples/handoffs/message_filter.py`) + - Message filter with streaming (`examples/handoffs/message_filter_streaming.py`) - **[hosted_mcp](https://github.com/openai/openai-agents-python/tree/main/examples/hosted_mcp):** - Examples demonstrating how to use hosted MCP (Model Context Protocol) connectors and approvals. + Examples demonstrating how to use hosted MCP (Model Context Protocol) with the OpenAI Responses API, including: + + - Simple hosted MCP without approval (`examples/hosted_mcp/simple.py`) + - MCP connectors such as Google Calendar (`examples/hosted_mcp/connectors.py`) + - Human-in-the-loop with interruption-based approvals (`examples/hosted_mcp/human_in_the_loop.py`) + - On-approval callback for MCP tool calls (`examples/hosted_mcp/on_approval.py`) - **[mcp](https://github.com/openai/openai-agents-python/tree/main/examples/mcp):** Learn how to build agents with MCP (Model Context Protocol), including: @@ -52,7 +69,13 @@ Check out a variety of sample implementations of the SDK in the examples section - Git examples - MCP prompt server examples - SSE (Server-Sent Events) examples + - SSE remote server connection (`examples/mcp/sse_remote_example`) - Streamable HTTP examples + - Streamable HTTP remote connection (`examples/mcp/streamable_http_remote_example`) + - Custom HTTP client factory for Streamable HTTP (`examples/mcp/streamablehttp_custom_client_example`) + - Prefetching all MCP tools with `MCPUtil.get_all_function_tools` (`examples/mcp/get_all_mcp_tools_example`) + - MCPServerManager with FastAPI (`examples/mcp/manager_example`) + - MCP tool filtering (`examples/mcp/tool_filter_example`) - **[memory](https://github.com/openai/openai-agents-python/tree/main/examples/memory):** Examples of different memory implementations for agents, including: @@ -66,6 +89,11 @@ Check out a variety of sample implementations of the SDK in the examples section - OpenAI Conversations session storage - Responses compaction session storage - Stateless Responses compaction with `ModelSettings(store=False)` (`examples/memory/compaction_session_stateless_example.py`) + - File-backed session storage (`examples/memory/file_session.py`) + - File-backed session with human-in-the-loop (`examples/memory/file_hitl_example.py`) + - SQLite in-memory session with human-in-the-loop (`examples/memory/memory_session_hitl_example.py`) + - OpenAI Conversations session with human-in-the-loop (`examples/memory/openai_session_hitl_example.py`) + - HITL approval/rejection scenario across sessions (`examples/memory/hitl_session_scenario.py`) - **[model_providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers):** Explore how to use non-OpenAI models with the SDK, including custom providers and third-party adapters. @@ -79,7 +107,11 @@ Check out a variety of sample implementations of the SDK in the examples section - Twilio SIP integration using Realtime Calls API attach flows - **[reasoning_content](https://github.com/openai/openai-agents-python/tree/main/examples/reasoning_content):** - Examples demonstrating how to work with reasoning content and structured outputs. + Examples demonstrating how to work with reasoning content, including: + + - Reasoning content with the Runner API, streaming and non-streaming (`examples/reasoning_content/runner_example.py`) + - Reasoning content with OSS models via OpenRouter (`examples/reasoning_content/gpt_oss_stream.py`) + - Basic reasoning content example (`examples/reasoning_content/main.py`) - **[research_bot](https://github.com/openai/openai-agents-python/tree/main/examples/research_bot):** Simple deep research clone that demonstrates complex multi-agent research workflows. @@ -90,6 +122,9 @@ Check out a variety of sample implementations of the SDK in the examples section - Web search and web search with filters - File search - Code interpreter + - Apply patch tool with file editing and approval (`examples/tools/apply_patch.py`) + - Shell tool execution with approval callbacks (`examples/tools/shell.py`) + - Shell tool with human-in-the-loop interruption-based approvals (`examples/tools/shell_human_in_the_loop.py`) - Hosted container shell with inline skills (`examples/tools/container_shell_inline_skill.py`) - Hosted container shell with skill references (`examples/tools/container_shell_skill_reference.py`) - Local shell with local skills (`examples/tools/local_shell_skill.py`) From fe9e70fd4d06667da889107c04fb5d0cc752cded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=9D=E5=AD=90=E7=A5=BA?= Date: Thu, 2 Apr 2026 13:50:28 +0800 Subject: [PATCH 31/40] fix: #2823 AnyLLM reasoning extraction for iterable vLLM/any-llm Reasoning objects (#2822) --- src/agents/extensions/models/any_llm_model.py | 8 +++++--- tests/models/test_any_llm_model.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/agents/extensions/models/any_llm_model.py b/src/agents/extensions/models/any_llm_model.py index e135768a73..7f321a919c 100644 --- a/src/agents/extensions/models/any_llm_model.py +++ b/src/agents/extensions/models/any_llm_model.py @@ -163,14 +163,15 @@ def _flatten_any_llm_reasoning_value(value: Any) -> str: if flattened: return flattened return "" - if isinstance(value, Iterable) and not isinstance(value, (str, bytes)): - parts = [_flatten_any_llm_reasoning_value(item) for item in value] - return "".join(part for part in parts if part) for attr in ("content", "text", "thinking"): flattened = _flatten_any_llm_reasoning_value(getattr(value, attr, None)) if flattened: return flattened + + if isinstance(value, Iterable) and not isinstance(value, (str, bytes)): + parts = [_flatten_any_llm_reasoning_value(item) for item in value] + return "".join(part for part in parts if part) return "" @@ -829,6 +830,7 @@ async def _fetch_responses_response( handoffs=handoffs, model=self._provider_model, ) + converted_tools = OpenAIResponsesConverter.convert_tools( tools, handoffs, diff --git a/tests/models/test_any_llm_model.py b/tests/models/test_any_llm_model.py index 69fdd8ab67..62f807149f 100644 --- a/tests/models/test_any_llm_model.py +++ b/tests/models/test_any_llm_model.py @@ -739,3 +739,17 @@ def test_any_llm_provider_passes_api_override() -> None: assert isinstance(model, AnyLLMModel) assert model.api == "chat_completions" + + +def test_any_llm_reasoning_objects_prefer_content_attributes_over_iterable_pairs() -> None: + pytest.importorskip( + "any_llm", + reason="`any-llm-sdk` is only available when the optional dependency is installed.", + ) + from any_llm.types.completion import Reasoning + + from agents.extensions.models.any_llm_model import _extract_any_llm_reasoning_text + + delta = pytypes.SimpleNamespace(reasoning=Reasoning(content="用户")) + + assert _extract_any_llm_reasoning_text(delta) == "用户" From 76f419f795ade10902b98e8f030914c7fbc53fd1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 15:05:13 +0900 Subject: [PATCH 32/40] docs: update translated document pages (#2828) --- docs/ja/examples.md | 105 +++++++++++++++++++++++++++++--------------- docs/ko/examples.md | 87 +++++++++++++++++++++++++----------- docs/zh/examples.md | 93 +++++++++++++++++++++++++++------------ 3 files changed, 195 insertions(+), 90 deletions(-) diff --git a/docs/ja/examples.md b/docs/ja/examples.md index dbe84067a5..820719c934 100644 --- a/docs/ja/examples.md +++ b/docs/ja/examples.md @@ -4,104 +4,139 @@ search: --- # コード例 -[repo](https://github.com/openai/openai-agents-python/tree/main/examples) の examples セクションで、 SDK のさまざまなサンプル実装を確認できます。examples は、異なるパターンと機能を示す複数のカテゴリーに整理されています。 +[repo](https://github.com/openai/openai-agents-python/tree/main/examples) の examples セクションで、 SDK のさまざまなサンプル実装を確認できます。これらのコード例は、異なるパターンと機能を示す複数のカテゴリーに整理されています。 ## カテゴリー - **[agent_patterns](https://github.com/openai/openai-agents-python/tree/main/examples/agent_patterns):** - このカテゴリーのコード例では、一般的なエージェント設計パターンを示しています。例: + このカテゴリーのコード例では、次のような一般的なエージェント設計パターンを示します。 - 決定論的ワークフロー - Agents as tools - - エージェントの並列実行 + - ストリーミングイベントを伴う Agents as tools (`examples/agent_patterns/agents_as_tools_streaming.py`) + - 構造化入力パラメーターを伴う Agents as tools (`examples/agent_patterns/agents_as_tools_structured.py`) + - 並列エージェント実行 - 条件付きツール使用 - - 入力/出力ガードレール - - 審判としての LLM + - 異なる挙動でツール使用を強制する (`examples/agent_patterns/forcing_tool_use.py`) + - 入力 / 出力ガードレール + - 審査者としての LLM - ルーティング - ストリーミングガードレール + - ツール承認と状態シリアライズを伴う Human-in-the-loop (`examples/agent_patterns/human_in_the_loop.py`) + - ストリーミングを伴う Human-in-the-loop (`examples/agent_patterns/human_in_the_loop_stream.py`) - 承認フロー向けのカスタム拒否メッセージ (`examples/agent_patterns/human_in_the_loop_custom_rejection.py`) - **[basic](https://github.com/openai/openai-agents-python/tree/main/examples/basic):** - これらのコード例では、 SDK の基礎的な機能を紹介しています。例: + これらのコード例では、次のような SDK の基本機能を紹介します。 - - Hello world のコード例 (デフォルトモデル、 GPT-5 、 open-weight モデル) + - Hello world のコード例 (デフォルトモデル、 GPT-5、 open-weight モデル) - エージェントライフサイクル管理 + - Run hooks と agent hooks のライフサイクル例 (`examples/basic/lifecycle_example.py`) - 動的システムプロンプト + - 基本的なツール使用 (`examples/basic/tools.py`) + - ツール入力 / 出力ガードレール (`examples/basic/tool_guardrails.py`) + - 画像ツール出力 (`examples/basic/image_tool_output.py`) - ストリーミング出力 (テキスト、項目、関数呼び出し引数) - - ターン間で共有セッションヘルパーを使う Responses websocket トランスポート (`examples/basic/stream_ws.py`) + - 複数ターンで共有セッションヘルパーを使用する Responses websocket transport (`examples/basic/stream_ws.py`) - プロンプトテンプレート - - ファイル処理 (ローカル/リモート、画像/PDF) - - 使用状況トラッキング - - Runner 管理のリトライ設定 (`examples/basic/retry.py`) - - サードパーティアダプターを介した Runner 管理のリトライ (`examples/basic/retry_litellm.py`) + - ファイル処理 (ローカルとリモート、画像と PDF) + - 使用状況追跡 + - Runner 管理の再試行設定 (`examples/basic/retry.py`) + - サードパーティアダプター経由の Runner 管理再試行 (`examples/basic/retry_litellm.py`) - 非 strict な出力型 - - 前回レスポンス ID の使用 + - 以前の response ID の使用 - **[customer_service](https://github.com/openai/openai-agents-python/tree/main/examples/customer_service):** 航空会社向けのカスタマーサービスシステムのコード例です。 - **[financial_research_agent](https://github.com/openai/openai-agents-python/tree/main/examples/financial_research_agent):** - 金融データ分析のためのエージェントとツールを使った構造化リサーチワークフローを示す、金融リサーチエージェントです。 + 金融データ分析のためのエージェントとツールを用いた、構造化された調査ワークフローを示す金融リサーチエージェントです。 - **[handoffs](https://github.com/openai/openai-agents-python/tree/main/examples/handoffs):** - メッセージフィルタリングを伴うエージェントハンドオフの実践的なコード例をご覧ください。 + メッセージフィルタリングを含む、エージェントのハンドオフの実践的なコード例です。 + + - メッセージフィルター例 (`examples/handoffs/message_filter.py`) + - ストリーミングを伴うメッセージフィルター (`examples/handoffs/message_filter_streaming.py`) - **[hosted_mcp](https://github.com/openai/openai-agents-python/tree/main/examples/hosted_mcp):** - hosted MCP (Model Context Protocol) コネクターと承認の使い方を示すコード例です。 + OpenAI Responses API で hosted MCP (Model Context Protocol) を使用する方法を示すコード例です。以下を含みます。 + + - 承認なしのシンプルな hosted MCP (`examples/hosted_mcp/simple.py`) + - Google Calendar などの MCP コネクター (`examples/hosted_mcp/connectors.py`) + - 割り込みベース承認を伴う Human-in-the-loop (`examples/hosted_mcp/human_in_the_loop.py`) + - MCP ツール呼び出しの on-approval コールバック (`examples/hosted_mcp/on_approval.py`) - **[mcp](https://github.com/openai/openai-agents-python/tree/main/examples/mcp):** - MCP (Model Context Protocol) を使ってエージェントを構築する方法を学べます。内容: + 以下を含め、 MCP (Model Context Protocol) でエージェントを構築する方法を学べます。 - - ファイルシステムのコード例 + - Filesystem のコード例 - Git のコード例 - - MCP プロンプトサーバーのコード例 + - MCP prompt server のコード例 - SSE (Server-Sent Events) のコード例 - - ストリーミング可能な HTTP のコード例 + - SSE リモートサーバー接続 (`examples/mcp/sse_remote_example`) + - Streamable HTTP のコード例 + - Streamable HTTP リモート接続 (`examples/mcp/streamable_http_remote_example`) + - Streamable HTTP 向けカスタム HTTP client factory (`examples/mcp/streamablehttp_custom_client_example`) + - `MCPUtil.get_all_function_tools` による全 MCP ツールの事前取得 (`examples/mcp/get_all_mcp_tools_example`) + - FastAPI を使用した MCPServerManager (`examples/mcp/manager_example`) + - MCP ツールフィルタリング (`examples/mcp/tool_filter_example`) - **[memory](https://github.com/openai/openai-agents-python/tree/main/examples/memory):** - エージェント向けのさまざまなメモリ実装のコード例です。内容: + エージェント向けのさまざまなメモリ実装のコード例です。以下を含みます。 - SQLite セッションストレージ - 高度な SQLite セッションストレージ - Redis セッションストレージ - SQLAlchemy セッションストレージ - - Dapr ステートストアセッションストレージ + - Dapr state store セッションストレージ - 暗号化セッションストレージ - OpenAI Conversations セッションストレージ - Responses compaction セッションストレージ - - `ModelSettings(store=False)` を使ったステートレスな Responses compaction (`examples/memory/compaction_session_stateless_example.py`) + - `ModelSettings(store=False)` を使用したステートレスな Responses compaction (`examples/memory/compaction_session_stateless_example.py`) + - ファイルベースのセッションストレージ (`examples/memory/file_session.py`) + - Human-in-the-loop を伴うファイルベースセッション (`examples/memory/file_hitl_example.py`) + - Human-in-the-loop を伴う SQLite インメモリセッション (`examples/memory/memory_session_hitl_example.py`) + - Human-in-the-loop を伴う OpenAI Conversations セッション (`examples/memory/openai_session_hitl_example.py`) + - セッションをまたぐ HITL 承認 / 拒否シナリオ (`examples/memory/hitl_session_scenario.py`) - **[model_providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers):** - カスタムプロバイダーやサードパーティアダプターを含め、 SDK で非 OpenAI モデルを使う方法を確認できます。 + カスタムプロバイダーやサードパーティアダプターを含め、 SDK で非 OpenAI モデルを使用する方法を確認できます。 - **[realtime](https://github.com/openai/openai-agents-python/tree/main/examples/realtime):** - SDK を使ってリアルタイム体験を構築する方法を示すコード例です。内容: + SDK を使用してリアルタイム体験を構築する方法を示すコード例です。以下を含みます。 - - 構造化テキストおよび画像メッセージを使った Web アプリケーションパターン + - 構造化されたテキストおよび画像メッセージによる Web アプリケーションパターン - コマンドライン音声ループと再生処理 - WebSocket 経由の Twilio Media Streams 統合 - Realtime Calls API attach フローを使用した Twilio SIP 統合 - **[reasoning_content](https://github.com/openai/openai-agents-python/tree/main/examples/reasoning_content):** - reasoning content と structured outputs の扱い方を示すコード例です。 + reasoning content の扱い方を示すコード例です。以下を含みます。 + + - Runner API、ストリーミング、非ストリーミングでの reasoning content (`examples/reasoning_content/runner_example.py`) + - OpenRouter 経由で OSS モデルを使用した reasoning content (`examples/reasoning_content/gpt_oss_stream.py`) + - 基本的な reasoning content のコード例 (`examples/reasoning_content/main.py`) - **[research_bot](https://github.com/openai/openai-agents-python/tree/main/examples/research_bot):** - 複雑なマルチエージェントのディープリサーチワークフローを示す、シンプルなディープリサーチクローンです。 + 複雑なマルチエージェント調査ワークフローを示す、シンプルなディープリサーチクローンです。 - **[tools](https://github.com/openai/openai-agents-python/tree/main/examples/tools):** - OpenAI がホストするツールと、次のような実験的な Codex ツール機能の実装方法を学べます。 + 以下のような OpenAI がホストするツールと実験的な Codex ツール機能の実装方法を学べます。 - Web 検索 とフィルター付き Web 検索 - ファイル検索 - - Code Interpreter - - インラインスキル付き hosted container shell (`examples/tools/container_shell_inline_skill.py`) - - スキル参照付き hosted container shell (`examples/tools/container_shell_skill_reference.py`) - - ローカルスキル付きローカル shell (`examples/tools/local_shell_skill.py`) - - 名前空間と遅延ツールを使ったツール検索 (`examples/tools/tool_search.py`) + - Code interpreter + - ファイル編集と承認を伴う apply patch ツール (`examples/tools/apply_patch.py`) + - 承認コールバックを伴う shell ツール実行 (`examples/tools/shell.py`) + - Human-in-the-loop 割り込みベース承認を伴う shell ツール (`examples/tools/shell_human_in_the_loop.py`) + - インラインスキルを伴う hosted container shell (`examples/tools/container_shell_inline_skill.py`) + - スキル参照を伴う hosted container shell (`examples/tools/container_shell_skill_reference.py`) + - ローカルスキルを伴う local shell (`examples/tools/local_shell_skill.py`) + - 名前空間と遅延ツールを伴うツール検索 (`examples/tools/tool_search.py`) - コンピュータ操作 - 画像生成 - 実験的な Codex ツールワークフロー (`examples/tools/codex.py`) - 実験的な Codex 同一スレッドワークフロー (`examples/tools/codex_same_thread.py`) - **[voice](https://github.com/openai/openai-agents-python/tree/main/examples/voice):** - ストリーミング音声のコード例を含む、 TTS / STT モデルを使用した音声エージェントのコード例をご覧ください。 \ No newline at end of file + ストリーミング音声のコード例を含む、 TTS および STT モデルを使用した音声エージェントのコード例を確認できます。 \ No newline at end of file diff --git a/docs/ko/examples.md b/docs/ko/examples.md index 9d3d59cee4..7718043d15 100644 --- a/docs/ko/examples.md +++ b/docs/ko/examples.md @@ -4,7 +4,7 @@ search: --- # 코드 예제 -[repo](https://github.com/openai/openai-agents-python/tree/main/examples)의 examples 섹션에서 SDK의 다양한 샘플 구현을 확인해 보세요. examples는 다양한 패턴과 기능을 보여주는 여러 카테고리로 구성되어 있습니다. +[repo](https://github.com/openai/openai-agents-python/tree/main/examples)의 examples 섹션에서 SDK의 다양한 샘플 구현을 확인해 보세요. examples는 서로 다른 패턴과 기능을 보여주는 여러 카테고리로 구성되어 있습니다. ## 카테고리 @@ -13,21 +13,30 @@ search: - 결정론적 워크플로 - Agents as tools + - 스트리밍 이벤트를 포함한 Agents as tools (`examples/agent_patterns/agents_as_tools_streaming.py`) + - 구조화된 입력 매개변수를 포함한 Agents as tools (`examples/agent_patterns/agents_as_tools_structured.py`) - 병렬 에이전트 실행 - 조건부 도구 사용 - - 입력/출력 가드레일 - - 심사자로서의 LLM + - 서로 다른 동작으로 도구 사용 강제 (`examples/agent_patterns/forcing_tool_use.py`) + - 입출력 가드레일 + - 심판 역할의 LLM - 라우팅 - 스트리밍 가드레일 - - 승인 흐름을 위한 사용자 지정 거절 메시지 (`examples/agent_patterns/human_in_the_loop_custom_rejection.py`) + - 도구 승인 및 상태 직렬화를 포함한 휴먼인더루프 (HITL) (`examples/agent_patterns/human_in_the_loop.py`) + - 스트리밍을 포함한 휴먼인더루프 (HITL) (`examples/agent_patterns/human_in_the_loop_stream.py`) + - 승인 플로를 위한 사용자 지정 거절 메시지 (`examples/agent_patterns/human_in_the_loop_custom_rejection.py`) - **[basic](https://github.com/openai/openai-agents-python/tree/main/examples/basic):** 이 예제들은 다음과 같은 SDK의 기본 기능을 보여줍니다 - - Hello World 예제 (기본 모델, GPT-5, 오픈 가중치 모델) + - Hello World 예제 (기본 모델, GPT-5, 오픈 웨이트 모델) - 에이전트 라이프사이클 관리 + - 실행 훅 및 에이전트 훅 라이프사이클 예제 (`examples/basic/lifecycle_example.py`) - 동적 시스템 프롬프트 - - 스트리밍 출력 (텍스트, 항목, 함수 호출 인수) + - 기본 도구 사용 (`examples/basic/tools.py`) + - 도구 입출력 가드레일 (`examples/basic/tool_guardrails.py`) + - 이미지 도구 출력 (`examples/basic/image_tool_output.py`) + - 스트리밍 출력 (텍스트, 항목, 함수 호출 인자) - 턴 간 공유 세션 헬퍼를 사용하는 Responses websocket 전송 (`examples/basic/stream_ws.py`) - 프롬프트 템플릿 - 파일 처리 (로컬 및 원격, 이미지 및 PDF) @@ -41,25 +50,39 @@ search: 항공사를 위한 고객 서비스 시스템 예제입니다 - **[financial_research_agent](https://github.com/openai/openai-agents-python/tree/main/examples/financial_research_agent):** - 금융 데이터 분석을 위해 에이전트와 도구를 활용한 구조화된 리서치 워크플로를 보여주는 금융 리서치 에이전트입니다 + 금융 데이터 분석을 위한 에이전트와 도구를 사용한 구조화된 리서치 워크플로를 보여주는 금융 리서치 에이전트입니다 - **[handoffs](https://github.com/openai/openai-agents-python/tree/main/examples/handoffs):** - 메시지 필터링을 사용하는 에이전트 핸드오프의 실용적인 예제를 확인하세요 + 메시지 필터링을 포함한 에이전트 핸드오프의 실용적인 예제: + + - 메시지 필터 예제 (`examples/handoffs/message_filter.py`) + - 스트리밍을 포함한 메시지 필터 (`examples/handoffs/message_filter_streaming.py`) - **[hosted_mcp](https://github.com/openai/openai-agents-python/tree/main/examples/hosted_mcp):** - 호스티드 MCP (Model context protocol) 커넥터와 승인 사용 방법을 보여주는 예제입니다 + OpenAI Responses API와 함께 호스티드 MCP (Model context protocol)를 사용하는 방법을 보여주는 예제: + + - 승인 없는 간단한 호스티드 MCP (`examples/hosted_mcp/simple.py`) + - Google Calendar 같은 MCP 커넥터 (`examples/hosted_mcp/connectors.py`) + - 인터럽션(중단 처리) 기반 승인을 포함한 휴먼인더루프 (HITL) (`examples/hosted_mcp/human_in_the_loop.py`) + - MCP 도구 호출용 승인 시 콜백 (`examples/hosted_mcp/on_approval.py`) - **[mcp](https://github.com/openai/openai-agents-python/tree/main/examples/mcp):** - 다음을 포함해 MCP (Model context protocol)로 에이전트를 구축하는 방법을 알아보세요 + MCP (Model context protocol)로 에이전트를 구축하는 방법을 알아보세요: - - 파일 시스템 예제 + - 파일시스템 예제 - Git 예제 - MCP 프롬프트 서버 예제 - SSE (Server-Sent Events) 예제 - - 스트리밍 가능한 HTTP 예제 + - SSE 원격 서버 연결 (`examples/mcp/sse_remote_example`) + - Streamable HTTP 예제 + - Streamable HTTP 원격 연결 (`examples/mcp/streamable_http_remote_example`) + - Streamable HTTP용 사용자 지정 HTTP 클라이언트 팩토리 (`examples/mcp/streamablehttp_custom_client_example`) + - `MCPUtil.get_all_function_tools`를 사용한 모든 MCP 도구 프리패칭 (`examples/mcp/get_all_mcp_tools_example`) + - FastAPI를 사용하는 MCPServerManager (`examples/mcp/manager_example`) + - MCP 도구 필터링 (`examples/mcp/tool_filter_example`) - **[memory](https://github.com/openai/openai-agents-python/tree/main/examples/memory):** - 다음을 포함한 에이전트를 위한 다양한 메모리 구현 예제입니다 + 에이전트를 위한 다양한 메모리 구현 예제: - SQLite 세션 저장소 - 고급 SQLite 세션 저장소 @@ -70,38 +93,50 @@ search: - OpenAI Conversations 세션 저장소 - Responses 컴팩션 세션 저장소 - `ModelSettings(store=False)`를 사용한 상태 비저장 Responses 컴팩션 (`examples/memory/compaction_session_stateless_example.py`) + - 파일 기반 세션 저장소 (`examples/memory/file_session.py`) + - 휴먼인더루프 (HITL)를 포함한 파일 기반 세션 (`examples/memory/file_hitl_example.py`) + - 휴먼인더루프 (HITL)를 포함한 SQLite 인메모리 세션 (`examples/memory/memory_session_hitl_example.py`) + - 휴먼인더루프 (HITL)를 포함한 OpenAI Conversations 세션 (`examples/memory/openai_session_hitl_example.py`) + - 세션 전반의 HITL 승인/거절 시나리오 (`examples/memory/hitl_session_scenario.py`) - **[model_providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers):** - 사용자 지정 제공자와 서드파티 어댑터를 포함해 SDK에서 OpenAI가 아닌 모델을 사용하는 방법을 살펴보세요 + 사용자 지정 프로바이더와 서드파티 어댑터를 포함해 SDK에서 OpenAI 이외 모델을 사용하는 방법을 살펴보세요 - **[realtime](https://github.com/openai/openai-agents-python/tree/main/examples/realtime):** - 다음을 포함해 SDK를 사용해 실시간 경험을 구축하는 방법을 보여주는 예제입니다 + SDK를 사용해 실시간 경험을 구축하는 방법을 보여주는 예제: - 구조화된 텍스트 및 이미지 메시지를 사용하는 웹 애플리케이션 패턴 - - 명령줄 오디오 루프 및 재생 처리 + - 커맨드라인 오디오 루프 및 재생 처리 - WebSocket을 통한 Twilio Media Streams 통합 - - Realtime Calls API attach 흐름을 사용하는 Twilio SIP 통합 + - Realtime Calls API attach 플로를 사용하는 Twilio SIP 통합 - **[reasoning_content](https://github.com/openai/openai-agents-python/tree/main/examples/reasoning_content):** - reasoning content 및 structured outputs를 다루는 방법을 보여주는 예제입니다 + reasoning content를 다루는 방법을 보여주는 예제: + + - Runner API의 reasoning content, 스트리밍 및 비스트리밍 (`examples/reasoning_content/runner_example.py`) + - OpenRouter를 통한 OSS 모델의 reasoning content (`examples/reasoning_content/gpt_oss_stream.py`) + - 기본 reasoning content 예제 (`examples/reasoning_content/main.py`) - **[research_bot](https://github.com/openai/openai-agents-python/tree/main/examples/research_bot):** 복잡한 멀티 에이전트 리서치 워크플로를 보여주는 간단한 딥 리서치 클론입니다 - **[tools](https://github.com/openai/openai-agents-python/tree/main/examples/tools):** - 다음과 같은 OpenAI 호스트하는 도구 및 실험적 Codex 도구 기능을 구현하는 방법을 알아보세요 + 다음과 같은 OpenAI 호스트하는 도구 및 실험적 Codex 도구 기능을 구현하는 방법을 알아보세요: - - 웹 검색 및 필터가 있는 웹 검색 + - 웹 검색 및 필터를 포함한 웹 검색 - 파일 검색 - - 코드 인터프리터 - - 인라인 스킬을 사용하는 호스티드 컨테이너 셸 (`examples/tools/container_shell_inline_skill.py`) - - 스킬 참조를 사용하는 호스티드 컨테이너 셸 (`examples/tools/container_shell_skill_reference.py`) - - 로컬 스킬을 사용하는 로컬 셸 (`examples/tools/local_shell_skill.py`) - - 네임스페이스와 지연 도구를 사용하는 도구 검색 (`examples/tools/tool_search.py`) + - Code Interpreter + - 파일 편집 및 승인을 포함한 패치 적용 도구 (`examples/tools/apply_patch.py`) + - 승인 콜백을 포함한 셸 도구 실행 (`examples/tools/shell.py`) + - 휴먼인더루프 (HITL) 인터럽션(중단 처리) 기반 승인을 포함한 셸 도구 (`examples/tools/shell_human_in_the_loop.py`) + - 인라인 스킬을 포함한 호스티드 컨테이너 셸 (`examples/tools/container_shell_inline_skill.py`) + - 스킬 참조를 포함한 호스티드 컨테이너 셸 (`examples/tools/container_shell_skill_reference.py`) + - 로컬 스킬을 포함한 로컬 셸 (`examples/tools/local_shell_skill.py`) + - 네임스페이스 및 지연 도구를 사용하는 도구 검색 (`examples/tools/tool_search.py`) - 컴퓨터 사용 - 이미지 생성 - 실험적 Codex 도구 워크플로 (`examples/tools/codex.py`) - 실험적 Codex 동일 스레드 워크플로 (`examples/tools/codex_same_thread.py`) - **[voice](https://github.com/openai/openai-agents-python/tree/main/examples/voice):** - 스트리밍 음성 예제를 포함해 TTS 및 STT 모델을 사용하는 음성 에이전트 예제를 확인하세요 \ No newline at end of file + 스트리밍 음성 예제를 포함해 TTS 및 STT 모델을 사용하는 음성 에이전트 예제를 확인해 보세요 \ No newline at end of file diff --git a/docs/zh/examples.md b/docs/zh/examples.md index 4ca594faf4..d2aee9af6d 100644 --- a/docs/zh/examples.md +++ b/docs/zh/examples.md @@ -4,62 +4,85 @@ search: --- # 示例 -在[repo](https://github.com/openai/openai-agents-python/tree/main/examples)的示例部分查看 SDK 的各种 sample code。这些示例按多个目录组织,展示了不同的模式与能力。 +请在 [repo](https://github.com/openai/openai-agents-python/tree/main/examples) 的示例部分查看 SDK 的多种 sample code。示例按多个目录组织,展示了不同的模式和能力。 ## 目录 - **[agent_patterns](https://github.com/openai/openai-agents-python/tree/main/examples/agent_patterns):** - 该目录中的示例展示了常见的智能体设计模式,例如 + 此目录中的示例展示了常见的智能体设计模式,例如 - 确定性工作流 - Agents as tools + - 带流式事件的 Agents as tools(`examples/agent_patterns/agents_as_tools_streaming.py`) + - 带结构化输入参数的 Agents as tools(`examples/agent_patterns/agents_as_tools_structured.py`) - 并行智能体执行 - 条件化工具使用 + - 通过不同行为强制工具使用(`examples/agent_patterns/forcing_tool_use.py`) - 输入/输出安全防护措施 - - LLM 作为评判者 + - LLM 作为裁判 - 路由 - - 流式传输安全防护措施 - - 用于审批流程的自定义拒绝消息(`examples/agent_patterns/human_in_the_loop_custom_rejection.py`) + - 流式安全防护措施 + - 带工具审批与状态序列化的人在回路(`examples/agent_patterns/human_in_the_loop.py`) + - 带流式传输的人在回路(`examples/agent_patterns/human_in_the_loop_stream.py`) + - 审批流程的自定义拒绝消息(`examples/agent_patterns/human_in_the_loop_custom_rejection.py`) - **[basic](https://github.com/openai/openai-agents-python/tree/main/examples/basic):** 这些示例展示了 SDK 的基础能力,例如 - - Hello world 示例(默认模型、GPT-5、开放权重模型) + - Hello World 示例(默认模型、GPT-5、开放权重模型) - 智能体生命周期管理 + - Run hooks 和 agent hooks 生命周期示例(`examples/basic/lifecycle_example.py`) - 动态系统提示词 - - 流式传输输出(文本、条目、函数调用参数) - - 跨轮次使用共享会话助手的 Responses websocket 传输(`examples/basic/stream_ws.py`) + - 基础工具使用(`examples/basic/tools.py`) + - 工具输入/输出安全防护措施(`examples/basic/tool_guardrails.py`) + - 图像工具输出(`examples/basic/image_tool_output.py`) + - 流式输出(文本、条目、函数调用参数) + - 跨轮次共享会话助手的 Responses websocket 传输(`examples/basic/stream_ws.py`) - 提示词模板 - - 文件处理(本地与远程,图像与 PDF) - - 用量跟踪 + - 文件处理(本地与远程、图像与 PDF) + - 用量追踪 - 由 Runner 管理的重试设置(`examples/basic/retry.py`) - - 通过第三方适配器进行由 Runner 管理的重试(`examples/basic/retry_litellm.py`) + - 通过第三方适配器由 Runner 管理重试(`examples/basic/retry_litellm.py`) - 非严格输出类型 - - 先前响应 ID 的使用 + - previous response ID 用法 - **[customer_service](https://github.com/openai/openai-agents-python/tree/main/examples/customer_service):** - 面向航空公司的客户服务系统示例。 + 航空公司的客户服务系统示例。 - **[financial_research_agent](https://github.com/openai/openai-agents-python/tree/main/examples/financial_research_agent):** - 一个金融研究智能体,展示了使用智能体与工具进行金融数据分析的结构化研究工作流。 + 一个金融研究智能体,展示了使用智能体和工具进行金融数据分析的结构化研究工作流。 - **[handoffs](https://github.com/openai/openai-agents-python/tree/main/examples/handoffs):** - 查看带有消息过滤的智能体任务转移实践示例。 + 智能体任务转移的实用示例,包含消息过滤,包括: + + - 消息过滤示例(`examples/handoffs/message_filter.py`) + - 带流式传输的消息过滤(`examples/handoffs/message_filter_streaming.py`) - **[hosted_mcp](https://github.com/openai/openai-agents-python/tree/main/examples/hosted_mcp):** - 演示如何使用托管 MCP(Model Context Protocol)连接器与审批的示例。 + 展示如何将托管 MCP(Model context protocol)与 OpenAI Responses API 一起使用的示例,包括: + + - 无需审批的简单托管 MCP(`examples/hosted_mcp/simple.py`) + - MCP 连接器,例如 Google Calendar(`examples/hosted_mcp/connectors.py`) + - 基于中断审批的人在回路(`examples/hosted_mcp/human_in_the_loop.py`) + - MCP 工具调用的审批回调(`examples/hosted_mcp/on_approval.py`) - **[mcp](https://github.com/openai/openai-agents-python/tree/main/examples/mcp):** - 了解如何使用 MCP(Model Context Protocol)构建智能体,包括: + 了解如何使用 MCP(Model context protocol)构建智能体,包括: - 文件系统示例 - Git 示例 - - MCP 提示词服务示例 - - SSE(Server-Sent Events)示例 - - 可流式 HTTP 示例 + - MCP prompt 服务示例 + - SSE(服务器发送事件)示例 + - SSE 远程服务连接(`examples/mcp/sse_remote_example`) + - Streamable HTTP 示例 + - Streamable HTTP 远程连接(`examples/mcp/streamable_http_remote_example`) + - 用于 Streamable HTTP 的自定义 HTTP 客户端工厂(`examples/mcp/streamablehttp_custom_client_example`) + - 使用 `MCPUtil.get_all_function_tools` 预获取所有 MCP 工具(`examples/mcp/get_all_mcp_tools_example`) + - 搭配 FastAPI 的 MCPServerManager(`examples/mcp/manager_example`) + - MCP 工具过滤(`examples/mcp/tool_filter_example`) - **[memory](https://github.com/openai/openai-agents-python/tree/main/examples/memory):** - 不同智能体记忆实现的示例,包括: + 面向智能体的不同内存实现示例,包括: - SQLite 会话存储 - 高级 SQLite 会话存储 @@ -70,6 +93,11 @@ search: - OpenAI Conversations 会话存储 - Responses 压缩会话存储 - 使用 `ModelSettings(store=False)` 的无状态 Responses 压缩(`examples/memory/compaction_session_stateless_example.py`) + - 文件后端会话存储(`examples/memory/file_session.py`) + - 带人在回路的文件后端会话(`examples/memory/file_hitl_example.py`) + - 带人在回路的 SQLite 内存会话(`examples/memory/memory_session_hitl_example.py`) + - 带人在回路的 OpenAI Conversations 会话(`examples/memory/openai_session_hitl_example.py`) + - 跨会话的 HITL 审批/拒绝场景(`examples/memory/hitl_session_scenario.py`) - **[model_providers](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers):** 探索如何在 SDK 中使用非 OpenAI 模型,包括自定义提供方和第三方适配器。 @@ -77,27 +105,34 @@ search: - **[realtime](https://github.com/openai/openai-agents-python/tree/main/examples/realtime):** 展示如何使用 SDK 构建实时体验的示例,包括: - - 使用结构化文本与图像消息的 Web 应用模式 + - 使用结构化文本和图像消息的 Web 应用模式 - 命令行音频循环与播放处理 - - 通过 WebSocket 集成 Twilio Media Streams - - 使用 Realtime Calls API 附加流程的 Twilio SIP 集成 + - 基于 WebSocket 的 Twilio Media Streams 集成 + - 使用 Realtime Calls API attach 流程的 Twilio SIP 集成 - **[reasoning_content](https://github.com/openai/openai-agents-python/tree/main/examples/reasoning_content):** - 演示如何处理推理内容和 structured outputs 的示例。 + 展示如何处理推理内容的示例,包括: + + - 使用 Runner API 的推理内容,含流式和非流式(`examples/reasoning_content/runner_example.py`) + - 通过 OpenRouter 使用 OSS 模型的推理内容(`examples/reasoning_content/gpt_oss_stream.py`) + - 基础推理内容示例(`examples/reasoning_content/main.py`) - **[research_bot](https://github.com/openai/openai-agents-python/tree/main/examples/research_bot):** - 简单的深度研究克隆,展示了复杂的多智能体研究工作流。 + 简单的深度研究克隆示例,展示复杂的多智能体研究工作流。 - **[tools](https://github.com/openai/openai-agents-python/tree/main/examples/tools):** 了解如何实现由OpenAI托管的工具和实验性 Codex 工具能力,例如: - - 网络检索与带过滤器的网络检索 + - 网络检索以及带过滤器的网络检索 - 文件检索 - - 代码解释器 + - Code Interpreter + - 带文件编辑与审批的 apply patch 工具(`examples/tools/apply_patch.py`) + - 带审批回调的 shell 工具执行(`examples/tools/shell.py`) + - 带基于中断审批的人在回路 shell 工具(`examples/tools/shell_human_in_the_loop.py`) - 带内联技能的托管容器 shell(`examples/tools/container_shell_inline_skill.py`) - 带技能引用的托管容器 shell(`examples/tools/container_shell_skill_reference.py`) - 带本地技能的本地 shell(`examples/tools/local_shell_skill.py`) - - 带命名空间与延迟工具的工具搜索(`examples/tools/tool_search.py`) + - 带命名空间和延迟工具的工具搜索(`examples/tools/tool_search.py`) - 计算机操作 - 图像生成 - 实验性 Codex 工具工作流(`examples/tools/codex.py`) From 6a89f1b564b0ddef82ab5bff174d213796b1d2f5 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Sat, 4 Apr 2026 12:19:29 +0900 Subject: [PATCH 33/40] fix: serialize SQLite session writes with shared file locks (#2843) --- .../memory/advanced_sqlite_session.py | 953 +++++++++--------- src/agents/memory/sqlite_session.py | 138 ++- .../memory/test_advanced_sqlite_session.py | 53 + tests/test_session.py | 30 + 4 files changed, 663 insertions(+), 511 deletions(-) diff --git a/src/agents/extensions/memory/advanced_sqlite_session.py b/src/agents/extensions/memory/advanced_sqlite_session.py index fcb4743cb3..f0c3cb8f3a 100644 --- a/src/agents/extensions/memory/advanced_sqlite_session.py +++ b/src/agents/extensions/memory/advanced_sqlite_session.py @@ -3,7 +3,7 @@ import asyncio import json import logging -import threading +import sqlite3 from contextlib import closing from pathlib import Path from typing import Any, Union, cast @@ -56,71 +56,70 @@ def _init_structure_tables(self): Creates the message_structure and turn_usage tables with appropriate indexes for conversation branching and usage analytics. """ - conn = self._get_connection() - - # Message structure with branch support - conn.execute(f""" - CREATE TABLE IF NOT EXISTS message_structure ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id TEXT NOT NULL, - message_id INTEGER NOT NULL, - branch_id TEXT NOT NULL DEFAULT 'main', - message_type TEXT NOT NULL, - sequence_number INTEGER NOT NULL, - user_turn_number INTEGER, - branch_turn_number INTEGER, - tool_name TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (session_id) - REFERENCES {self.sessions_table}(session_id) ON DELETE CASCADE, - FOREIGN KEY (message_id) - REFERENCES {self.messages_table}(id) ON DELETE CASCADE - ) - """) - - # Turn-level usage tracking with branch support and full JSON details - conn.execute(f""" - CREATE TABLE IF NOT EXISTS turn_usage ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id TEXT NOT NULL, - branch_id TEXT NOT NULL DEFAULT 'main', - user_turn_number INTEGER NOT NULL, - requests INTEGER DEFAULT 0, - input_tokens INTEGER DEFAULT 0, - output_tokens INTEGER DEFAULT 0, - total_tokens INTEGER DEFAULT 0, - input_tokens_details JSON, - output_tokens_details JSON, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (session_id) - REFERENCES {self.sessions_table}(session_id) ON DELETE CASCADE, - UNIQUE(session_id, branch_id, user_turn_number) - ) - """) - - # Indexes - conn.execute(""" - CREATE INDEX IF NOT EXISTS idx_structure_session_seq - ON message_structure(session_id, sequence_number) - """) - conn.execute(""" - CREATE INDEX IF NOT EXISTS idx_structure_branch - ON message_structure(session_id, branch_id) - """) - conn.execute(""" - CREATE INDEX IF NOT EXISTS idx_structure_turn - ON message_structure(session_id, branch_id, user_turn_number) - """) - conn.execute(""" - CREATE INDEX IF NOT EXISTS idx_structure_branch_seq - ON message_structure(session_id, branch_id, sequence_number) - """) - conn.execute(""" - CREATE INDEX IF NOT EXISTS idx_turn_usage_session_turn - ON turn_usage(session_id, branch_id, user_turn_number) - """) - - conn.commit() + with self._locked_connection() as conn: + # Message structure with branch support + conn.execute(f""" + CREATE TABLE IF NOT EXISTS message_structure ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + message_id INTEGER NOT NULL, + branch_id TEXT NOT NULL DEFAULT 'main', + message_type TEXT NOT NULL, + sequence_number INTEGER NOT NULL, + user_turn_number INTEGER, + branch_turn_number INTEGER, + tool_name TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (session_id) + REFERENCES {self.sessions_table}(session_id) ON DELETE CASCADE, + FOREIGN KEY (message_id) + REFERENCES {self.messages_table}(id) ON DELETE CASCADE + ) + """) + + # Turn-level usage tracking with branch support and full JSON details + conn.execute(f""" + CREATE TABLE IF NOT EXISTS turn_usage ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + branch_id TEXT NOT NULL DEFAULT 'main', + user_turn_number INTEGER NOT NULL, + requests INTEGER DEFAULT 0, + input_tokens INTEGER DEFAULT 0, + output_tokens INTEGER DEFAULT 0, + total_tokens INTEGER DEFAULT 0, + input_tokens_details JSON, + output_tokens_details JSON, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (session_id) + REFERENCES {self.sessions_table}(session_id) ON DELETE CASCADE, + UNIQUE(session_id, branch_id, user_turn_number) + ) + """) + + # Indexes + conn.execute(""" + CREATE INDEX IF NOT EXISTS idx_structure_session_seq + ON message_structure(session_id, sequence_number) + """) + conn.execute(""" + CREATE INDEX IF NOT EXISTS idx_structure_branch + ON message_structure(session_id, branch_id) + """) + conn.execute(""" + CREATE INDEX IF NOT EXISTS idx_structure_turn + ON message_structure(session_id, branch_id, user_turn_number) + """) + conn.execute(""" + CREATE INDEX IF NOT EXISTS idx_structure_branch_seq + ON message_structure(session_id, branch_id, sequence_number) + """) + conn.execute(""" + CREATE INDEX IF NOT EXISTS idx_turn_usage_session_turn + ON turn_usage(session_id, branch_id, user_turn_number) + """) + + conn.commit() async def add_items(self, items: list[TResponseInputItem]) -> None: """Add items to the session. @@ -128,12 +127,34 @@ async def add_items(self, items: list[TResponseInputItem]) -> None: Args: items: The items to add to the session """ - # Add to base table first - await super().add_items(items) + if not items: + return + + def _add_items_sync(): + """Synchronous helper to add items and structure metadata together.""" + with self._locked_connection() as conn: + # Keep both writes in one critical section so message IDs and metadata stay aligned. + self._insert_items(conn, items) + conn.commit() + try: + self._insert_structure_metadata(conn, items) + conn.commit() + except Exception as e: + conn.rollback() + self._logger.error( + f"Failed to add structure metadata for session {self.session_id}: {e}" + ) + try: + deleted_count = self._cleanup_orphaned_messages_sync(conn) + if deleted_count: + conn.commit() + else: + conn.rollback() + except Exception as cleanup_error: + conn.rollback() + self._logger.error(f"Failed to cleanup orphaned messages: {cleanup_error}") - # Extract structure metadata with precise sequencing - if items: - await self._add_structure_metadata(items) + await asyncio.to_thread(_add_items_sync) async def get_items( self, @@ -157,9 +178,7 @@ async def get_items( # Get all items for this branch def _get_all_items_sync(): """Synchronous helper to get all items for a branch.""" - conn = self._get_connection() - # TODO: Refactor SQLiteSession to use asyncio.Lock instead of threading.Lock and update this code # noqa: E501 - with self._lock if self._is_memory_db else threading.Lock(): + with self._locked_connection() as conn: with closing(conn.cursor()) as cursor: if session_limit is None: cursor.execute( @@ -202,9 +221,7 @@ def _get_all_items_sync(): def _get_items_sync(): """Synchronous helper to get items for a specific branch.""" - conn = self._get_connection() - # TODO: Refactor SQLiteSession to use asyncio.Lock instead of threading.Lock and update this code # noqa: E501 - with self._lock if self._is_memory_db else threading.Lock(): + with self._locked_connection() as conn: with closing(conn.cursor()) as cursor: # Get message IDs in correct order for this branch if session_limit is None: @@ -273,19 +290,19 @@ def _get_next_turn_number(self, branch_id: str) -> int: Returns: The next available turn number for the specified branch. """ - conn = self._get_connection() - with closing(conn.cursor()) as cursor: - cursor.execute( - """ - SELECT COALESCE(MAX(user_turn_number), 0) - FROM message_structure - WHERE session_id = ? AND branch_id = ? - """, - (self.session_id, branch_id), - ) - result = cursor.fetchone() - max_turn = result[0] if result else 0 - return max_turn + 1 + with self._locked_connection() as conn: + with closing(conn.cursor()) as cursor: + cursor.execute( + """ + SELECT COALESCE(MAX(user_turn_number), 0) + FROM message_structure + WHERE session_id = ? AND branch_id = ? + """, + (self.session_id, branch_id), + ) + result = cursor.fetchone() + max_turn = result[0] if result else 0 + return max_turn + 1 def _get_next_branch_turn_number(self, branch_id: str) -> int: """Get the next branch turn number for a specific branch. @@ -296,19 +313,19 @@ def _get_next_branch_turn_number(self, branch_id: str) -> int: Returns: The next available branch turn number for the specified branch. """ - conn = self._get_connection() - with closing(conn.cursor()) as cursor: - cursor.execute( - """ - SELECT COALESCE(MAX(branch_turn_number), 0) - FROM message_structure - WHERE session_id = ? AND branch_id = ? - """, - (self.session_id, branch_id), - ) - result = cursor.fetchone() - max_turn = result[0] if result else 0 - return max_turn + 1 + with self._locked_connection() as conn: + with closing(conn.cursor()) as cursor: + cursor.execute( + """ + SELECT COALESCE(MAX(branch_turn_number), 0) + FROM message_structure + WHERE session_id = ? AND branch_id = ? + """, + (self.session_id, branch_id), + ) + result = cursor.fetchone() + max_turn = result[0] if result else 0 + return max_turn + 1 def _get_current_turn_number(self) -> int: """Get the current turn number for the current branch. @@ -316,18 +333,18 @@ def _get_current_turn_number(self) -> int: Returns: The current turn number for the active branch. """ - conn = self._get_connection() - with closing(conn.cursor()) as cursor: - cursor.execute( - """ - SELECT COALESCE(MAX(user_turn_number), 0) - FROM message_structure - WHERE session_id = ? AND branch_id = ? - """, - (self.session_id, self._current_branch_id), - ) - result = cursor.fetchone() - return result[0] if result else 0 + with self._locked_connection() as conn: + with closing(conn.cursor()) as cursor: + cursor.execute( + """ + SELECT COALESCE(MAX(user_turn_number), 0) + FROM message_structure + WHERE session_id = ? AND branch_id = ? + """, + (self.session_id, self._current_branch_id), + ) + result = cursor.fetchone() + return result[0] if result else 0 async def _add_structure_metadata(self, items: list[TResponseInputItem]) -> None: """Extract structure metadata with branch-aware turn tracking. @@ -344,89 +361,9 @@ async def _add_structure_metadata(self, items: list[TResponseInputItem]) -> None def _add_structure_sync(): """Synchronous helper to add structure metadata to database.""" - conn = self._get_connection() - # TODO: Refactor SQLiteSession to use asyncio.Lock instead of threading.Lock and update this code # noqa: E501 - with self._lock if self._is_memory_db else threading.Lock(): - # Get the IDs of messages we just inserted, in order - with closing(conn.cursor()) as cursor: - cursor.execute( - f"SELECT id FROM {self.messages_table} " - f"WHERE session_id = ? ORDER BY id DESC LIMIT ?", - (self.session_id, len(items)), - ) - message_ids = [row[0] for row in cursor.fetchall()] - message_ids.reverse() # Match order of items - - # Get current max sequence number (global) - with closing(conn.cursor()) as cursor: - cursor.execute( - """ - SELECT COALESCE(MAX(sequence_number), 0) - FROM message_structure - WHERE session_id = ? - """, - (self.session_id,), - ) - seq_start = cursor.fetchone()[0] - - # Get current turn numbers atomically with a single query - with closing(conn.cursor()) as cursor: - cursor.execute( - """ - SELECT - COALESCE(MAX(user_turn_number), 0) as max_global_turn, - COALESCE(MAX(branch_turn_number), 0) as max_branch_turn - FROM message_structure - WHERE session_id = ? AND branch_id = ? - """, - (self.session_id, self._current_branch_id), - ) - result = cursor.fetchone() - current_turn = result[0] if result else 0 - current_branch_turn = result[1] if result else 0 - - # Process items and assign turn numbers correctly - structure_data = [] - user_message_count = 0 - - for i, (item, msg_id) in enumerate(zip(items, message_ids)): - msg_type = self._classify_message_type(item) - tool_name = self._extract_tool_name(item) - - # If this is a user message, increment turn counters - if self._is_user_message(item): - user_message_count += 1 - item_turn = current_turn + user_message_count - item_branch_turn = current_branch_turn + user_message_count - else: - # Non-user messages inherit the turn number of the most recent user message - item_turn = current_turn + user_message_count - item_branch_turn = current_branch_turn + user_message_count - - structure_data.append( - ( - self.session_id, - msg_id, - self._current_branch_id, - msg_type, - seq_start + i + 1, # Global sequence - item_turn, # Global turn number - item_branch_turn, # Branch-specific turn number - tool_name, - ) - ) - - with closing(conn.cursor()) as cursor: - cursor.executemany( - """ - INSERT INTO message_structure - (session_id, message_id, branch_id, message_type, sequence_number, - user_turn_number, branch_turn_number, tool_name) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - """, - structure_data, - ) - conn.commit() + with self._locked_connection() as conn: + self._insert_structure_metadata(conn, items) + conn.commit() try: await asyncio.to_thread(_add_structure_sync) @@ -441,6 +378,94 @@ def _add_structure_sync(): self._logger.error(f"Failed to cleanup orphaned messages: {cleanup_error}") # Don't re-raise - structure metadata is supplementary + def _insert_structure_metadata( + self, + conn: sqlite3.Connection, + items: list[TResponseInputItem], + ) -> None: + # Get the IDs of messages we just inserted, in order. + with closing(conn.cursor()) as cursor: + cursor.execute( + f"SELECT id FROM {self.messages_table} " + f"WHERE session_id = ? ORDER BY id DESC LIMIT ?", + (self.session_id, len(items)), + ) + message_ids = [row[0] for row in cursor.fetchall()] + message_ids.reverse() + + if len(message_ids) != len(items): + raise RuntimeError( + "Failed to resolve inserted message IDs while writing structure metadata" + ) + + # Get current max sequence number (global). + with closing(conn.cursor()) as cursor: + cursor.execute( + """ + SELECT COALESCE(MAX(sequence_number), 0) + FROM message_structure + WHERE session_id = ? + """, + (self.session_id,), + ) + seq_start = cursor.fetchone()[0] + + # Get current turn numbers atomically with a single query. + with closing(conn.cursor()) as cursor: + cursor.execute( + """ + SELECT + COALESCE(MAX(user_turn_number), 0) as max_global_turn, + COALESCE(MAX(branch_turn_number), 0) as max_branch_turn + FROM message_structure + WHERE session_id = ? AND branch_id = ? + """, + (self.session_id, self._current_branch_id), + ) + result = cursor.fetchone() + current_turn = result[0] if result else 0 + current_branch_turn = result[1] if result else 0 + + # Process items and assign turn numbers correctly. + structure_data = [] + user_message_count = 0 + + for i, (item, msg_id) in enumerate(zip(items, message_ids)): + msg_type = self._classify_message_type(item) + tool_name = self._extract_tool_name(item) + + if self._is_user_message(item): + user_message_count += 1 + item_turn = current_turn + user_message_count + item_branch_turn = current_branch_turn + user_message_count + else: + item_turn = current_turn + user_message_count + item_branch_turn = current_branch_turn + user_message_count + + structure_data.append( + ( + self.session_id, + msg_id, + self._current_branch_id, + msg_type, + seq_start + i + 1, + item_turn, + item_branch_turn, + tool_name, + ) + ) + + with closing(conn.cursor()) as cursor: + cursor.executemany( + """ + INSERT INTO message_structure + (session_id, message_id, branch_id, message_type, sequence_number, + user_turn_number, branch_turn_number, tool_name) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, + structure_data, + ) + async def _cleanup_orphaned_messages(self) -> int: """Remove messages that exist in the configured message table but not in message_structure. @@ -450,40 +475,43 @@ async def _cleanup_orphaned_messages(self) -> int: def _cleanup_sync(): """Synchronous helper to cleanup orphaned messages.""" - conn = self._get_connection() - # TODO: Refactor SQLiteSession to use asyncio.Lock instead of threading.Lock and update this code # noqa: E501 - with self._lock if self._is_memory_db else threading.Lock(): - with closing(conn.cursor()) as cursor: - # Find messages without structure metadata - cursor.execute( - f""" - SELECT am.id - FROM {self.messages_table} am - LEFT JOIN message_structure ms ON am.id = ms.message_id - WHERE am.session_id = ? AND ms.message_id IS NULL - """, - (self.session_id,), - ) + with self._locked_connection() as conn: + deleted_count = self._cleanup_orphaned_messages_sync(conn) + if deleted_count: + conn.commit() + else: + conn.rollback() + return deleted_count - orphaned_ids = [row[0] for row in cursor.fetchall()] + return await asyncio.to_thread(_cleanup_sync) - if orphaned_ids: - # Delete orphaned messages - placeholders = ",".join("?" * len(orphaned_ids)) - cursor.execute( - f"DELETE FROM {self.messages_table} WHERE id IN ({placeholders})", - orphaned_ids, - ) + def _cleanup_orphaned_messages_sync(self, conn: sqlite3.Connection) -> int: + with closing(conn.cursor()) as cursor: + # Find messages without structure metadata. + cursor.execute( + f""" + SELECT am.id + FROM {self.messages_table} am + LEFT JOIN message_structure ms ON am.id = ms.message_id + WHERE am.session_id = ? AND ms.message_id IS NULL + """, + (self.session_id,), + ) - deleted_count = cursor.rowcount - conn.commit() + orphaned_ids = [row[0] for row in cursor.fetchall()] - self._logger.info(f"Cleaned up {deleted_count} orphaned messages") - return deleted_count + if not orphaned_ids: + return 0 - return 0 + placeholders = ",".join("?" * len(orphaned_ids)) + cursor.execute( + f"DELETE FROM {self.messages_table} WHERE id IN ({placeholders})", + orphaned_ids, + ) - return await asyncio.to_thread(_cleanup_sync) + deleted_count = cursor.rowcount + self._logger.info(f"Cleaned up {deleted_count} orphaned messages") + return deleted_count def _classify_message_type(self, item: TResponseInputItem) -> str: """Classify the type of a message item. @@ -588,32 +616,32 @@ async def create_branch_from_turn( # Validate the turn exists and contains a user message def _validate_turn(): """Synchronous helper to validate turn exists and contains user message.""" - conn = self._get_connection() - with closing(conn.cursor()) as cursor: - cursor.execute( - f""" - SELECT am.message_data - FROM message_structure ms - JOIN {self.messages_table} am ON ms.message_id = am.id - WHERE ms.session_id = ? AND ms.branch_id = ? - AND ms.branch_turn_number = ? AND ms.message_type = 'user' - """, - (self.session_id, self._current_branch_id, turn_number), - ) - - result = cursor.fetchone() - if not result: - raise ValueError( - f"Turn {turn_number} does not contain a user message " - f"in branch '{self._current_branch_id}'" + with self._locked_connection() as conn: + with closing(conn.cursor()) as cursor: + cursor.execute( + f""" + SELECT am.message_data + FROM message_structure ms + JOIN {self.messages_table} am ON ms.message_id = am.id + WHERE ms.session_id = ? AND ms.branch_id = ? + AND ms.branch_turn_number = ? AND ms.message_type = 'user' + """, + (self.session_id, self._current_branch_id, turn_number), ) - message_data = result[0] - try: - content = json.loads(message_data).get("content", "") - return content[:50] + "..." if len(content) > 50 else content - except Exception: - return "Unable to parse content" + result = cursor.fetchone() + if not result: + raise ValueError( + f"Turn {turn_number} does not contain a user message " + f"in branch '{self._current_branch_id}'" + ) + + message_data = result[0] + try: + content = json.loads(message_data).get("content", "") + return content[:50] + "..." if len(content) > 50 else content + except Exception: + return "Unable to parse content" turn_content = await asyncio.to_thread(_validate_turn) @@ -670,19 +698,19 @@ async def switch_to_branch(self, branch_id: str) -> None: # Validate branch exists def _validate_branch(): """Synchronous helper to validate branch exists.""" - conn = self._get_connection() - with closing(conn.cursor()) as cursor: - cursor.execute( - """ - SELECT COUNT(*) FROM message_structure - WHERE session_id = ? AND branch_id = ? - """, - (self.session_id, branch_id), - ) + with self._locked_connection() as conn: + with closing(conn.cursor()) as cursor: + cursor.execute( + """ + SELECT COUNT(*) FROM message_structure + WHERE session_id = ? AND branch_id = ? + """, + (self.session_id, branch_id), + ) - count = cursor.fetchone()[0] - if count == 0: - raise ValueError(f"Branch '{branch_id}' does not exist") + count = cursor.fetchone()[0] + if count == 0: + raise ValueError(f"Branch '{branch_id}' does not exist") await asyncio.to_thread(_validate_branch) @@ -721,9 +749,7 @@ async def delete_branch(self, branch_id: str, force: bool = False) -> None: def _delete_sync(): """Synchronous helper to delete branch and associated data.""" - conn = self._get_connection() - # TODO: Refactor SQLiteSession to use asyncio.Lock instead of threading.Lock and update this code # noqa: E501 - with self._lock if self._is_memory_db else threading.Lock(): + with self._locked_connection() as conn: with closing(conn.cursor()) as cursor: # First verify the branch exists cursor.execute( @@ -784,37 +810,37 @@ async def list_branches(self) -> list[dict[str, Any]]: def _list_branches_sync(): """Synchronous helper to list all branches.""" - conn = self._get_connection() - with closing(conn.cursor()) as cursor: - cursor.execute( - """ - SELECT - ms.branch_id, - COUNT(*) as message_count, - COUNT(CASE WHEN ms.message_type = 'user' THEN 1 END) as user_turns, - MIN(ms.created_at) as created_at - FROM message_structure ms - WHERE ms.session_id = ? - GROUP BY ms.branch_id - ORDER BY created_at - """, - (self.session_id,), - ) - - branches = [] - for row in cursor.fetchall(): - branch_id, msg_count, user_turns, created_at = row - branches.append( - { - "branch_id": branch_id, - "message_count": msg_count, - "user_turns": user_turns, - "is_current": branch_id == self._current_branch_id, - "created_at": created_at, - } + with self._locked_connection() as conn: + with closing(conn.cursor()) as cursor: + cursor.execute( + """ + SELECT + ms.branch_id, + COUNT(*) as message_count, + COUNT(CASE WHEN ms.message_type = 'user' THEN 1 END) as user_turns, + MIN(ms.created_at) as created_at + FROM message_structure ms + WHERE ms.session_id = ? + GROUP BY ms.branch_id + ORDER BY created_at + """, + (self.session_id,), ) - return branches + branches = [] + for row in cursor.fetchall(): + branch_id, msg_count, user_turns, created_at = row + branches.append( + { + "branch_id": branch_id, + "message_count": msg_count, + "user_turns": user_turns, + "is_current": branch_id == self._current_branch_id, + "created_at": created_at, + } + ) + + return branches return await asyncio.to_thread(_list_branches_sync) @@ -828,9 +854,7 @@ async def _copy_messages_to_new_branch(self, new_branch_id: str, from_turn_numbe def _copy_sync(): """Synchronous helper to copy messages to new branch.""" - conn = self._get_connection() - # TODO: Refactor SQLiteSession to use asyncio.Lock instead of threading.Lock and update this code # noqa: E501 - with self._lock if self._is_memory_db else threading.Lock(): + with self._locked_connection() as conn: with closing(conn.cursor()) as cursor: # Get all messages before the branch point cursor.execute( @@ -921,41 +945,43 @@ async def get_conversation_turns(self, branch_id: str | None = None) -> list[dic def _get_turns_sync(): """Synchronous helper to get conversation turns.""" - conn = self._get_connection() - with closing(conn.cursor()) as cursor: - cursor.execute( - f""" - SELECT - ms.branch_turn_number, - am.message_data, - ms.created_at - FROM message_structure ms - JOIN {self.messages_table} am ON ms.message_id = am.id - WHERE ms.session_id = ? AND ms.branch_id = ? - AND ms.message_type = 'user' - ORDER BY ms.branch_turn_number - """, - (self.session_id, branch_id), - ) + with self._locked_connection() as conn: + with closing(conn.cursor()) as cursor: + cursor.execute( + f""" + SELECT + ms.branch_turn_number, + am.message_data, + ms.created_at + FROM message_structure ms + JOIN {self.messages_table} am ON ms.message_id = am.id + WHERE ms.session_id = ? AND ms.branch_id = ? + AND ms.message_type = 'user' + ORDER BY ms.branch_turn_number + """, + (self.session_id, branch_id), + ) - turns = [] - for row in cursor.fetchall(): - turn_num, message_data, created_at = row - try: - content = json.loads(message_data).get("content", "") - turns.append( - { - "turn": turn_num, - "content": content[:100] + "..." if len(content) > 100 else content, - "full_content": content, - "timestamp": created_at, - "can_branch": True, - } - ) - except (json.JSONDecodeError, AttributeError): - continue + turns = [] + for row in cursor.fetchall(): + turn_num, message_data, created_at = row + try: + content = json.loads(message_data).get("content", "") + turns.append( + { + "turn": turn_num, + "content": ( + content[:100] + "..." if len(content) > 100 else content + ), + "full_content": content, + "timestamp": created_at, + "can_branch": True, + } + ) + except (json.JSONDecodeError, AttributeError): + continue - return turns + return turns return await asyncio.to_thread(_get_turns_sync) @@ -976,42 +1002,42 @@ async def find_turns_by_content( def _search_sync(): """Synchronous helper to search turns by content.""" - conn = self._get_connection() - with closing(conn.cursor()) as cursor: - cursor.execute( - f""" - SELECT - ms.branch_turn_number, - am.message_data, - ms.created_at - FROM message_structure ms - JOIN {self.messages_table} am ON ms.message_id = am.id - WHERE ms.session_id = ? AND ms.branch_id = ? - AND ms.message_type = 'user' - AND am.message_data LIKE ? - ORDER BY ms.branch_turn_number - """, - (self.session_id, branch_id, f"%{search_term}%"), - ) + with self._locked_connection() as conn: + with closing(conn.cursor()) as cursor: + cursor.execute( + f""" + SELECT + ms.branch_turn_number, + am.message_data, + ms.created_at + FROM message_structure ms + JOIN {self.messages_table} am ON ms.message_id = am.id + WHERE ms.session_id = ? AND ms.branch_id = ? + AND ms.message_type = 'user' + AND am.message_data LIKE ? + ORDER BY ms.branch_turn_number + """, + (self.session_id, branch_id, f"%{search_term}%"), + ) - matches = [] - for row in cursor.fetchall(): - turn_num, message_data, created_at = row - try: - content = json.loads(message_data).get("content", "") - matches.append( - { - "turn": turn_num, - "content": content, - "full_content": content, - "timestamp": created_at, - "can_branch": True, - } - ) - except (json.JSONDecodeError, AttributeError): - continue + matches = [] + for row in cursor.fetchall(): + turn_num, message_data, created_at = row + try: + content = json.loads(message_data).get("content", "") + matches.append( + { + "turn": turn_num, + "content": content, + "full_content": content, + "timestamp": created_at, + "can_branch": True, + } + ) + except (json.JSONDecodeError, AttributeError): + continue - return matches + return matches return await asyncio.to_thread(_search_sync) @@ -1031,25 +1057,25 @@ async def get_conversation_by_turns( def _get_conversation_sync(): """Synchronous helper to get conversation by turns.""" - conn = self._get_connection() - with closing(conn.cursor()) as cursor: - cursor.execute( - """ - SELECT user_turn_number, message_type, tool_name - FROM message_structure - WHERE session_id = ? AND branch_id = ? - ORDER BY sequence_number - """, - (self.session_id, branch_id), - ) + with self._locked_connection() as conn: + with closing(conn.cursor()) as cursor: + cursor.execute( + """ + SELECT user_turn_number, message_type, tool_name + FROM message_structure + WHERE session_id = ? AND branch_id = ? + ORDER BY sequence_number + """, + (self.session_id, branch_id), + ) - turns: dict[int, list[dict[str, str | None]]] = {} - for row in cursor.fetchall(): - turn_num, msg_type, tool_name = row - if turn_num not in turns: - turns[turn_num] = [] - turns[turn_num].append({"type": msg_type, "tool_name": tool_name}) - return turns + turns: dict[int, list[dict[str, str | None]]] = {} + for row in cursor.fetchall(): + turn_num, msg_type, tool_name = row + if turn_num not in turns: + turns[turn_num] = [] + turns[turn_num].append({"type": msg_type, "tool_name": tool_name}) + return turns return await asyncio.to_thread(_get_conversation_sync) @@ -1067,47 +1093,47 @@ async def get_tool_usage(self, branch_id: str | None = None) -> list[tuple[str, def _get_tool_usage_sync(): """Synchronous helper to get tool usage statistics.""" - conn = self._get_connection() - with closing(conn.cursor()) as cursor: - cursor.execute( - """ - SELECT tool_name, SUM(usage_count), user_turn_number - FROM ( - SELECT tool_name, 1 AS usage_count, user_turn_number - FROM message_structure - WHERE session_id = ? AND branch_id = ? AND message_type IN ( - 'tool_call', 'function_call', 'computer_call', 'file_search_call', - 'web_search_call', 'code_interpreter_call', 'tool_search_call', - 'custom_tool_call', 'mcp_call', 'mcp_approval_request' - ) - - UNION ALL + with self._locked_connection() as conn: + with closing(conn.cursor()) as cursor: + cursor.execute( + """ + SELECT tool_name, SUM(usage_count), user_turn_number + FROM ( + SELECT tool_name, 1 AS usage_count, user_turn_number + FROM message_structure + WHERE session_id = ? AND branch_id = ? AND message_type IN ( + 'tool_call', 'function_call', 'computer_call', 'file_search_call', + 'web_search_call', 'code_interpreter_call', 'tool_search_call', + 'custom_tool_call', 'mcp_call', 'mcp_approval_request' + ) - SELECT ms.tool_name, 1 AS usage_count, ms.user_turn_number - FROM message_structure ms - WHERE ms.session_id = ? AND ms.branch_id = ? - AND ms.message_type = 'tool_search_output' - AND NOT EXISTS ( - SELECT 1 - FROM message_structure calls - WHERE calls.session_id = ms.session_id - AND calls.branch_id = ms.branch_id - AND calls.user_turn_number = ms.user_turn_number - AND calls.tool_name = ms.tool_name - AND calls.message_type = 'tool_search_call' - ) + UNION ALL + + SELECT ms.tool_name, 1 AS usage_count, ms.user_turn_number + FROM message_structure ms + WHERE ms.session_id = ? AND ms.branch_id = ? + AND ms.message_type = 'tool_search_output' + AND NOT EXISTS ( + SELECT 1 + FROM message_structure calls + WHERE calls.session_id = ms.session_id + AND calls.branch_id = ms.branch_id + AND calls.user_turn_number = ms.user_turn_number + AND calls.tool_name = ms.tool_name + AND calls.message_type = 'tool_search_call' + ) + ) + GROUP BY tool_name, user_turn_number + ORDER BY user_turn_number + """, + ( + self.session_id, + branch_id, + self.session_id, + branch_id, + ), ) - GROUP BY tool_name, user_turn_number - ORDER BY user_turn_number - """, - ( - self.session_id, - branch_id, - self.session_id, - branch_id, - ), - ) - return cursor.fetchall() + return cursor.fetchall() return await asyncio.to_thread(_get_tool_usage_sync) @@ -1123,9 +1149,7 @@ async def get_session_usage(self, branch_id: str | None = None) -> dict[str, int def _get_usage_sync(): """Synchronous helper to get session usage data.""" - conn = self._get_connection() - # TODO: Refactor SQLiteSession to use asyncio.Lock instead of threading.Lock and update this code # noqa: E501 - with self._lock if self._is_memory_db else threading.Lock(): + with self._locked_connection() as conn: if branch_id: # Branch-specific usage query = """ @@ -1191,47 +1215,46 @@ async def get_turn_usage( def _get_turn_usage_sync(): """Synchronous helper to get turn usage statistics.""" - conn = self._get_connection() - - if user_turn_number is not None: - query = """ - SELECT requests, input_tokens, output_tokens, total_tokens, - input_tokens_details, output_tokens_details - FROM turn_usage - WHERE session_id = ? AND branch_id = ? AND user_turn_number = ? - """ - - with closing(conn.cursor()) as cursor: - cursor.execute(query, (self.session_id, branch_id, user_turn_number)) - row = cursor.fetchone() - - if row: - # Parse JSON details if present - input_details = None - output_details = None - - if row[4]: # input_tokens_details - try: - input_details = json.loads(row[4]) - except json.JSONDecodeError: - pass + with self._locked_connection() as conn: + if user_turn_number is not None: + query = """ + SELECT requests, input_tokens, output_tokens, total_tokens, + input_tokens_details, output_tokens_details + FROM turn_usage + WHERE session_id = ? AND branch_id = ? AND user_turn_number = ? + """ - if row[5]: # output_tokens_details - try: - output_details = json.loads(row[5]) - except json.JSONDecodeError: - pass + with closing(conn.cursor()) as cursor: + cursor.execute(query, (self.session_id, branch_id, user_turn_number)) + row = cursor.fetchone() + + if row: + # Parse JSON details if present + input_details = None + output_details = None + + if row[4]: # input_tokens_details + try: + input_details = json.loads(row[4]) + except json.JSONDecodeError: + pass + + if row[5]: # output_tokens_details + try: + output_details = json.loads(row[5]) + except json.JSONDecodeError: + pass + + return { + "requests": row[0], + "input_tokens": row[1], + "output_tokens": row[2], + "total_tokens": row[3], + "input_tokens_details": input_details, + "output_tokens_details": output_details, + } + return {} - return { - "requests": row[0], - "input_tokens": row[1], - "output_tokens": row[2], - "total_tokens": row[3], - "input_tokens_details": input_details, - "output_tokens_details": output_details, - } - return {} - else: query = """ SELECT user_turn_number, requests, input_tokens, output_tokens, total_tokens, input_tokens_details, output_tokens_details @@ -1287,9 +1310,7 @@ async def _update_turn_usage_internal(self, user_turn_number: int, usage_data: U def _update_sync(): """Synchronous helper to update turn usage data.""" - conn = self._get_connection() - # TODO: Refactor SQLiteSession to use asyncio.Lock instead of threading.Lock and update this code # noqa: E501 - with self._lock if self._is_memory_db else threading.Lock(): + with self._locked_connection() as conn: # Serialize token details as JSON input_details_json = None output_details_json = None diff --git a/src/agents/memory/sqlite_session.py b/src/agents/memory/sqlite_session.py index 92c9630c9b..d0ca2557a2 100644 --- a/src/agents/memory/sqlite_session.py +++ b/src/agents/memory/sqlite_session.py @@ -4,7 +4,10 @@ import json import sqlite3 import threading +from collections.abc import Iterator +from contextlib import contextmanager from pathlib import Path +from typing import ClassVar from ..items import TResponseInputItem from .session import SessionABC @@ -20,6 +23,9 @@ class SQLiteSession(SessionABC): """ session_settings: SessionSettings | None = None + _file_locks: ClassVar[dict[Path, threading.RLock]] = {} + _file_lock_counts: ClassVar[dict[Path, int]] = {} + _file_locks_guard: ClassVar[threading.Lock] = threading.Lock() def __init__( self, @@ -46,21 +52,66 @@ def __init__( self.sessions_table = sessions_table self.messages_table = messages_table self._local = threading.local() - self._lock = threading.Lock() # For in-memory databases, we need a shared connection to avoid thread isolation # For file databases, we use thread-local connections for better concurrency self._is_memory_db = str(db_path) == ":memory:" + self._lock_path: Path | None = None + self._lock_released = False if self._is_memory_db: - self._shared_connection = sqlite3.connect(":memory:", check_same_thread=False) - self._shared_connection.execute("PRAGMA journal_mode=WAL") - self._init_db_for_connection(self._shared_connection) + self._lock = threading.RLock() else: - # For file databases, initialize the schema once since it persists - init_conn = sqlite3.connect(str(self.db_path), check_same_thread=False) - init_conn.execute("PRAGMA journal_mode=WAL") - self._init_db_for_connection(init_conn) - init_conn.close() + self._lock_path, self._lock = self._acquire_file_lock(Path(self.db_path)) + + try: + if self._is_memory_db: + self._shared_connection = sqlite3.connect(":memory:", check_same_thread=False) + self._shared_connection.execute("PRAGMA journal_mode=WAL") + self._init_db_for_connection(self._shared_connection) + else: + # For file databases, initialize the schema once since it persists + with self._lock: + init_conn = sqlite3.connect(str(self.db_path), check_same_thread=False) + init_conn.execute("PRAGMA journal_mode=WAL") + self._init_db_for_connection(init_conn) + init_conn.close() + except Exception: + if self._lock_path is not None and not self._lock_released: + self._release_file_lock(self._lock_path) + self._lock_released = True + raise + + @classmethod + def _acquire_file_lock(cls, db_path: Path) -> tuple[Path, threading.RLock]: + """Return the path key and process-local lock for sessions sharing one SQLite file.""" + lock_path = db_path.expanduser().resolve() + with cls._file_locks_guard: + lock = cls._file_locks.get(lock_path) + if lock is None: + lock = threading.RLock() + cls._file_locks[lock_path] = lock + cls._file_lock_counts[lock_path] = 0 + cls._file_lock_counts[lock_path] += 1 + return lock_path, lock + + @classmethod + def _release_file_lock(cls, lock_path: Path) -> None: + """Drop the shared lock for a file-backed DB once the last session closes.""" + with cls._file_locks_guard: + ref_count = cls._file_lock_counts.get(lock_path) + if ref_count is None: + return + if ref_count <= 1: + cls._file_lock_counts.pop(lock_path, None) + cls._file_locks.pop(lock_path, None) + else: + cls._file_lock_counts[lock_path] = ref_count - 1 + + @contextmanager + def _locked_connection(self) -> Iterator[sqlite3.Connection]: + """Serialize sqlite3 access while each operation runs in a worker thread.""" + with self._lock: + yield self._get_connection() def _get_connection(self) -> sqlite3.Connection: """Get a database connection.""" @@ -114,6 +165,31 @@ def _init_db_for_connection(self, conn: sqlite3.Connection) -> None: conn.commit() + def _insert_items(self, conn: sqlite3.Connection, items: list[TResponseInputItem]) -> None: + conn.execute( + f""" + INSERT OR IGNORE INTO {self.sessions_table} (session_id) VALUES (?) + """, + (self.session_id,), + ) + + message_data = [(self.session_id, json.dumps(item)) for item in items] + conn.executemany( + f""" + INSERT INTO {self.messages_table} (session_id, message_data) VALUES (?, ?) + """, + message_data, + ) + + conn.execute( + f""" + UPDATE {self.sessions_table} + SET updated_at = CURRENT_TIMESTAMP + WHERE session_id = ? + """, + (self.session_id,), + ) + async def get_items(self, limit: int | None = None) -> list[TResponseInputItem]: """Retrieve the conversation history for this session. @@ -127,8 +203,7 @@ async def get_items(self, limit: int | None = None) -> list[TResponseInputItem]: session_limit = resolve_session_limit(limit, self.session_settings) def _get_items_sync(): - conn = self._get_connection() - with self._lock if self._is_memory_db else threading.Lock(): + with self._locked_connection() as conn: if session_limit is None: # Fetch all items in chronological order cursor = conn.execute( @@ -180,36 +255,8 @@ async def add_items(self, items: list[TResponseInputItem]) -> None: return def _add_items_sync(): - conn = self._get_connection() - - with self._lock if self._is_memory_db else threading.Lock(): - # Ensure session exists - conn.execute( - f""" - INSERT OR IGNORE INTO {self.sessions_table} (session_id) VALUES (?) - """, - (self.session_id,), - ) - - # Add items - message_data = [(self.session_id, json.dumps(item)) for item in items] - conn.executemany( - f""" - INSERT INTO {self.messages_table} (session_id, message_data) VALUES (?, ?) - """, - message_data, - ) - - # Update session timestamp - conn.execute( - f""" - UPDATE {self.sessions_table} - SET updated_at = CURRENT_TIMESTAMP - WHERE session_id = ? - """, - (self.session_id,), - ) - + with self._locked_connection() as conn: + self._insert_items(conn, items) conn.commit() await asyncio.to_thread(_add_items_sync) @@ -222,8 +269,7 @@ async def pop_item(self) -> TResponseInputItem | None: """ def _pop_item_sync(): - conn = self._get_connection() - with self._lock if self._is_memory_db else threading.Lock(): + with self._locked_connection() as conn: # Use DELETE with RETURNING to atomically delete and return the most recent item cursor = conn.execute( f""" @@ -259,8 +305,7 @@ async def clear_session(self) -> None: """Clear all items for this session.""" def _clear_session_sync(): - conn = self._get_connection() - with self._lock if self._is_memory_db else threading.Lock(): + with self._locked_connection() as conn: conn.execute( f"DELETE FROM {self.messages_table} WHERE session_id = ?", (self.session_id,), @@ -281,3 +326,6 @@ def close(self) -> None: else: if hasattr(self._local, "connection"): self._local.connection.close() + if self._lock_path is not None and not self._lock_released: + self._release_file_lock(self._lock_path) + self._lock_released = True diff --git a/tests/extensions/memory/test_advanced_sqlite_session.py b/tests/extensions/memory/test_advanced_sqlite_session.py index 7be57e6b00..b61c5235f0 100644 --- a/tests/extensions/memory/test_advanced_sqlite_session.py +++ b/tests/extensions/memory/test_advanced_sqlite_session.py @@ -1,5 +1,9 @@ """Tests for AdvancedSQLiteSession functionality.""" +import asyncio +import json +import tempfile +from pathlib import Path from typing import Any, Optional, cast import pytest @@ -1343,3 +1347,52 @@ async def test_runner_with_session_settings_override(agent: Agent): assert len(history_items) == 2 session.close() + + +async def test_concurrent_add_items_preserves_message_structure_for_file_db(): + """Concurrent add_items calls should keep agent_messages and message_structure aligned.""" + with tempfile.TemporaryDirectory() as temp_dir: + db_path = Path(temp_dir) / "advanced_concurrent.db" + session = AdvancedSQLiteSession( + session_id="advanced_concurrent", + db_path=db_path, + create_tables=True, + ) + + async def add_batch(worker_id: int) -> list[str]: + contents = [f"worker-{worker_id}-message-{index}" for index in range(10)] + await session.add_items([{"role": "user", "content": content} for content in contents]) + return contents + + expected_batches = await asyncio.gather(*(add_batch(worker_id) for worker_id in range(8))) + expected_contents = {content for batch in expected_batches for content in batch} + + retrieved_items = await session.get_items() + retrieved_contents = { + content + for item in retrieved_items + for content in [item.get("content")] + if isinstance(content, str) + } + + assert retrieved_contents == expected_contents + assert len(retrieved_items) == len(expected_contents) + + with session._locked_connection() as conn: + rows = conn.execute( + f""" + SELECT m.message_data + FROM {session.messages_table} m + JOIN message_structure s ON s.message_id = m.id + WHERE m.session_id = ? + ORDER BY s.sequence_number ASC + """, + (session.session_id,), + ).fetchall() + + structured_contents = {json.loads(message_data).get("content") for (message_data,) in rows} + + assert structured_contents == expected_contents + assert len(rows) == len(expected_contents) + + session.close() diff --git a/tests/test_session.py b/tests/test_session.py index aaa80ec7aa..8ede928812 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -537,6 +537,36 @@ def add_item(item): session.close() +@pytest.mark.asyncio +async def test_sqlite_session_file_lock_is_shared_across_instances(): + """File-backed sessions pointing at the same DB path should reuse one process-local lock.""" + with tempfile.TemporaryDirectory() as temp_dir: + db_path = Path(temp_dir) / "test_shared_lock.db" + lock_path = db_path.resolve() + + session_1 = SQLiteSession("session_1", db_path) + session_2 = SQLiteSession("session_2", db_path) + + assert session_1._lock is session_2._lock + assert SQLiteSession._file_lock_counts[lock_path] == 2 + + await asyncio.gather( + session_1.add_items([{"role": "user", "content": "session_1"}]), + session_2.add_items([{"role": "user", "content": "session_2"}]), + ) + + assert [item.get("content") for item in await session_1.get_items()] == ["session_1"] + assert [item.get("content") for item in await session_2.get_items()] == ["session_2"] + + session_1.close() + assert SQLiteSession._file_lock_counts[lock_path] == 1 + assert lock_path in SQLiteSession._file_locks + + session_2.close() + assert lock_path not in SQLiteSession._file_lock_counts + assert lock_path not in SQLiteSession._file_locks + + @pytest.mark.asyncio async def test_session_add_items_exception_propagates_in_streamed(): """Test that exceptions from session.add_items are properly propagated From 7a3f6b70d1bd4e700f3a06547b673295fb52e74d Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Sat, 4 Apr 2026 12:19:50 +0900 Subject: [PATCH 34/40] feat: #2135 add public flush_traces API (#2844) --- src/agents/__init__.py | 2 + src/agents/tracing/__init__.py | 12 +++ src/agents/tracing/processors.py | 34 ++++---- src/agents/tracing/provider.py | 28 ++++++- tests/test_trace_processor.py | 131 ++++++++++++++++++++++++++++++- 5 files changed, 186 insertions(+), 21 deletions(-) diff --git a/src/agents/__init__.py b/src/agents/__init__.py index 214e814d3e..54c739bbda 100644 --- a/src/agents/__init__.py +++ b/src/agents/__init__.py @@ -203,6 +203,7 @@ add_trace_processor, agent_span, custom_span, + flush_traces, function_span, gen_span_id, gen_trace_id, @@ -451,6 +452,7 @@ def enable_verbose_stdout_logging(): "add_trace_processor", "agent_span", "custom_span", + "flush_traces", "function_span", "generation_span", "get_current_span", diff --git a/src/agents/tracing/__init__.py b/src/agents/tracing/__init__.py index 9f5e4f7568..76a77fe0ab 100644 --- a/src/agents/tracing/__init__.py +++ b/src/agents/tracing/__init__.py @@ -42,6 +42,7 @@ "add_trace_processor", "agent_span", "custom_span", + "flush_traces", "function_span", "generation_span", "get_current_span", @@ -108,3 +109,14 @@ def set_tracing_export_api_key(api_key: str) -> None: Set the OpenAI API key for the backend exporter. """ default_exporter().set_api_key(api_key) + + +def flush_traces() -> None: + """Force immediate export of buffered traces and spans. + + The default ``BatchTraceProcessor`` already exports traces periodically in the + background. Call this when a worker, background job, or request handler needs + traces to be visible immediately after a unit of work finishes instead of + waiting for the next scheduled flush. + """ + get_trace_provider().force_flush() diff --git a/src/agents/tracing/processors.py b/src/agents/tracing/processors.py index 7132faf1c8..1f8cfa9d4e 100644 --- a/src/agents/tracing/processors.py +++ b/src/agents/tracing/processors.py @@ -491,6 +491,7 @@ def __init__( # We lazily start the background worker thread the first time a span/trace is queued. self._worker_thread: threading.Thread | None = None self._thread_start_lock = threading.Lock() + self._export_lock = threading.Lock() def _ensure_thread_started(self) -> None: # Fast path without holding the lock @@ -571,25 +572,26 @@ def _export_batches(self, force: bool = False): """Drains the queue and exports in batches. If force=True, export everything. Otherwise, export up to `max_batch_size` repeatedly until the queue is completely empty. """ - while True: - items_to_export: list[Span[Any] | Trace] = [] + with self._export_lock: + while True: + items_to_export: list[Span[Any] | Trace] = [] + + # Gather a batch of spans up to max_batch_size + while not self._queue.empty() and ( + force or len(items_to_export) < self._max_batch_size + ): + try: + items_to_export.append(self._queue.get_nowait()) + except queue.Empty: + # Another thread might have emptied the queue between checks + break - # Gather a batch of spans up to max_batch_size - while not self._queue.empty() and ( - force or len(items_to_export) < self._max_batch_size - ): - try: - items_to_export.append(self._queue.get_nowait()) - except queue.Empty: - # Another thread might have emptied the queue between checks + # If we collected nothing, we're done + if not items_to_export: break - # If we collected nothing, we're done - if not items_to_export: - break - - # Export the batch - self._exporter.export(items_to_export) + # Export the batch + self._exporter.export(items_to_export) # Lazily initialized defaults to avoid creating network clients or threading diff --git a/src/agents/tracing/provider.py b/src/agents/tracing/provider.py index 90ea85cbf0..e37841ddf2 100644 --- a/src/agents/tracing/provider.py +++ b/src/agents/tracing/provider.py @@ -188,9 +188,21 @@ def create_span( ) -> Span[TSpanData]: """Create a new span.""" - @abstractmethod + def force_flush(self) -> None: + """Force all registered processors to flush buffered traces/spans immediately. + + The default implementation is a no-op so existing custom ``TraceProvider`` + implementations continue to work without adding this method. + """ + return None + def shutdown(self) -> None: - """Clean up any resources used by the provider.""" + """Clean up any resources used by the provider. + + The default implementation is a no-op so existing custom ``TraceProvider`` + implementations continue to work without adding this method. + """ + return None class DefaultTraceProvider(TraceProvider): @@ -365,7 +377,19 @@ def create_span( trace_metadata=trace_metadata, ) + def force_flush(self) -> None: + """Force all processors to flush their buffers immediately.""" + self._refresh_disabled_flag() + if self._disabled: + return + + try: + self._multi_processor.force_flush() + except Exception as e: + logger.error(f"Error flushing trace provider: {e}") + def shutdown(self) -> None: + self._refresh_disabled_flag() if self._disabled: return diff --git a/tests/test_trace_processor.py b/tests/test_trace_processor.py index ad061d7995..1c917990d5 100644 --- a/tests/test_trace_processor.py +++ b/tests/test_trace_processor.py @@ -1,4 +1,5 @@ import os +import threading import time from typing import Any, cast from unittest.mock import MagicMock, patch @@ -6,11 +7,13 @@ import httpx import pytest -from agents.tracing.processor_interface import TracingProcessor +from agents.tracing import flush_traces, get_trace_provider +from agents.tracing.processor_interface import TracingExporter, TracingProcessor from agents.tracing.processors import BackendSpanExporter, BatchTraceProcessor +from agents.tracing.provider import DefaultTraceProvider, TraceProvider from agents.tracing.span_data import AgentSpanData -from agents.tracing.spans import SpanImpl -from agents.tracing.traces import TraceImpl +from agents.tracing.spans import Span, SpanImpl +from agents.tracing.traces import Trace, TraceImpl def get_span(processor: TracingProcessor) -> SpanImpl[AgentSpanData]: @@ -123,6 +126,34 @@ def test_batch_trace_processor_force_flush(mocked_exporter): processor.shutdown() +def test_batch_trace_processor_force_flush_waits_for_in_flight_background_export(): + export_started = threading.Event() + export_continue = threading.Event() + + class BlockingExporter(TracingExporter): + def export(self, items: list[Trace | Span[Any]]) -> None: + export_started.set() + assert export_continue.wait(timeout=2.0) + + processor = BatchTraceProcessor(exporter=BlockingExporter(), schedule_delay=0.01) + processor.on_trace_start(get_trace(processor)) + + assert export_started.wait(timeout=2.0) + + flush_thread = threading.Thread(target=processor.force_flush) + flush_thread.start() + + time.sleep(0.1) + assert flush_thread.is_alive(), "force_flush() should wait for an in-flight export" + + export_continue.set() + flush_thread.join(timeout=2.0) + + assert not flush_thread.is_alive() + + processor.shutdown() + + def test_batch_trace_processor_shutdown_flushes(mocked_exporter): processor = BatchTraceProcessor(exporter=mocked_exporter, schedule_delay=5.0) processor.on_trace_start(get_trace(processor)) @@ -171,6 +202,100 @@ def test_batch_trace_processor_scheduled_export(mocked_exporter): assert total_exported == 1, "Item should be exported after scheduled delay" +def test_flush_traces_delegates_to_default_trace_provider(): + provider = DefaultTraceProvider() + mock_processor = MagicMock() + provider.register_processor(mock_processor) + + with patch("agents.tracing.setup.GLOBAL_TRACE_PROVIDER", provider): + flush_traces() + + mock_processor.force_flush.assert_called_once() + + +def test_flush_traces_is_importable_from_top_level_agents_package(): + from agents import flush_traces as top_level_flush_traces + + assert top_level_flush_traces is flush_traces + + +def test_default_trace_provider_force_flush_respects_disabled_flag(): + provider = DefaultTraceProvider() + mock_processor = MagicMock() + provider.register_processor(mock_processor) + + provider.set_disabled(True) + provider.force_flush() + + mock_processor.force_flush.assert_not_called() + + +def test_trace_provider_force_flush_and_shutdown_default_to_noops(): + class MinimalProvider(TraceProvider): + def register_processor(self, processor: TracingProcessor) -> None: + pass + + def set_processors(self, processors: list[TracingProcessor]) -> None: + pass + + def get_current_trace(self): + return None + + def get_current_span(self): + return None + + def set_disabled(self, disabled: bool) -> None: + pass + + def time_iso(self) -> str: + return "" + + def gen_trace_id(self) -> str: + return "trace_123" + + def gen_span_id(self) -> str: + return "span_123" + + def gen_group_id(self) -> str: + return "group_123" + + def create_trace( + self, + name, + trace_id=None, + group_id=None, + metadata=None, + disabled=False, + tracing=None, + ): + raise NotImplementedError + + def create_span(self, span_data, span_id=None, parent=None, disabled=False): + raise NotImplementedError + + provider = MinimalProvider() + provider.force_flush() + provider.shutdown() + + +def test_get_trace_provider_force_flush_flushes_default_processor(mocked_exporter): + provider = DefaultTraceProvider() + processor = BatchTraceProcessor(exporter=mocked_exporter, schedule_delay=60.0) + provider.register_processor(processor) + + with patch("agents.tracing.setup.GLOBAL_TRACE_PROVIDER", provider): + processor.on_trace_start(get_trace(processor)) + processor.on_span_end(get_span(processor)) + + get_trace_provider().force_flush() + + total_exported = sum( + len(call_args[0][0]) for call_args in mocked_exporter.export.call_args_list + ) + assert total_exported == 2 + processor.shutdown() + + @pytest.fixture def patched_time_sleep(): """ From b0ab25c2d6796c7699492dd2289efa509aae3705 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 13:10:33 +0900 Subject: [PATCH 35/40] Release 0.13.5 (#2821) --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 015ca452e9..87758b68e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openai-agents" -version = "0.13.4" +version = "0.13.5" description = "OpenAI Agents SDK" readme = "README.md" requires-python = ">=3.10" diff --git a/uv.lock b/uv.lock index f3471e2c85..da5b4621da 100644 --- a/uv.lock +++ b/uv.lock @@ -1902,7 +1902,7 @@ wheels = [ [[package]] name = "openai-agents" -version = "0.13.4" +version = "0.13.5" source = { editable = "." } dependencies = [ { name = "griffelib" }, From e1d861ca5076a262e395344bbcfc95857cedaf78 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Mon, 6 Apr 2026 13:12:14 +0900 Subject: [PATCH 36/40] docs: updates for #2844 changes (#2845) --- docs/tracing.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/docs/tracing.md b/docs/tracing.md index 15cdfd6c31..43d0951da9 100644 --- a/docs/tracing.md +++ b/docs/tracing.md @@ -44,6 +44,57 @@ By default, the trace is named "Agent workflow". You can set this name if you us In addition, you can set up [custom trace processors](#custom-tracing-processors) to push traces to other destinations (as a replacement, or secondary destination). +## Long-running workers and immediate exports + +The default [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor] exports traces +in the background every few seconds, or sooner when the in-memory queue reaches its size trigger, +and also performs a final flush when the process exits. In long-running workers such as Celery, +RQ, Dramatiq, or FastAPI background tasks, this means traces are usually exported automatically +without any extra code, but they may not appear in the Traces dashboard immediately after each job +finishes. + +If you need an immediate delivery guarantee at the end of a unit of work, call +[`flush_traces()`][agents.tracing.flush_traces] after the trace context exits. + +```python +from agents import Runner, flush_traces, trace + + +@celery_app.task +def run_agent_task(prompt: str): + try: + with trace("celery_task"): + result = Runner.run_sync(agent, prompt) + return result.final_output + finally: + flush_traces() +``` + +```python +from fastapi import BackgroundTasks, FastAPI +from agents import Runner, flush_traces, trace + +app = FastAPI() + + +def process_in_background(prompt: str) -> None: + try: + with trace("background_job"): + Runner.run_sync(agent, prompt) + finally: + flush_traces() + + +@app.post("/run") +async def run(prompt: str, background_tasks: BackgroundTasks): + background_tasks.add_task(process_in_background, prompt) + return {"status": "queued"} +``` + +[`flush_traces()`][agents.tracing.flush_traces] blocks until currently buffered traces and spans are +exported, so call it after `trace()` closes to avoid flushing a partially built trace. You can skip +this call when the default export latency is acceptable. + ## Higher level traces Sometimes, you might want multiple calls to `run()` to be part of a single trace. You can do this by wrapping the entire code in a `trace()`. From fa508992edc6640f2ccf6addfbdb30c98f109b5d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 13:32:35 +0900 Subject: [PATCH 37/40] docs: update translated document pages (#2847) --- docs/ja/tracing.md | 144 ++++++++++++++++++++++++++++++-------------- docs/ko/tracing.md | 123 +++++++++++++++++++++++++------------- docs/zh/tracing.md | 146 ++++++++++++++++++++++++++++++--------------- 3 files changed, 279 insertions(+), 134 deletions(-) diff --git a/docs/ja/tracing.md b/docs/ja/tracing.md index 84930611a5..dd512baa20 100644 --- a/docs/ja/tracing.md +++ b/docs/ja/tracing.md @@ -4,53 +4,105 @@ search: --- # トレーシング -Agents SDK には組み込みのトレーシングが含まれており、エージェント実行中のイベント( LLM 生成、ツール呼び出し、ハンドオフ、ガードレール、さらに発生したカスタムイベント)を包括的に記録します。[Traces ダッシュボード](https://platform.openai.com/traces) を使用すると、開発中および本番環境でワークフローをデバッグ、可視化、監視できます。 +Agents SDK には組み込みのトレーシングがあり、エージェント実行中のイベントを包括的に記録します: LLM 生成、ツール呼び出し、ハンドオフ、ガードレール、さらに発生したカスタムイベントも含まれます。[Traces ダッシュボード](https://platform.openai.com/traces) を使うことで、開発中および本番環境でワークフローをデバッグ、可視化、監視できます。 !!!note - トレーシングはデフォルトで有効です。一般的には次の 3 つの方法で無効化できます。 + トレーシングはデフォルトで有効です。一般的な無効化方法は 3 つあります: 1. 環境変数 `OPENAI_AGENTS_DISABLE_TRACING=1` を設定して、グローバルにトレーシングを無効化できます - 2. [`set_tracing_disabled(True)`][agents.set_tracing_disabled] を使って、コード上でグローバルにトレーシングを無効化できます + 2. [`set_tracing_disabled(True)`][agents.set_tracing_disabled] を使って、コード内でグローバルにトレーシングを無効化できます 3. [`agents.run.RunConfig.tracing_disabled`][] を `True` に設定して、単一の実行でトレーシングを無効化できます -***OpenAI の API を使用し、Zero Data Retention ( ZDR ) ポリシーの下で運用している組織では、トレーシングは利用できません。*** +***OpenAI の API を使用し、Zero Data Retention (ZDR) ポリシー下で運用している組織では、トレーシングは利用できません。*** -## Traces と spans +## トレースとスパン -- **Traces** は 1 つの「ワークフロー」のエンドツーエンドの単一操作を表します。これは Span で構成されます。Traces には次のプロパティがあります。 - - `workflow_name`: 論理的なワークフローまたはアプリです。例: 「Code generation」や「Customer service」。 - - `trace_id`: トレースの一意な ID です。指定しない場合は自動生成されます。形式は `trace_<32_alphanumeric>` である必要があります。 - - `group_id`: 任意のグループ ID で、同じ会話内の複数のトレースを関連付けます。たとえば、チャットスレッド ID を使用できます。 - - `disabled`: True の場合、そのトレースは記録されません。 - - `metadata`: トレースの任意のメタデータです。 -- **Spans** は開始時刻と終了時刻を持つ操作を表します。Spans には次が含まれます。 +- **トレース**は 1 つの「ワークフロー」におけるエンドツーエンドの単一操作を表します。トレースはスパンで構成されます。トレースには次のプロパティがあります: + - `workflow_name`: 論理的なワークフローまたはアプリです。たとえば「Code generation」や「Customer service」です。 + - `trace_id`: トレースの一意 ID です。指定しない場合は自動生成されます。形式は `trace_<32_alphanumeric>` である必要があります。 + - `group_id`: 同じ会話の複数トレースを関連付けるための任意のグループ ID です。たとえば、チャットスレッド ID を使えます。 + - `disabled`: True の場合、トレースは記録されません。 + - `metadata`: トレースの任意メタデータです。 +- **スパン**は開始時刻と終了時刻を持つ操作を表します。スパンには次があります: - `started_at` と `ended_at` のタイムスタンプ。 - - `trace_id`(所属するトレースを表します) - - `parent_id`(この Span の親 Span を指します。存在する場合) - - `span_data`( Span に関する情報)。たとえば、`AgentSpanData` は Agent の情報、`GenerationSpanData` は LLM 生成の情報を含みます。 + - 属するトレースを表す `trace_id` + - このスパンの親スパンを指す `parent_id`(存在する場合) + - スパンに関する情報である `span_data`。たとえば `AgentSpanData` には Agent の情報、`GenerationSpanData` には LLM 生成の情報などが含まれます。 ## デフォルトトレーシング -デフォルトで、 SDK は次をトレースします。 +デフォルトで SDK は次をトレースします: - `Runner.{run, run_sync, run_streamed}()` 全体は `trace()` でラップされます。 -- エージェントが実行されるたびに、`agent_span()` でラップされます +- エージェントが実行されるたびに `agent_span()` でラップされます - LLM 生成は `generation_span()` でラップされます - 関数ツール呼び出しはそれぞれ `function_span()` でラップされます - ガードレールは `guardrail_span()` でラップされます - ハンドオフは `handoff_span()` でラップされます -- 音声入力( speech-to-text )は `transcription_span()` でラップされます -- 音声出力( text-to-speech )は `speech_span()` でラップされます -- 関連する音声 Span は `speech_group_span()` の配下になる場合があります +- 音声入力(speech-to-text)は `transcription_span()` でラップされます +- 音声出力(text-to-speech)は `speech_span()` でラップされます +- 関連する音声スパンは `speech_group_span()` の子になる場合があります -デフォルトで、トレース名は「Agent workflow」です。`trace` を使用する場合はこの名前を設定できます。また、[`RunConfig`][agents.run.RunConfig] で名前やその他のプロパティを設定することもできます。 +デフォルトでトレース名は「Agent workflow」です。`trace` を使う場合はこの名前を設定できます。または [`RunConfig`][agents.run.RunConfig] で名前やその他のプロパティを設定できます。 -さらに、[カスタムトレースプロセッサー](#custom-tracing-processors) を設定して、トレースを他の送信先にプッシュできます(置き換えまたは副次的な送信先として)。 +さらに、[カスタムトレースプロセッサー](#custom-tracing-processors) を設定して、トレースを他の宛先へ送信できます(置き換えまたは副次的な宛先として)。 + +## 長時間稼働ワーカーと即時エクスポート + +デフォルトの [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor] は、 +数秒ごとにバックグラウンドでトレースをエクスポートし、メモリー内キューがサイズしきい値に達した場合はより早く実行し、 +さらにプロセス終了時に最終フラッシュも実行します。Celery、 +RQ、Dramatiq、または FastAPI バックグラウンドタスクのような長時間稼働ワーカーでは、これにより通常は追加コードなしで +トレースが自動的にエクスポートされますが、各ジョブ完了直後には Traces ダッシュボードに +表示されない場合があります。 + +作業単位の終了時に即時配信保証が必要な場合は、 +トレースコンテキスト終了後に [`flush_traces()`][agents.tracing.flush_traces] を呼び出してください。 + +```python +from agents import Runner, flush_traces, trace + + +@celery_app.task +def run_agent_task(prompt: str): + try: + with trace("celery_task"): + result = Runner.run_sync(agent, prompt) + return result.final_output + finally: + flush_traces() +``` + +```python +from fastapi import BackgroundTasks, FastAPI +from agents import Runner, flush_traces, trace + +app = FastAPI() + + +def process_in_background(prompt: str) -> None: + try: + with trace("background_job"): + Runner.run_sync(agent, prompt) + finally: + flush_traces() + + +@app.post("/run") +async def run(prompt: str, background_tasks: BackgroundTasks): + background_tasks.add_task(process_in_background, prompt) + return {"status": "queued"} +``` + +[`flush_traces()`][agents.tracing.flush_traces] は現在バッファされているトレースとスパンが +エクスポートされるまでブロックするため、部分的に構築されたトレースをフラッシュしないよう +`trace()` が閉じた後で呼び出してください。デフォルトのエクスポート遅延を許容できる場合は、 +この呼び出しは省略できます。 ## 上位レベルトレース -場合によっては、`run()` の複数回呼び出しを 1 つのトレースの一部にしたいことがあります。これは、コード全体を `trace()` でラップすることで実現できます。 +場合によっては、複数の `run()` 呼び出しを単一のトレースの一部にしたいことがあります。これは、コード全体を `trace()` でラップすることで実現できます。 ```python from agents import Agent, Runner, trace @@ -65,49 +117,49 @@ async def main(): print(f"Rating: {second_result.final_output}") ``` -1. `Runner.run` の 2 回の呼び出しは `with trace()` でラップされているため、2 つのトレースを作成するのではなく、個々の実行が全体トレースの一部になります。 +1. `Runner.run` への 2 回の呼び出しが `with trace()` でラップされているため、個々の実行は 2 つのトレースを作成するのではなく、全体トレースの一部になります。 ## トレース作成 -[`trace()`][agents.tracing.trace] 関数を使用してトレースを作成できます。トレースは開始と終了が必要です。方法は 2 つあります。 +[`trace()`][agents.tracing.trace] 関数を使ってトレースを作成できます。トレースは開始と終了が必要です。方法は 2 つあります: -1. **推奨**: トレースをコンテキストマネージャーとして使います(例: `with trace(...) as my_trace`)。これにより、適切なタイミングでトレースが自動的に開始・終了されます。 +1. **推奨**: `with trace(...) as my_trace` のように、トレースをコンテキストマネージャーとして使います。これにより適切なタイミングで自動的にトレースが開始・終了されます。 2. [`trace.start()`][agents.tracing.Trace.start] と [`trace.finish()`][agents.tracing.Trace.finish] を手動で呼び出すこともできます。 -現在のトレースは Python の [`contextvar`](https://docs.python.org/3/library/contextvars.html) を通じて追跡されます。これは並行処理でも自動的に機能することを意味します。トレースを手動で開始・終了する場合は、現在のトレースを更新するために `start()`/`finish()` に `mark_as_current` と `reset_current` を渡す必要があります。 +現在のトレースは Python の [`contextvar`](https://docs.python.org/3/library/contextvars.html) で追跡されます。これは並行処理でも自動的に機能することを意味します。トレースを手動で開始/終了する場合は、現在のトレースを更新するために `start()`/`finish()` に `mark_as_current` と `reset_current` を渡す必要があります。 -## Span 作成 +## スパン作成 -さまざまな [`*_span()`][agents.tracing.create] メソッドを使って Span を作成できます。一般に、Span を手動で作成する必要はありません。カスタム Span 情報を追跡するための [`custom_span()`][agents.tracing.custom_span] 関数も利用できます。 +さまざまな [`*_span()`][agents.tracing.create] メソッドを使ってスパンを作成できます。一般的には、スパンを手動で作成する必要はありません。カスタムスパン情報を追跡するための [`custom_span()`][agents.tracing.custom_span] 関数も利用できます。 -Span は自動的に現在のトレースの一部となり、最も近い現在の Span の配下にネストされます。これは Python の [`contextvar`](https://docs.python.org/3/library/contextvars.html) で追跡されます。 +スパンは自動的に現在のトレースの一部となり、最も近い現在のスパンの下にネストされます。これは Python の [`contextvar`](https://docs.python.org/3/library/contextvars.html) で追跡されます。 -## 機微データ +## 機密データ -一部の Span では、機微データが含まれる可能性があります。 +一部のスパンは機密性の可能性があるデータを取得する場合があります。 -`generation_span()` は LLM 生成の入出力を保存し、`function_span()` は関数呼び出しの入出力を保存します。これらには機微データが含まれる可能性があるため、[`RunConfig.trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data] でそのデータの収集を無効化できます。 +`generation_span()` は LLM 生成の入力/出力を保存し、`function_span()` は関数呼び出しの入力/出力を保存します。これらには機密データが含まれる可能性があるため、[`RunConfig.trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data] を使ってそのデータ取得を無効化できます。 -同様に、Audio Span にはデフォルトで入力・出力音声の base64 エンコードされた PCM データが含まれます。[`VoicePipelineConfig.trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data] を設定することで、この音声データの収集を無効化できます。 +同様に、Audio スパンにはデフォルトで入力/出力音声の base64 エンコード済み PCM データが含まれます。[`VoicePipelineConfig.trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data] を設定して、この音声データの取得を無効化できます。 -デフォルトでは、`trace_include_sensitive_data` は `True` です。アプリ実行前に環境変数 `OPENAI_AGENTS_TRACE_INCLUDE_SENSITIVE_DATA` を `true/1` または `false/0` に設定することで、コードなしでデフォルト値を設定できます。 +デフォルトでは `trace_include_sensitive_data` は `True` です。アプリ実行前に環境変数 `OPENAI_AGENTS_TRACE_INCLUDE_SENSITIVE_DATA` を `true/1` または `false/0` に設定することで、コードを書かずにデフォルトを設定できます。 ## カスタムトレーシングプロセッサー -トレーシングの高レベルアーキテクチャは次のとおりです。 +トレーシングの高レベルアーキテクチャは次のとおりです: -- 初期化時に、トレース作成を担うグローバル [`TraceProvider`][agents.tracing.setup.TraceProvider] を作成します。 -- `TraceProvider` に [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor] を設定します。これはトレース / Span をバッチで [`BackendSpanExporter`][agents.tracing.processors.BackendSpanExporter] に送信し、さらに Span とトレースを OpenAI バックエンドへバッチでエクスポートします。 +- 初期化時に、トレース作成を担当するグローバル [`TraceProvider`][agents.tracing.setup.TraceProvider] を作成します。 +- `TraceProvider` に [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor] を設定し、これがトレース/スパンをバッチで [`BackendSpanExporter`][agents.tracing.processors.BackendSpanExporter] に送信します。`BackendSpanExporter` はスパンとトレースをバッチで OpenAI バックエンドにエクスポートします。 -このデフォルト設定をカスタマイズして、別のまたは追加のバックエンドにトレースを送信したり、エクスポーターの動作を変更したりするには、次の 2 つの方法があります。 +このデフォルト設定をカスタマイズして、代替または追加のバックエンドにトレースを送信したり、エクスポーターの動作を変更したりするには、次の 2 つの方法があります: -1. [`add_trace_processor()`][agents.tracing.add_trace_processor] を使うと、準備できたトレースと Span を受け取る**追加の**トレースプロセッサーを追加できます。これにより、OpenAI バックエンドへの送信に加えて独自の処理を行えます。 -2. [`set_trace_processors()`][agents.tracing.set_trace_processors] を使うと、デフォルトプロセッサーを独自のトレースプロセッサーで**置き換え**できます。これは、`TracingProcessor` を含めない限り、トレースが OpenAI バックエンドへ送信されないことを意味します。 +1. [`add_trace_processor()`][agents.tracing.add_trace_processor] は、準備でき次第トレースとスパンを受け取る**追加**のトレースプロセッサーを追加できます。これにより、OpenAI バックエンドへの送信に加えて独自処理を行えます。 +2. [`set_trace_processors()`][agents.tracing.set_trace_processors] は、デフォルトプロセッサーを独自のトレースプロセッサーで**置き換える**ことができます。これは、そうする `TracingProcessor` を含めない限り、トレースが OpenAI バックエンドに送信されないことを意味します。 ## 非 OpenAI モデルでのトレーシング -トレーシングを無効化しなくても、OpenAI 以外のモデルで OpenAI API キーを使用して OpenAI Traces ダッシュボードで無料トレーシングを有効化できます。アダプター選択とセットアップ時の注意点については、Models ガイドの [Third-party adapters](models/index.md#third-party-adapters) セクションを参照してください。 +トレーシングを無効化せずに OpenAI Traces ダッシュボードで無料トレーシングを有効化するために、非 OpenAI モデルで OpenAI API キーを使用できます。アダプター選択と設定時の注意点については、Models ガイドの [Third-party adapters](models/index.md#third-party-adapters) セクションを参照してください。 ```python import os @@ -128,7 +180,7 @@ agent = Agent( ) ``` -単一の実行にのみ別のトレーシングキーが必要な場合は、グローバルエクスポーターを変更する代わりに `RunConfig` 経由で渡してください。 +単一実行にのみ別のトレーシングキーが必要な場合は、グローバルエクスポーターを変更する代わりに `RunConfig` で渡してください。 ```python from agents import Runner, RunConfig @@ -140,13 +192,13 @@ await Runner.run( ) ``` -## 追加メモ -- Openai Traces ダッシュボードで無料トレースを表示します。 +## 追加ノート +- Openai Traces ダッシュボードで無料トレースを確認できます。 ## エコシステム統合 -以下のコミュニティおよびベンダーの統合は、OpenAI Agents SDK のトレーシング機能をサポートしています。 +以下のコミュニティおよびベンダー統合は、OpenAI Agents SDK のトレーシングサーフェスをサポートしています。 ### 外部トレーシングプロセッサー一覧 diff --git a/docs/ko/tracing.md b/docs/ko/tracing.md index cd360544b7..5579463a83 100644 --- a/docs/ko/tracing.md +++ b/docs/ko/tracing.md @@ -4,31 +4,31 @@ search: --- # 트레이싱 -Agents SDK에는 기본 제공 트레이싱이 포함되어 있으며, 에이전트 실행 중 발생하는 이벤트의 포괄적인 기록을 수집합니다: LLM 생성, 도구 호출, 핸드오프, 가드레일, 그리고 발생한 사용자 정의 이벤트까지 포함됩니다. [Traces 대시보드](https://platform.openai.com/traces)를 사용하면 개발 중과 프로덕션에서 워크플로를 디버그, 시각화, 모니터링할 수 있습니다. +Agents SDK에는 내장 트레이싱이 포함되어 있으며, 에이전트 실행 중 발생하는 이벤트(LLM 생성, 도구 호출, 핸드오프, 가드레일, 사용자 정의 이벤트 포함)에 대한 포괄적인 기록을 수집합니다. [Traces 대시보드](https://platform.openai.com/traces)를 사용하면 개발 중과 프로덕션에서 워크플로를 디버깅, 시각화, 모니터링할 수 있습니다. !!!note - 트레이싱은 기본적으로 활성화되어 있습니다. 일반적으로 다음 세 가지 방법으로 비활성화할 수 있습니다: + 트레이싱은 기본적으로 활성화되어 있습니다. 다음 세 가지 일반적인 방법으로 비활성화할 수 있습니다: - 1. 환경 변수 `OPENAI_AGENTS_DISABLE_TRACING=1` 을 설정해 전역으로 트레이싱을 비활성화할 수 있습니다 - 2. 코드에서 [`set_tracing_disabled(True)`][agents.set_tracing_disabled]로 전역 트레이싱을 비활성화할 수 있습니다 - 3. 단일 실행에 대해 [`agents.run.RunConfig.tracing_disabled`][]를 `True`로 설정해 트레이싱을 비활성화할 수 있습니다 + 1. 환경 변수 `OPENAI_AGENTS_DISABLE_TRACING=1`을 설정하여 전역적으로 트레이싱을 비활성화할 수 있습니다 + 2. 코드에서 [`set_tracing_disabled(True)`][agents.set_tracing_disabled]로 전역적으로 트레이싱을 비활성화할 수 있습니다 + 3. 단일 실행에 대해 [`agents.run.RunConfig.tracing_disabled`][]를 `True`로 설정하여 트레이싱을 비활성화할 수 있습니다 -***OpenAI API를 사용하면서 ZDR(Zero Data Retention) 정책으로 운영되는 조직에서는 트레이싱을 사용할 수 없습니다.*** +***OpenAI API를 사용하면서 Zero Data Retention(ZDR) 정책을 적용하는 조직에서는 트레이싱을 사용할 수 없습니다.*** ## 트레이스와 스팬 -- **트레이스**는 "워크플로"의 단일 엔드투엔드 작업을 나타냅니다. 트레이스는 스팬으로 구성됩니다. 트레이스에는 다음 속성이 있습니다: +- **트레이스**는 하나의 "워크플로"에 대한 단일 end-to-end 작업을 나타냅니다. 트레이스는 스팬으로 구성됩니다. 트레이스에는 다음 속성이 있습니다: - `workflow_name`: 논리적 워크플로 또는 앱입니다. 예: "Code generation" 또는 "Customer service" - - `trace_id`: 트레이스의 고유 ID입니다. 전달하지 않으면 자동 생성됩니다. 형식은 `trace_<32_alphanumeric>` 이어야 합니다 - - `group_id`: 선택적 그룹 ID로, 같은 대화의 여러 트레이스를 연결합니다. 예를 들어 채팅 스레드 ID를 사용할 수 있습니다 + - `trace_id`: 트레이스의 고유 ID입니다. 전달하지 않으면 자동 생성됩니다. 형식은 `trace_<32_alphanumeric>`이어야 합니다 + - `group_id`: 선택적 그룹 ID로, 동일한 대화의 여러 트레이스를 연결합니다. 예를 들어 채팅 스레드 ID를 사용할 수 있습니다 - `disabled`: True이면 트레이스가 기록되지 않습니다 - - `metadata`: 트레이스에 대한 선택적 메타데이터입니다 + - `metadata`: 트레이스용 선택적 메타데이터입니다 - **스팬**은 시작 시간과 종료 시간이 있는 작업을 나타냅니다. 스팬에는 다음이 있습니다: - `started_at` 및 `ended_at` 타임스탬프 - - `trace_id`: 해당 스팬이 속한 트레이스를 나타냅니다 - - `parent_id`: 이 스팬의 부모 스팬을 가리킵니다(있는 경우) - - `span_data`: 스팬에 대한 정보입니다. 예를 들어 `AgentSpanData`에는 에이전트 정보가, `GenerationSpanData`에는 LLM 생성 정보가 포함됩니다 + - `trace_id`: 해당 스팬이 속한 트레이스를 나타냄 + - `parent_id`: 이 스팬의 상위 스팬을 가리킴(있는 경우) + - `span_data`: 스팬에 대한 정보입니다. 예를 들어 `AgentSpanData`는 에이전트 정보, `GenerationSpanData`는 LLM 생성 정보 등을 포함합니다 ## 기본 트레이싱 @@ -40,17 +40,60 @@ Agents SDK에는 기본 제공 트레이싱이 포함되어 있으며, 에이전 - 함수 도구 호출은 각각 `function_span()`으로 감싸집니다 - 가드레일은 `guardrail_span()`으로 감싸집니다 - 핸드오프는 `handoff_span()`으로 감싸집니다 -- 오디오 입력(음성-텍스트)은 `transcription_span()`으로 감싸집니다 -- 오디오 출력(텍스트-음성)은 `speech_span()`으로 감싸집니다 -- 관련 오디오 스팬은 `speech_group_span()` 아래에 부모-자식으로 연결될 수 있습니다 +- 오디오 입력(speech-to-text)은 `transcription_span()`으로 감싸집니다 +- 오디오 출력(text-to-speech)은 `speech_span()`으로 감싸집니다 +- 관련 오디오 스팬은 `speech_group_span()` 아래에 부모-자식으로 배치될 수 있습니다 -기본적으로 트레이스 이름은 "Agent workflow"입니다. `trace`를 사용하면 이 이름을 설정할 수 있고, [`RunConfig`][agents.run.RunConfig]로 이름과 기타 속성을 구성할 수도 있습니다. +기본적으로 트레이스 이름은 "Agent workflow"입니다. `trace`를 사용하면 이 이름을 설정할 수 있고, [`RunConfig`][agents.run.RunConfig]를 통해 이름과 기타 속성을 구성할 수도 있습니다. -또한 [사용자 정의 트레이스 프로세서](#custom-tracing-processors)를 설정하여 트레이스를 다른 대상(대체 또는 보조 대상)으로 전송할 수 있습니다. +또한 [사용자 정의 트레이스 프로세서](#custom-tracing-processors)를 설정해 트레이스를 다른 대상으로 전송할 수 있습니다(대체 또는 보조 대상). + +## 장기 실행 워커와 즉시 내보내기 + +기본 [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor]는 몇 초마다 백그라운드에서 트레이스를 내보내거나, 메모리 내 큐가 크기 임계값에 도달하면 더 빨리 내보내며, 프로세스 종료 시 최종 flush도 수행합니다. Celery, RQ, Dramatiq 또는 FastAPI 백그라운드 작업 같은 장기 실행 워커에서는 보통 추가 코드 없이도 트레이스가 자동으로 내보내지지만, 각 작업이 끝난 직후 Traces 대시보드에 즉시 표시되지 않을 수 있습니다. + +작업 단위 종료 시 즉시 전달 보장이 필요하다면, 트레이스 컨텍스트가 종료된 후 [`flush_traces()`][agents.tracing.flush_traces]를 호출하세요. + +```python +from agents import Runner, flush_traces, trace + + +@celery_app.task +def run_agent_task(prompt: str): + try: + with trace("celery_task"): + result = Runner.run_sync(agent, prompt) + return result.final_output + finally: + flush_traces() +``` + +```python +from fastapi import BackgroundTasks, FastAPI +from agents import Runner, flush_traces, trace + +app = FastAPI() + + +def process_in_background(prompt: str) -> None: + try: + with trace("background_job"): + Runner.run_sync(agent, prompt) + finally: + flush_traces() + + +@app.post("/run") +async def run(prompt: str, background_tasks: BackgroundTasks): + background_tasks.add_task(process_in_background, prompt) + return {"status": "queued"} +``` + +[`flush_traces()`][agents.tracing.flush_traces]는 현재 버퍼링된 트레이스와 스팬이 내보내질 때까지 블로킹하므로, 부분적으로 구성된 트레이스를 flush하지 않으려면 `trace()`가 닫힌 뒤 호출해야 합니다. 기본 내보내기 지연이 허용 가능한 경우 이 호출은 생략할 수 있습니다. ## 상위 수준 트레이스 -때로는 `run()` 여러 호출을 단일 트레이스의 일부로 만들고 싶을 수 있습니다. 이 경우 전체 코드를 `trace()`로 감싸면 됩니다. +경우에 따라 `run()` 호출 여러 개를 하나의 트레이스로 묶고 싶을 수 있습니다. 이 경우 전체 코드를 `trace()`로 감싸면 됩니다. ```python from agents import Agent, Runner, trace @@ -65,49 +108,49 @@ async def main(): print(f"Rating: {second_result.final_output}") ``` -1. `Runner.run`에 대한 두 호출이 `with trace()`로 감싸져 있으므로, 각 실행은 두 개의 트레이스를 생성하는 대신 전체 트레이스의 일부가 됩니다 +1. `Runner.run`에 대한 두 호출이 `with trace()`로 감싸져 있으므로, 각 실행은 별도의 트레이스 두 개를 만드는 대신 전체 트레이스의 일부가 됩니다 ## 트레이스 생성 -[`trace()`][agents.tracing.trace] 함수를 사용해 트레이스를 생성할 수 있습니다. 트레이스는 시작 및 종료되어야 하며, 이를 위한 두 가지 방법이 있습니다: +[`trace()`][agents.tracing.trace] 함수를 사용해 트레이스를 생성할 수 있습니다. 트레이스는 시작과 종료가 필요하며, 이를 위한 두 가지 방법이 있습니다: -1. **권장**: 트레이스를 컨텍스트 매니저로 사용합니다. 즉 `with trace(...) as my_trace` 형태입니다. 이렇게 하면 적절한 시점에 트레이스가 자동으로 시작되고 종료됩니다 -2. [`trace.start()`][agents.tracing.Trace.start]와 [`trace.finish()`][agents.tracing.Trace.finish]를 수동으로 호출할 수도 있습니다 +1. **권장**: 트레이스를 컨텍스트 매니저로 사용합니다. 즉 `with trace(...) as my_trace` 형태입니다. 이렇게 하면 적절한 시점에 트레이스가 자동으로 시작/종료됩니다 +2. [`trace.start()`][agents.tracing.Trace.start]와 [`trace.finish()`][agents.tracing.Trace.finish]를 수동 호출할 수도 있습니다 -현재 트레이스는 Python [`contextvar`](https://docs.python.org/3/library/contextvars.html)를 통해 추적됩니다. 즉, 동시성에서도 자동으로 동작합니다. 트레이스를 수동으로 시작/종료하는 경우 현재 트레이스를 업데이트하려면 `start()`/`finish()`에 `mark_as_current` 및 `reset_current`를 전달해야 합니다. +현재 트레이스는 Python [`contextvar`](https://docs.python.org/3/library/contextvars.html)를 통해 추적됩니다. 즉, 동시성 환경에서도 자동으로 동작합니다. 트레이스를 수동으로 시작/종료하는 경우 현재 트레이스를 갱신하려면 `start()`/`finish()`에 `mark_as_current`와 `reset_current`를 전달해야 합니다. ## 스팬 생성 -다양한 [`*_span()`][agents.tracing.create] 메서드를 사용해 스팬을 생성할 수 있습니다. 일반적으로 스팬을 수동으로 생성할 필요는 없습니다. 사용자 정의 스팬 정보를 추적하기 위한 [`custom_span()`][agents.tracing.custom_span] 함수도 제공됩니다. +다양한 [`*_span()`][agents.tracing.create] 메서드를 사용해 스팬을 생성할 수 있습니다. 일반적으로 스팬을 수동으로 만들 필요는 없습니다. 사용자 정의 스팬 정보를 추적하기 위해 [`custom_span()`][agents.tracing.custom_span] 함수를 사용할 수 있습니다. -스팬은 자동으로 현재 트레이스의 일부가 되며, Python [`contextvar`](https://docs.python.org/3/library/contextvars.html)로 추적되는 가장 가까운 현재 스팬 아래에 중첩됩니다. +스팬은 자동으로 현재 트레이스에 속하며, Python [`contextvar`](https://docs.python.org/3/library/contextvars.html)로 추적되는 가장 가까운 현재 스팬 아래에 중첩됩니다. ## 민감한 데이터 -특정 스팬은 잠재적으로 민감한 데이터를 캡처할 수 있습니다. +일부 스팬은 잠재적으로 민감한 데이터를 캡처할 수 있습니다. -`generation_span()`은 LLM 생성의 입력/출력을 저장하고, `function_span()`은 함수 호출의 입력/출력을 저장합니다. 여기에는 민감한 데이터가 포함될 수 있으므로 [`RunConfig.trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data]를 통해 해당 데이터 캡처를 비활성화할 수 있습니다. +`generation_span()`은 LLM 생성의 입력/출력을 저장하고, `function_span()`은 함수 호출의 입력/출력을 저장합니다. 여기에 민감한 데이터가 포함될 수 있으므로 [`RunConfig.trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data]를 통해 해당 데이터 캡처를 비활성화할 수 있습니다. -마찬가지로, 오디오 스팬은 기본적으로 입력 및 출력 오디오에 대한 base64 인코딩 PCM 데이터를 포함합니다. [`VoicePipelineConfig.trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data]를 구성하여 이 오디오 데이터 캡처를 비활성화할 수 있습니다. +마찬가지로 오디오 스팬에는 기본적으로 입력/출력 오디오에 대한 base64 인코딩 PCM 데이터가 포함됩니다. [`VoicePipelineConfig.trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data]를 구성해 이 오디오 데이터 캡처를 비활성화할 수 있습니다. -기본적으로 `trace_include_sensitive_data`는 `True`입니다. 코드 없이 기본값을 설정하려면 앱 실행 전에 `OPENAI_AGENTS_TRACE_INCLUDE_SENSITIVE_DATA` 환경 변수를 `true/1` 또는 `false/0`으로 export하면 됩니다. +기본적으로 `trace_include_sensitive_data`는 `True`입니다. 코드를 변경하지 않고도 앱 실행 전에 `OPENAI_AGENTS_TRACE_INCLUDE_SENSITIVE_DATA` 환경 변수를 `true/1` 또는 `false/0`으로 내보내 기본값을 설정할 수 있습니다. ## 사용자 정의 트레이싱 프로세서 -트레이싱의 상위 아키텍처는 다음과 같습니다: +트레이싱의 상위 수준 아키텍처는 다음과 같습니다: -- 초기화 시, 트레이스를 생성하는 역할을 담당하는 전역 [`TraceProvider`][agents.tracing.setup.TraceProvider]를 생성합니다 -- `TraceProvider`를 [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor]로 구성하고, 이 프로세서는 트레이스/스팬을 배치로 [`BackendSpanExporter`][agents.tracing.processors.BackendSpanExporter]에 전송합니다. `BackendSpanExporter`는 스팬과 트레이스를 배치로 OpenAI 백엔드에 내보냅니다 +- 초기화 시 트레이스 생성을 담당하는 전역 [`TraceProvider`][agents.tracing.setup.TraceProvider]를 생성합니다 +- `TraceProvider`를 [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor]로 구성하고, 이 프로세서는 트레이스/스팬을 배치로 [`BackendSpanExporter`][agents.tracing.processors.BackendSpanExporter]에 전송하며, `BackendSpanExporter`는 스팬과 트레이스를 배치로 OpenAI 백엔드에 내보냅니다 -이 기본 설정을 사용자 정의해 트레이스를 대체 또는 추가 백엔드로 전송하거나 exporter 동작을 수정하려면 두 가지 방법이 있습니다: +이 기본 구성을 사용자 지정하여 대체 또는 추가 백엔드로 트레이스를 보내거나 exporter 동작을 수정하려면 두 가지 방법이 있습니다: -1. [`add_trace_processor()`][agents.tracing.add_trace_processor]를 사용하면 준비되는 즉시 트레이스와 스팬을 수신하는 **추가** 트레이스 프로세서를 추가할 수 있습니다. 이를 통해 OpenAI 백엔드로 전송하는 것에 더해 자체 처리를 수행할 수 있습니다 -2. [`set_trace_processors()`][agents.tracing.set_trace_processors]를 사용하면 기본 프로세서를 사용자 정의 트레이스 프로세서로 **대체**할 수 있습니다. 즉, 이를 수행하는 `TracingProcessor`를 포함하지 않으면 트레이스는 OpenAI 백엔드로 전송되지 않습니다 +1. [`add_trace_processor()`][agents.tracing.add_trace_processor]를 사용하면 준비된 트레이스와 스팬을 수신하는 **추가** 트레이스 프로세서를 더할 수 있습니다. 이를 통해 OpenAI 백엔드 전송과 더불어 자체 처리를 수행할 수 있습니다 +2. [`set_trace_processors()`][agents.tracing.set_trace_processors]를 사용하면 기본 프로세서를 사용자 정의 트레이스 프로세서로 **교체**할 수 있습니다. 이 경우 해당 동작을 수행하는 `TracingProcessor`를 포함하지 않으면 트레이스가 OpenAI 백엔드로 전송되지 않습니다 -## 비 OpenAI 모델에서의 트레이싱 +## non-OpenAI 모델에서의 트레이싱 -OpenAI API 키를 비 OpenAI 모델과 함께 사용하면 트레이싱을 비활성화하지 않고도 OpenAI Traces 대시보드에서 무료 트레이싱을 사용할 수 있습니다. 어댑터 선택 및 설정 시 주의사항은 Models 가이드의 [서드파티 어댑터](models/index.md#third-party-adapters) 섹션을 참고하세요. +트레이싱을 비활성화할 필요 없이 OpenAI Traces 대시보드에서 무료 트레이싱을 활성화하려면 non-OpenAI 모델에 OpenAI API 키를 사용할 수 있습니다. 어댑터 선택 및 설정 시 주의사항은 Models 가이드의 [Third-party adapters](models/index.md#third-party-adapters) 섹션을 참조하세요. ```python import os @@ -128,7 +171,7 @@ agent = Agent( ) ``` -단일 실행에만 다른 트레이싱 키가 필요하다면 전역 exporter를 변경하는 대신 `RunConfig`를 통해 전달하세요. +단일 실행에만 다른 트레이싱 키가 필요하다면 전역 exporter를 변경하는 대신 `RunConfig`로 전달하세요. ```python from agents import Runner, RunConfig @@ -141,7 +184,7 @@ await Runner.run( ``` ## 추가 참고 사항 -- Openai Traces 대시보드에서 무료 트레이스를 확인하세요 +- Openai Traces dashboard에서 무료 트레이스를 확인하세요 ## 에코시스템 통합 diff --git a/docs/zh/tracing.md b/docs/zh/tracing.md index 6580b0f350..fc8173fd39 100644 --- a/docs/zh/tracing.md +++ b/docs/zh/tracing.md @@ -4,53 +4,103 @@ search: --- # 追踪 -Agents SDK 内置了追踪功能,可在智能体运行期间收集完整的事件记录:LLM 生成、工具调用、任务转移、安全防护措施,甚至发生的自定义事件。使用[Traces 仪表板](https://platform.openai.com/traces),你可以在开发和生产中调试、可视化并监控你的工作流。 +Agents SDK 包含内置追踪功能,可在智能体运行期间收集完整的事件记录:LLM 生成、工具调用、任务转移、安全防护措施,甚至发生的自定义事件。使用[Traces 控制台](https://platform.openai.com/traces),你可以在开发和生产阶段调试、可视化并监控你的工作流。 !!!note - 默认启用追踪。你可以通过三种常见方式禁用: + 追踪默认启用。你可以通过三种常见方式禁用: - 1. 通过设置环境变量 `OPENAI_AGENTS_DISABLE_TRACING=1` 全局禁用追踪 - 2. 在代码中使用 [`set_tracing_disabled(True)`][agents.set_tracing_disabled] 全局禁用追踪 - 3. 通过将 [`agents.run.RunConfig.tracing_disabled`][] 设为 `True`,为单次运行禁用追踪 + 1. 你可以通过设置环境变量 `OPENAI_AGENTS_DISABLE_TRACING=1` 全局禁用追踪 + 2. 你可以在代码中通过 [`set_tracing_disabled(True)`][agents.set_tracing_disabled] 全局禁用追踪 + 3. 你可以通过将 [`agents.run.RunConfig.tracing_disabled`][] 设为 `True` 来禁用单次运行的追踪 -***对于在 OpenAI API 上使用零数据保留(ZDR)策略运行的组织,追踪不可用。*** +***对于在 OpenAI API 下使用零数据保留(ZDR)策略的组织,追踪功能不可用。*** ## Traces 与 spans -- **Traces** 表示“工作流”的一次端到端操作。它由 Span 组成。Trace 具有以下属性: - - `workflow_name`:逻辑工作流或应用。例如“代码生成”或“客户服务”。 +- **Traces** 表示“工作流”的一次端到端操作。它们由 Span 组成。Traces 具有以下属性: + - `workflow_name`:这是逻辑工作流或应用。例如“代码生成”或“客户服务”。 - `trace_id`:Trace 的唯一 ID。如果你未传入则自动生成。格式必须为 `trace_<32_alphanumeric>`。 - - `group_id`:可选分组 ID,用于关联同一会话中的多个 Trace。例如,你可以使用聊天线程 ID。 - - `disabled`:若为 True,则不会记录该 Trace。 + - `group_id`:可选的组 ID,用于关联同一会话中的多个 Trace。例如,你可以使用聊天线程 ID。 + - `disabled`:如果为 True,则不会记录该 Trace。 - `metadata`:Trace 的可选元数据。 -- **Spans** 表示具有开始和结束时间的操作。Span 具有: +- **Spans** 表示具有开始和结束时间的操作。Spans 包含: - `started_at` 和 `ended_at` 时间戳。 - `trace_id`,表示其所属的 Trace - - `parent_id`,指向该 Span 的父 Span(若有) + - `parent_id`,指向该 Span 的父 Span(如果有) - `span_data`,即 Span 的信息。例如,`AgentSpanData` 包含智能体信息,`GenerationSpanData` 包含 LLM 生成信息,等等。 ## 默认追踪 默认情况下,SDK 会追踪以下内容: -- 整个 `Runner.{run, run_sync, run_streamed}()` 被包裹在 `trace()` 中。 -- 每次智能体运行时,都会包裹在 `agent_span()` 中 -- LLM 生成包裹在 `generation_span()` 中 -- 每次函数工具调用都包裹在 `function_span()` 中 -- 安全防护措施包裹在 `guardrail_span()` 中 -- 任务转移包裹在 `handoff_span()` 中 -- 音频输入(语音转文本)包裹在 `transcription_span()` 中 -- 音频输出(文本转语音)包裹在 `speech_span()` 中 +- 整个 `Runner.{run, run_sync, run_streamed}()` 会包裹在 `trace()` 中。 +- 每次智能体运行都会包裹在 `agent_span()` 中 +- LLM 生成会包裹在 `generation_span()` 中 +- 每次工具调用都会分别包裹在 `function_span()` 中 +- 安全防护措施会包裹在 `guardrail_span()` 中 +- 任务转移会包裹在 `handoff_span()` 中 +- 音频输入(语音转文本)会包裹在 `transcription_span()` 中 +- 音频输出(文本转语音)会包裹在 `speech_span()` 中 - 相关音频 Span 可能作为 `speech_group_span()` 的子级 -默认情况下,Trace 名称为“Agent workflow”。如果你使用 `trace`,可以设置该名称;也可以通过[`RunConfig`][agents.run.RunConfig]配置名称和其他属性。 +默认情况下,Trace 名称为“Agent workflow”。如果你使用 `trace`,可以设置该名称;你也可以通过 [`RunConfig`][agents.run.RunConfig] 配置名称和其他属性。 -此外,你还可以设置[自定义追踪进程](#custom-tracing-processors),将追踪发送到其他目标(作为替代目标或次要目标)。 +此外,你还可以设置[自定义追踪进程](#custom-tracing-processors),将 Trace 推送到其他目标(作为替代或次级目标)。 -## 更高层级的追踪 +## 长时间运行的 worker 与立即导出 -有时,你可能希望将多次 `run()` 调用纳入同一个 Trace。你可以通过将整段代码包裹在 `trace()` 中来实现。 +默认的 [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor] 会在后台每隔几秒导出一次追踪, +或在内存队列达到大小触发阈值时更早导出, +并且在进程退出时执行最终 flush。在 Celery、 +RQ、Dramatiq 或 FastAPI 后台任务等长时间运行的 worker 中,这意味着追踪通常会自动导出, +无需额外代码,但每个任务刚结束后它们可能不会立即出现在 Traces 控制台中。 + +如果你需要在一个工作单元结束时立即送达的保证,请调用 +[`flush_traces()`][agents.tracing.flush_traces](在 trace 上下文退出后)。 + +```python +from agents import Runner, flush_traces, trace + + +@celery_app.task +def run_agent_task(prompt: str): + try: + with trace("celery_task"): + result = Runner.run_sync(agent, prompt) + return result.final_output + finally: + flush_traces() +``` + +```python +from fastapi import BackgroundTasks, FastAPI +from agents import Runner, flush_traces, trace + +app = FastAPI() + + +def process_in_background(prompt: str) -> None: + try: + with trace("background_job"): + Runner.run_sync(agent, prompt) + finally: + flush_traces() + + +@app.post("/run") +async def run(prompt: str, background_tasks: BackgroundTasks): + background_tasks.add_task(process_in_background, prompt) + return {"status": "queued"} +``` + +[`flush_traces()`][agents.tracing.flush_traces] 会阻塞直到当前缓冲的 traces 和 spans +导出完成,因此应在 `trace()` 关闭后调用,以避免刷新到部分构建的 trace。若默认 +导出延迟可接受,则可跳过此调用。 + +## 更高层级的 traces + +有时你可能希望多次调用 `run()` 属于同一个 trace。你可以通过将整段代码包裹在 `trace()` 中来实现。 ```python from agents import Agent, Runner, trace @@ -65,49 +115,49 @@ async def main(): print(f"Rating: {second_result.final_output}") ``` -1. 因为这两次 `Runner.run` 调用被包裹在 `with trace()` 中,所以这些单独运行会成为整体 Trace 的一部分,而不是创建两个 Trace。 +1. 由于两次对 `Runner.run` 的调用都包裹在 `with trace()` 中,单次运行将归入整体 trace,而不是创建两个 trace。 -## 创建 Trace +## 创建 traces -你可以使用 [`trace()`][agents.tracing.trace] 函数创建 Trace。Trace 需要被启动和结束。你有两种方式: +你可以使用 [`trace()`][agents.tracing.trace] 函数创建 trace。Trace 需要被启动和结束。你有两种方式: -1. **推荐**:将 Trace 用作上下文管理器,即 `with trace(...) as my_trace`。这会在正确的时间自动启动和结束 Trace。 +1. **推荐**:将 trace 用作上下文管理器,即 `with trace(...) as my_trace`。这样会在正确的时间自动启动和结束 trace。 2. 你也可以手动调用 [`trace.start()`][agents.tracing.Trace.start] 和 [`trace.finish()`][agents.tracing.Trace.finish]。 -当前 Trace 通过 Python 的 [`contextvar`](https://docs.python.org/3/library/contextvars.html) 跟踪。这意味着它可自动适配并发。如果你手动启动/结束 Trace,则需要向 `start()`/`finish()` 传递 `mark_as_current` 和 `reset_current` 来更新当前 Trace。 +当前 trace 通过 Python 的 [`contextvar`](https://docs.python.org/3/library/contextvars.html) 跟踪。这意味着它可自动适配并发场景。如果你手动启动/结束 trace,则需要向 `start()`/`finish()` 传入 `mark_as_current` 和 `reset_current` 以更新当前 trace。 -## 创建 Span +## 创建 spans -你可以使用各种 [`*_span()`][agents.tracing.create] 方法创建 Span。通常你不需要手动创建 Span。可使用 [`custom_span()`][agents.tracing.custom_span] 函数来跟踪自定义 Span 信息。 +你可以使用各种 [`*_span()`][agents.tracing.create] 方法创建 span。通常你不需要手动创建 span。可使用 [`custom_span()`][agents.tracing.custom_span] 函数跟踪自定义 span 信息。 -Span 会自动归属于当前 Trace,并嵌套在最近的当前 Span 之下,后者通过 Python 的 [`contextvar`](https://docs.python.org/3/library/contextvars.html) 进行跟踪。 +Spans 会自动归属于当前 trace,并嵌套在最近的当前 span 下,该状态通过 Python 的 [`contextvar`](https://docs.python.org/3/library/contextvars.html) 跟踪。 ## 敏感数据 -某些 Span 可能会捕获潜在的敏感数据。 +某些 span 可能会捕获潜在的敏感数据。 -`generation_span()` 会存储 LLM 生成的输入/输出,`function_span()` 会存储函数调用的输入/输出。这些可能包含敏感数据,因此你可以通过 [`RunConfig.trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data] 禁用这类数据采集。 +`generation_span()` 会存储 LLM 生成的输入/输出,`function_span()` 会存储函数调用的输入/输出。这些可能包含敏感数据,因此你可以通过 [`RunConfig.trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data] 禁用这类数据的采集。 -类似地,音频 Span 默认包含输入和输出音频的 base64 编码 PCM 数据。你可以通过配置 [`VoicePipelineConfig.trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data] 禁用该音频数据采集。 +同样,音频 span 默认会包含输入和输出音频的 base64 编码 PCM 数据。你可以通过配置 [`VoicePipelineConfig.trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data] 禁用该音频数据采集。 -默认情况下,`trace_include_sensitive_data` 为 `True`。你可以在运行应用前,通过导出 `OPENAI_AGENTS_TRACE_INCLUDE_SENSITIVE_DATA` 环境变量为 `true/1` 或 `false/0`,在不改代码的情况下设置默认值。 +默认情况下,`trace_include_sensitive_data` 为 `True`。你可以在运行应用前导出 `OPENAI_AGENTS_TRACE_INCLUDE_SENSITIVE_DATA` 环境变量并设为 `true/1` 或 `false/0`,从而无需改代码设置默认值。 ## 自定义追踪进程 追踪的高层架构如下: -- 初始化时,我们会创建一个全局 [`TraceProvider`][agents.tracing.setup.TraceProvider],负责创建 Trace。 -- 我们为 `TraceProvider` 配置一个 [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor],它会将 Trace/Span 批量发送给 [`BackendSpanExporter`][agents.tracing.processors.BackendSpanExporter],后者再将 Span 和 Trace 批量导出到 OpenAI 后端。 +- 在初始化时,我们会创建全局 [`TraceProvider`][agents.tracing.setup.TraceProvider],负责创建 traces。 +- 我们为 `TraceProvider` 配置 [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor],它将 traces/spans 批量发送到 [`BackendSpanExporter`][agents.tracing.processors.BackendSpanExporter],后者再将 spans 和 traces 批量导出到 OpenAI 后端。 -要自定义此默认设置,将追踪发送到替代或附加后端,或修改导出器行为,你有两个选项: +若要自定义此默认设置,将 traces 发送到替代或附加后端,或修改导出器行为,你有两种选择: -1. [`add_trace_processor()`][agents.tracing.add_trace_processor] 允许你添加一个**额外**的追踪进程,它会在 Trace 和 Span 就绪时接收它们。这样你就可以在发送到 OpenAI 后端之外执行自己的处理。 -2. [`set_trace_processors()`][agents.tracing.set_trace_processors] 允许你用自己的追踪进程**替换**默认进程。这意味着除非你包含可执行该操作的 `TracingProcessor`,否则 Trace 不会发送到 OpenAI 后端。 +1. [`add_trace_processor()`][agents.tracing.add_trace_processor] 允许你添加一个**额外的**追踪进程,在 traces 和 spans 就绪时接收它们。这使你可以在发送 traces 到 OpenAI 后端之外执行自己的处理。 +2. [`set_trace_processors()`][agents.tracing.set_trace_processors] 允许你用自己的追踪进程**替换**默认进程。这意味着除非你包含可执行该发送行为的 `TracingProcessor`,否则 traces 不会发送到 OpenAI 后端。 -## 对非 OpenAI 模型进行追踪 +## 使用非 OpenAI 模型进行追踪 -你可以将 OpenAI API key 与非 OpenAI 模型一起使用,从而在无需禁用追踪的情况下,在 OpenAI Traces 仪表板启用免费追踪。有关适配器选择和设置注意事项,请参阅 Models 指南中的[第三方适配器](models/index.md#third-party-adapters)部分。 +你可以将 OpenAI API key 与非 OpenAI 模型一起使用,以在 OpenAI Traces 控制台中启用免费追踪,而无需禁用追踪。有关适配器选择与设置注意事项,请参阅 Models 指南中的[第三方适配器](models/index.md#third-party-adapters)部分。 ```python import os @@ -128,7 +178,7 @@ agent = Agent( ) ``` -如果你只需为单次运行使用不同的追踪 key,请通过 `RunConfig` 传入,而不是更改全局导出器。 +如果你只需要为单次运行使用不同的追踪 key,请通过 `RunConfig` 传入,而不是修改全局导出器。 ```python from agents import Runner, RunConfig @@ -141,12 +191,12 @@ await Runner.run( ``` ## 附加说明 -- 在 Openai Traces 仪表板查看免费追踪。 +- 在 Openai Traces 控制台查看免费追踪。 -## 生态系统集成 +## 生态集成 -以下社区和供应商集成支持 OpenAI Agents SDK 的追踪接口。 +以下社区与厂商集成支持 OpenAI Agents SDK 的追踪接口。 ### 外部追踪进程列表 @@ -154,7 +204,7 @@ await Runner.run( - [Arize-Phoenix](https://docs.arize.com/phoenix/tracing/integrations-tracing/openai-agents-sdk) - [Future AGI](https://docs.futureagi.com/future-agi/products/observability/auto-instrumentation/openai_agents) - [MLflow(self-hosted/OSS)](https://mlflow.org/docs/latest/tracing/integrations/openai-agent) -- [MLflow(Databricks 托管)](https://docs.databricks.com/aws/en/mlflow/mlflow-tracing#-automatic-tracing) +- [MLflow(Databricks hosted)](https://docs.databricks.com/aws/en/mlflow/mlflow-tracing#-automatic-tracing) - [Braintrust](https://braintrust.dev/docs/guides/traces/integrations#openai-agents-sdk) - [Pydantic Logfire](https://logfire.pydantic.dev/docs/integrations/llms/openai/#openai-agents) - [AgentOps](https://docs.agentops.ai/v1/integrations/agentssdk) From 0386210b3bc754ce13165672720ee2cecae53767 Mon Sep 17 00:00:00 2001 From: m1lestones <91078895+m1lestones@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:51:20 -0400 Subject: [PATCH 38/40] docs(tracing): add HoneyHive to tracing integrations list (#2851) --- docs/tracing.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/tracing.md b/docs/tracing.md index 43d0951da9..8215c03fc2 100644 --- a/docs/tracing.md +++ b/docs/tracing.md @@ -220,3 +220,4 @@ The following community and vendor integrations support the OpenAI Agents SDK tr - [PostHog](https://posthog.com/docs/llm-analytics/installation/openai-agents) - [Traccia](https://traccia.ai/docs/integrations/openai-agents) - [PromptLayer](https://docs.promptlayer.com/languages/integrations#openai-agents-sdk) +- [HoneyHive](https://docs.honeyhive.ai/v2/integrations/openai-agents) From aeb653e56286895e7d8918b220e9d99b9e739f35 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 06:56:00 +0900 Subject: [PATCH 39/40] docs: update translated document pages (#2853) --- docs/ja/tracing.md | 103 ++++++++++++++++++++------------------- docs/ko/tracing.md | 98 ++++++++++++++++++++----------------- docs/zh/tracing.md | 119 +++++++++++++++++++++++---------------------- 3 files changed, 166 insertions(+), 154 deletions(-) diff --git a/docs/ja/tracing.md b/docs/ja/tracing.md index dd512baa20..9824fb2a44 100644 --- a/docs/ja/tracing.md +++ b/docs/ja/tracing.md @@ -4,61 +4,61 @@ search: --- # トレーシング -Agents SDK には組み込みのトレーシングがあり、エージェント実行中のイベントを包括的に記録します: LLM 生成、ツール呼び出し、ハンドオフ、ガードレール、さらに発生したカスタムイベントも含まれます。[Traces ダッシュボード](https://platform.openai.com/traces) を使うことで、開発中および本番環境でワークフローをデバッグ、可視化、監視できます。 +Agents SDK にはビルトインのトレーシングが含まれており、エージェント実行中のイベントの包括的な記録を収集します: LLM 生成、ツール呼び出し、ハンドオフ、ガードレール、さらに発生したカスタムイベントも含まれます。[Traces ダッシュボード](https://platform.openai.com/traces) を使うと、開発中および本番環境でワークフローをデバッグ、可視化、監視できます。 !!!note - トレーシングはデフォルトで有効です。一般的な無効化方法は 3 つあります: + トレーシングはデフォルトで有効です。一般的には次の 3 つの方法で無効化できます: 1. 環境変数 `OPENAI_AGENTS_DISABLE_TRACING=1` を設定して、グローバルにトレーシングを無効化できます - 2. [`set_tracing_disabled(True)`][agents.set_tracing_disabled] を使って、コード内でグローバルにトレーシングを無効化できます - 3. [`agents.run.RunConfig.tracing_disabled`][] を `True` に設定して、単一の実行でトレーシングを無効化できます + 2. コード内で [`set_tracing_disabled(True)`][agents.set_tracing_disabled] を使って、グローバルにトレーシングを無効化できます + 3. 単一の実行に対しては、[`agents.run.RunConfig.tracing_disabled`][] を `True` に設定して無効化できます -***OpenAI の API を使用し、Zero Data Retention (ZDR) ポリシー下で運用している組織では、トレーシングは利用できません。*** +***OpenAI の API を利用し、Zero Data Retention ( ZDR ) ポリシー下で運用している組織では、トレーシングは利用できません。*** ## トレースとスパン -- **トレース**は 1 つの「ワークフロー」におけるエンドツーエンドの単一操作を表します。トレースはスパンで構成されます。トレースには次のプロパティがあります: - - `workflow_name`: 論理的なワークフローまたはアプリです。たとえば「Code generation」や「Customer service」です。 - - `trace_id`: トレースの一意 ID です。指定しない場合は自動生成されます。形式は `trace_<32_alphanumeric>` である必要があります。 - - `group_id`: 同じ会話の複数トレースを関連付けるための任意のグループ ID です。たとえば、チャットスレッド ID を使えます。 +- **トレース**は「ワークフロー」の単一のエンドツーエンド操作を表します。トレースはスパンで構成されます。トレースには次のプロパティがあります: + - `workflow_name`: 論理的なワークフローまたはアプリです。たとえば「コード生成」や「カスタマーサービス」です。 + - `trace_id`: トレースの一意な ID です。渡さない場合は自動生成されます。形式は `trace_<32_alphanumeric>` である必要があります。 + - `group_id`: 同じ会話内の複数のトレースを関連付けるための任意のグループ ID です。たとえばチャットスレッド ID を使用できます。 - `disabled`: True の場合、トレースは記録されません。 - - `metadata`: トレースの任意メタデータです。 + - `metadata`: トレースの任意のメタデータです。 - **スパン**は開始時刻と終了時刻を持つ操作を表します。スパンには次があります: - `started_at` と `ended_at` のタイムスタンプ。 - - 属するトレースを表す `trace_id` - - このスパンの親スパンを指す `parent_id`(存在する場合) - - スパンに関する情報である `span_data`。たとえば `AgentSpanData` には Agent の情報、`GenerationSpanData` には LLM 生成の情報などが含まれます。 + - `trace_id`: 所属するトレースを表します + - `parent_id`: このスパンの親スパン (存在する場合) を指します + - `span_data`: スパンに関する情報です。たとえば、`AgentSpanData` にはエージェントに関する情報、`GenerationSpanData` には LLM 生成に関する情報などが含まれます。 ## デフォルトトレーシング -デフォルトで SDK は次をトレースします: +デフォルトでは、SDK は次をトレースします: - `Runner.{run, run_sync, run_streamed}()` 全体は `trace()` でラップされます。 -- エージェントが実行されるたびに `agent_span()` でラップされます +- エージェントが実行されるたびに、`agent_span()` でラップされます - LLM 生成は `generation_span()` でラップされます - 関数ツール呼び出しはそれぞれ `function_span()` でラップされます - ガードレールは `guardrail_span()` でラップされます - ハンドオフは `handoff_span()` でラップされます -- 音声入力(speech-to-text)は `transcription_span()` でラップされます -- 音声出力(text-to-speech)は `speech_span()` でラップされます +- 音声入力 ( speech-to-text ) は `transcription_span()` でラップされます +- 音声出力 ( text-to-speech ) は `speech_span()` でラップされます - 関連する音声スパンは `speech_group_span()` の子になる場合があります -デフォルトでトレース名は「Agent workflow」です。`trace` を使う場合はこの名前を設定できます。または [`RunConfig`][agents.run.RunConfig] で名前やその他のプロパティを設定できます。 +デフォルトでは、トレース名は「Agent workflow」です。`trace` を使う場合はこの名前を設定でき、[`RunConfig`][agents.run.RunConfig] で名前やその他のプロパティを設定することもできます。 -さらに、[カスタムトレースプロセッサー](#custom-tracing-processors) を設定して、トレースを他の宛先へ送信できます(置き換えまたは副次的な宛先として)。 +さらに、[カスタムトレースプロセッサー](#custom-tracing-processors) を設定して、トレースを別の送信先にプッシュできます (置き換え先または副次的な送信先として)。 -## 長時間稼働ワーカーと即時エクスポート +## 長時間実行ワーカーと即時エクスポート -デフォルトの [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor] は、 -数秒ごとにバックグラウンドでトレースをエクスポートし、メモリー内キューがサイズしきい値に達した場合はより早く実行し、 -さらにプロセス終了時に最終フラッシュも実行します。Celery、 -RQ、Dramatiq、または FastAPI バックグラウンドタスクのような長時間稼働ワーカーでは、これにより通常は追加コードなしで -トレースが自動的にエクスポートされますが、各ジョブ完了直後には Traces ダッシュボードに -表示されない場合があります。 +デフォルトの [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor] は、トレースを +数秒ごとにバックグラウンドでエクスポートし、メモリ内キューがサイズしきい値に達した場合はより早く実行され、 +さらにプロセス終了時に最終フラッシュも行います。Celery、 +RQ、Dramatiq、または FastAPI のバックグラウンドタスクのような長時間実行ワーカーでは、これは通常、 +追加コードなしでトレースが自動エクスポートされることを意味しますが、各ジョブ終了直後には +Traces ダッシュボードにすぐ表示されない場合があります。 作業単位の終了時に即時配信保証が必要な場合は、 -トレースコンテキスト終了後に [`flush_traces()`][agents.tracing.flush_traces] を呼び出してください。 +トレースコンテキストを抜けた後に [`flush_traces()`][agents.tracing.flush_traces] を呼び出してください。 ```python from agents import Runner, flush_traces, trace @@ -96,13 +96,13 @@ async def run(prompt: str, background_tasks: BackgroundTasks): ``` [`flush_traces()`][agents.tracing.flush_traces] は現在バッファされているトレースとスパンが -エクスポートされるまでブロックするため、部分的に構築されたトレースをフラッシュしないよう -`trace()` が閉じた後で呼び出してください。デフォルトのエクスポート遅延を許容できる場合は、 -この呼び出しは省略できます。 +エクスポートされるまでブロックするため、部分的に構築されたトレースをフラッシュしないよう、 +`trace()` が閉じた後に呼び出してください。デフォルトのエクスポート遅延で問題ない場合は、 +この呼び出しを省略できます。 ## 上位レベルトレース -場合によっては、複数の `run()` 呼び出しを単一のトレースの一部にしたいことがあります。これは、コード全体を `trace()` でラップすることで実現できます。 +場合によっては、`run()` の複数回の呼び出しを単一のトレースの一部にしたいことがあります。これは、コード全体を `trace()` でラップすることで実現できます。 ```python from agents import Agent, Runner, trace @@ -117,49 +117,49 @@ async def main(): print(f"Rating: {second_result.final_output}") ``` -1. `Runner.run` への 2 回の呼び出しが `with trace()` でラップされているため、個々の実行は 2 つのトレースを作成するのではなく、全体トレースの一部になります。 +1. `Runner.run` の 2 回の呼び出しは `with trace()` でラップされているため、個別に 2 つのトレースを作成するのではなく、全体トレースの一部になります。 ## トレース作成 -[`trace()`][agents.tracing.trace] 関数を使ってトレースを作成できます。トレースは開始と終了が必要です。方法は 2 つあります: +トレース作成には [`trace()`][agents.tracing.trace] 関数を使用できます。トレースは開始と終了が必要です。方法は 2 つあります: -1. **推奨**: `with trace(...) as my_trace` のように、トレースをコンテキストマネージャーとして使います。これにより適切なタイミングで自動的にトレースが開始・終了されます。 -2. [`trace.start()`][agents.tracing.Trace.start] と [`trace.finish()`][agents.tracing.Trace.finish] を手動で呼び出すこともできます。 +1. **推奨**: `with trace(...) as my_trace` のように、トレースをコンテキストマネージャーとして使用します。これにより、適切なタイミングで自動的にトレースが開始・終了されます。 +2. 手動で [`trace.start()`][agents.tracing.Trace.start] と [`trace.finish()`][agents.tracing.Trace.finish] を呼び出すこともできます。 -現在のトレースは Python の [`contextvar`](https://docs.python.org/3/library/contextvars.html) で追跡されます。これは並行処理でも自動的に機能することを意味します。トレースを手動で開始/終了する場合は、現在のトレースを更新するために `start()`/`finish()` に `mark_as_current` と `reset_current` を渡す必要があります。 +現在のトレースは Python の [`contextvar`](https://docs.python.org/3/library/contextvars.html) を通じて追跡されます。これは並行処理でも自動的に機能することを意味します。トレースを手動で開始/終了する場合は、現在のトレースを更新するために `start()`/`finish()` に `mark_as_current` と `reset_current` を渡す必要があります。 ## スパン作成 -さまざまな [`*_span()`][agents.tracing.create] メソッドを使ってスパンを作成できます。一般的には、スパンを手動で作成する必要はありません。カスタムスパン情報を追跡するための [`custom_span()`][agents.tracing.custom_span] 関数も利用できます。 +スパン作成には、さまざまな [`*_span()`][agents.tracing.create] メソッドを使用できます。一般的には、スパンを手動で作成する必要はありません。カスタムスパン情報を追跡するために [`custom_span()`][agents.tracing.custom_span] 関数が利用できます。 スパンは自動的に現在のトレースの一部となり、最も近い現在のスパンの下にネストされます。これは Python の [`contextvar`](https://docs.python.org/3/library/contextvars.html) で追跡されます。 ## 機密データ -一部のスパンは機密性の可能性があるデータを取得する場合があります。 +一部のスパンは機密性の高い可能性があるデータを取得する場合があります。 -`generation_span()` は LLM 生成の入力/出力を保存し、`function_span()` は関数呼び出しの入力/出力を保存します。これらには機密データが含まれる可能性があるため、[`RunConfig.trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data] を使ってそのデータ取得を無効化できます。 +`generation_span()` は LLM 生成の入力/出力を保存し、`function_span()` は関数呼び出しの入力/出力を保存します。これらには機密データが含まれる可能性があるため、[`RunConfig.trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data] でそのデータの取得を無効化できます。 -同様に、Audio スパンにはデフォルトで入力/出力音声の base64 エンコード済み PCM データが含まれます。[`VoicePipelineConfig.trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data] を設定して、この音声データの取得を無効化できます。 +同様に、音声スパンにはデフォルトで入力音声と出力音声の base64 エンコードされた PCM データが含まれます。[`VoicePipelineConfig.trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data] を設定することで、この音声データの取得を無効化できます。 -デフォルトでは `trace_include_sensitive_data` は `True` です。アプリ実行前に環境変数 `OPENAI_AGENTS_TRACE_INCLUDE_SENSITIVE_DATA` を `true/1` または `false/0` に設定することで、コードを書かずにデフォルトを設定できます。 +デフォルトで `trace_include_sensitive_data` は `True` です。アプリ実行前に `OPENAI_AGENTS_TRACE_INCLUDE_SENSITIVE_DATA` 環境変数を `true/1` または `false/0` に設定することで、コードを変更せずにデフォルト値を設定できます。 ## カスタムトレーシングプロセッサー トレーシングの高レベルアーキテクチャは次のとおりです: -- 初期化時に、トレース作成を担当するグローバル [`TraceProvider`][agents.tracing.setup.TraceProvider] を作成します。 -- `TraceProvider` に [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor] を設定し、これがトレース/スパンをバッチで [`BackendSpanExporter`][agents.tracing.processors.BackendSpanExporter] に送信します。`BackendSpanExporter` はスパンとトレースをバッチで OpenAI バックエンドにエクスポートします。 +- 初期化時に、トレース作成を担うグローバルな [`TraceProvider`][agents.tracing.setup.TraceProvider] を作成します。 +- `TraceProvider` を [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor] で構成し、このプロセッサーがトレース/スパンをバッチで [`BackendSpanExporter`][agents.tracing.processors.BackendSpanExporter] に送信します。`BackendSpanExporter` はスパンとトレースをバッチで OpenAI バックエンドにエクスポートします。 -このデフォルト設定をカスタマイズして、代替または追加のバックエンドにトレースを送信したり、エクスポーターの動作を変更したりするには、次の 2 つの方法があります: +このデフォルト設定をカスタマイズして、トレースを別または追加のバックエンドに送信したり、エクスポーターの挙動を変更したりするには、次の 2 つの方法があります: -1. [`add_trace_processor()`][agents.tracing.add_trace_processor] は、準備でき次第トレースとスパンを受け取る**追加**のトレースプロセッサーを追加できます。これにより、OpenAI バックエンドへの送信に加えて独自処理を行えます。 -2. [`set_trace_processors()`][agents.tracing.set_trace_processors] は、デフォルトプロセッサーを独自のトレースプロセッサーで**置き換える**ことができます。これは、そうする `TracingProcessor` を含めない限り、トレースが OpenAI バックエンドに送信されないことを意味します。 +1. [`add_trace_processor()`][agents.tracing.add_trace_processor] を使うと、準備できたトレースとスパンを受け取る **追加の** トレースプロセッサーを追加できます。これにより、トレースを OpenAI バックエンドに送信しつつ、独自の処理も実行できます。 +2. [`set_trace_processors()`][agents.tracing.set_trace_processors] を使うと、デフォルトプロセッサーを独自のトレースプロセッサーで **置き換え** できます。これは、そうする `TracingProcessor` を含めない限り、トレースが OpenAI バックエンドに送信されないことを意味します。 ## 非 OpenAI モデルでのトレーシング -トレーシングを無効化せずに OpenAI Traces ダッシュボードで無料トレーシングを有効化するために、非 OpenAI モデルで OpenAI API キーを使用できます。アダプター選択と設定時の注意点については、Models ガイドの [Third-party adapters](models/index.md#third-party-adapters) セクションを参照してください。 +非 OpenAI モデルでも OpenAI API キーを使うことで、トレーシングを無効化することなく OpenAI Traces ダッシュボードで無料トレーシングを有効にできます。アダプター選択とセットアップ時の注意点は、Models ガイドの [Third-party adapters](models/index.md#third-party-adapters) セクションを参照してください。 ```python import os @@ -180,7 +180,7 @@ agent = Agent( ) ``` -単一実行にのみ別のトレーシングキーが必要な場合は、グローバルエクスポーターを変更する代わりに `RunConfig` で渡してください。 +単一の実行に対してのみ別のトレーシングキーが必要な場合は、グローバルエクスポーターを変更する代わりに `RunConfig` 経由で渡してください。 ```python from agents import Runner, RunConfig @@ -193,12 +193,12 @@ await Runner.run( ``` ## 追加ノート -- Openai Traces ダッシュボードで無料トレースを確認できます。 +- Openai Traces ダッシュボードで無料トレースを表示します。 ## エコシステム統合 -以下のコミュニティおよびベンダー統合は、OpenAI Agents SDK のトレーシングサーフェスをサポートしています。 +次のコミュニティおよびベンダー統合は、OpenAI Agents SDK のトレーシングインターフェースをサポートしています。 ### 外部トレーシングプロセッサー一覧 @@ -224,4 +224,5 @@ await Runner.run( - [Agenta](https://docs.agenta.ai/observability/integrations/openai-agents) - [PostHog](https://posthog.com/docs/llm-analytics/installation/openai-agents) - [Traccia](https://traccia.ai/docs/integrations/openai-agents) -- [PromptLayer](https://docs.promptlayer.com/languages/integrations#openai-agents-sdk) \ No newline at end of file +- [PromptLayer](https://docs.promptlayer.com/languages/integrations#openai-agents-sdk) +- [HoneyHive](https://docs.honeyhive.ai/v2/integrations/openai-agents) \ No newline at end of file diff --git a/docs/ko/tracing.md b/docs/ko/tracing.md index 5579463a83..9c10056feb 100644 --- a/docs/ko/tracing.md +++ b/docs/ko/tracing.md @@ -4,55 +4,61 @@ search: --- # 트레이싱 -Agents SDK에는 내장 트레이싱이 포함되어 있으며, 에이전트 실행 중 발생하는 이벤트(LLM 생성, 도구 호출, 핸드오프, 가드레일, 사용자 정의 이벤트 포함)에 대한 포괄적인 기록을 수집합니다. [Traces 대시보드](https://platform.openai.com/traces)를 사용하면 개발 중과 프로덕션에서 워크플로를 디버깅, 시각화, 모니터링할 수 있습니다. +Agents SDK에는 기본 제공 트레이싱이 포함되어 있으며, 에이전트 실행 중 발생한 이벤트의 포괄적인 기록을 수집합니다: LLM 생성, 도구 호출, 핸드오프, 가드레일, 그리고 발생하는 사용자 정의 이벤트까지 포함됩니다. [Traces 대시보드](https://platform.openai.com/traces)를 사용하면 개발 중과 프로덕션에서 워크플로를 디버그, 시각화, 모니터링할 수 있습니다. !!!note - 트레이싱은 기본적으로 활성화되어 있습니다. 다음 세 가지 일반적인 방법으로 비활성화할 수 있습니다: + 트레이싱은 기본적으로 활성화되어 있습니다. 다음의 일반적인 세 가지 방법으로 비활성화할 수 있습니다: 1. 환경 변수 `OPENAI_AGENTS_DISABLE_TRACING=1`을 설정하여 전역적으로 트레이싱을 비활성화할 수 있습니다 - 2. 코드에서 [`set_tracing_disabled(True)`][agents.set_tracing_disabled]로 전역적으로 트레이싱을 비활성화할 수 있습니다 - 3. 단일 실행에 대해 [`agents.run.RunConfig.tracing_disabled`][]를 `True`로 설정하여 트레이싱을 비활성화할 수 있습니다 + 2. 코드에서 [`set_tracing_disabled(True)`][agents.set_tracing_disabled]를 사용해 전역적으로 트레이싱을 비활성화할 수 있습니다 + 3. 단일 실행에 대해 [`agents.run.RunConfig.tracing_disabled`][]를 `True`로 설정해 트레이싱을 비활성화할 수 있습니다 -***OpenAI API를 사용하면서 Zero Data Retention(ZDR) 정책을 적용하는 조직에서는 트레이싱을 사용할 수 없습니다.*** +***OpenAI API를 사용하면서 ZDR(Zero Data Retention) 정책으로 운영하는 조직에서는 트레이싱을 사용할 수 없습니다.*** ## 트레이스와 스팬 - **트레이스**는 하나의 "워크플로"에 대한 단일 end-to-end 작업을 나타냅니다. 트레이스는 스팬으로 구성됩니다. 트레이스에는 다음 속성이 있습니다: - - `workflow_name`: 논리적 워크플로 또는 앱입니다. 예: "Code generation" 또는 "Customer service" + - `workflow_name`: 논리적 워크플로 또는 앱입니다. 예: "코드 생성" 또는 "고객 서비스" - `trace_id`: 트레이스의 고유 ID입니다. 전달하지 않으면 자동 생성됩니다. 형식은 `trace_<32_alphanumeric>`이어야 합니다 - - `group_id`: 선택적 그룹 ID로, 동일한 대화의 여러 트레이스를 연결합니다. 예를 들어 채팅 스레드 ID를 사용할 수 있습니다 + - `group_id`: 선택적 그룹 ID로, 같은 대화의 여러 트레이스를 연결합니다. 예를 들어 채팅 스레드 ID를 사용할 수 있습니다 - `disabled`: True이면 트레이스가 기록되지 않습니다 - `metadata`: 트레이스용 선택적 메타데이터입니다 - **스팬**은 시작 시간과 종료 시간이 있는 작업을 나타냅니다. 스팬에는 다음이 있습니다: - `started_at` 및 `ended_at` 타임스탬프 - - `trace_id`: 해당 스팬이 속한 트레이스를 나타냄 - - `parent_id`: 이 스팬의 상위 스팬을 가리킴(있는 경우) - - `span_data`: 스팬에 대한 정보입니다. 예를 들어 `AgentSpanData`는 에이전트 정보, `GenerationSpanData`는 LLM 생성 정보 등을 포함합니다 + - 소속된 트레이스를 나타내는 `trace_id` + - 이 스팬의 부모 스팬을 가리키는 `parent_id`(있는 경우) + - 스팬에 대한 정보인 `span_data` 예: `AgentSpanData`에는 Agent 정보가, `GenerationSpanData`에는 LLM 생성 정보가 포함됩니다 ## 기본 트레이싱 기본적으로 SDK는 다음을 트레이싱합니다: -- 전체 `Runner.{run, run_sync, run_streamed}()`는 `trace()`로 감싸집니다 -- 에이전트가 실행될 때마다 `agent_span()`으로 감싸집니다 -- LLM 생성은 `generation_span()`으로 감싸집니다 -- 함수 도구 호출은 각각 `function_span()`으로 감싸집니다 -- 가드레일은 `guardrail_span()`으로 감싸집니다 -- 핸드오프는 `handoff_span()`으로 감싸집니다 -- 오디오 입력(speech-to-text)은 `transcription_span()`으로 감싸집니다 -- 오디오 출력(text-to-speech)은 `speech_span()`으로 감싸집니다 -- 관련 오디오 스팬은 `speech_group_span()` 아래에 부모-자식으로 배치될 수 있습니다 +- 전체 `Runner.{run, run_sync, run_streamed}()`는 `trace()`로 래핑됩니다 +- 에이전트가 실행될 때마다 `agent_span()`으로 래핑됩니다 +- LLM 생성은 `generation_span()`으로 래핑됩니다 +- 함수 도구 호출은 각각 `function_span()`으로 래핑됩니다 +- 가드레일은 `guardrail_span()`으로 래핑됩니다 +- 핸드오프는 `handoff_span()`으로 래핑됩니다 +- 오디오 입력(음성-텍스트)은 `transcription_span()`으로 래핑됩니다 +- 오디오 출력(텍스트-음성)은 `speech_span()`으로 래핑됩니다 +- 관련 오디오 스팬은 `speech_group_span()` 아래에 부모로 연결될 수 있습니다 -기본적으로 트레이스 이름은 "Agent workflow"입니다. `trace`를 사용하면 이 이름을 설정할 수 있고, [`RunConfig`][agents.run.RunConfig]를 통해 이름과 기타 속성을 구성할 수도 있습니다. +기본적으로 트레이스 이름은 "Agent workflow"입니다. `trace`를 사용하면 이 이름을 설정할 수 있고, [`RunConfig`][agents.run.RunConfig]를 사용하면 이름 및 기타 속성을 구성할 수 있습니다. 또한 [사용자 정의 트레이스 프로세서](#custom-tracing-processors)를 설정해 트레이스를 다른 대상으로 전송할 수 있습니다(대체 또는 보조 대상). ## 장기 실행 워커와 즉시 내보내기 -기본 [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor]는 몇 초마다 백그라운드에서 트레이스를 내보내거나, 메모리 내 큐가 크기 임계값에 도달하면 더 빨리 내보내며, 프로세스 종료 시 최종 flush도 수행합니다. Celery, RQ, Dramatiq 또는 FastAPI 백그라운드 작업 같은 장기 실행 워커에서는 보통 추가 코드 없이도 트레이스가 자동으로 내보내지지만, 각 작업이 끝난 직후 Traces 대시보드에 즉시 표시되지 않을 수 있습니다. +기본 [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor]는 트레이스를 +몇 초마다 백그라운드에서 내보내거나, 메모리 내 큐가 크기 트리거에 도달하면 더 빨리 내보내며, +프로세스 종료 시 최종 플러시도 수행합니다. Celery, +RQ, Dramatiq 또는 FastAPI 백그라운드 작업 같은 장기 실행 워커에서는 일반적으로 트레이스가 +추가 코드 없이 자동으로 내보내지지만, 각 작업이 끝난 직후에는 Traces 대시보드에 즉시 +표시되지 않을 수 있습니다. -작업 단위 종료 시 즉시 전달 보장이 필요하다면, 트레이스 컨텍스트가 종료된 후 [`flush_traces()`][agents.tracing.flush_traces]를 호출하세요. +작업 단위 종료 시 즉시 전달 보장이 필요하다면 +트레이스 컨텍스트를 종료한 후 [`flush_traces()`][agents.tracing.flush_traces]를 호출하세요. ```python from agents import Runner, flush_traces, trace @@ -89,11 +95,14 @@ async def run(prompt: str, background_tasks: BackgroundTasks): return {"status": "queued"} ``` -[`flush_traces()`][agents.tracing.flush_traces]는 현재 버퍼링된 트레이스와 스팬이 내보내질 때까지 블로킹하므로, 부분적으로 구성된 트레이스를 flush하지 않으려면 `trace()`가 닫힌 뒤 호출해야 합니다. 기본 내보내기 지연이 허용 가능한 경우 이 호출은 생략할 수 있습니다. +[`flush_traces()`][agents.tracing.flush_traces]는 현재 버퍼링된 트레이스와 스팬이 +내보내질 때까지 블로킹하므로, 부분적으로 구성된 트레이스를 플러시하지 않도록 `trace()`가 +닫힌 뒤 호출하세요. 기본 내보내기 지연 시간이 허용 가능하다면 이 호출은 +생략할 수 있습니다. ## 상위 수준 트레이스 -경우에 따라 `run()` 호출 여러 개를 하나의 트레이스로 묶고 싶을 수 있습니다. 이 경우 전체 코드를 `trace()`로 감싸면 됩니다. +때로는 여러 `run()` 호출을 하나의 단일 트레이스에 포함하고 싶을 수 있습니다. 이 경우 전체 코드를 `trace()`로 래핑하면 됩니다. ```python from agents import Agent, Runner, trace @@ -108,49 +117,49 @@ async def main(): print(f"Rating: {second_result.final_output}") ``` -1. `Runner.run`에 대한 두 호출이 `with trace()`로 감싸져 있으므로, 각 실행은 별도의 트레이스 두 개를 만드는 대신 전체 트레이스의 일부가 됩니다 +1. 두 번의 `Runner.run` 호출이 `with trace()`로 래핑되어 있으므로, 개별 실행이 각각 두 개의 트레이스를 만드는 대신 전체 트레이스의 일부가 됩니다 ## 트레이스 생성 -[`trace()`][agents.tracing.trace] 함수를 사용해 트레이스를 생성할 수 있습니다. 트레이스는 시작과 종료가 필요하며, 이를 위한 두 가지 방법이 있습니다: +[`trace()`][agents.tracing.trace] 함수를 사용해 트레이스를 생성할 수 있습니다. 트레이스는 시작과 종료가 필요하며, 방법은 두 가지입니다: 1. **권장**: 트레이스를 컨텍스트 매니저로 사용합니다. 즉 `with trace(...) as my_trace` 형태입니다. 이렇게 하면 적절한 시점에 트레이스가 자동으로 시작/종료됩니다 -2. [`trace.start()`][agents.tracing.Trace.start]와 [`trace.finish()`][agents.tracing.Trace.finish]를 수동 호출할 수도 있습니다 +2. [`trace.start()`][agents.tracing.Trace.start]와 [`trace.finish()`][agents.tracing.Trace.finish]를 수동으로 호출할 수도 있습니다 -현재 트레이스는 Python [`contextvar`](https://docs.python.org/3/library/contextvars.html)를 통해 추적됩니다. 즉, 동시성 환경에서도 자동으로 동작합니다. 트레이스를 수동으로 시작/종료하는 경우 현재 트레이스를 갱신하려면 `start()`/`finish()`에 `mark_as_current`와 `reset_current`를 전달해야 합니다. +현재 트레이스는 Python [`contextvar`](https://docs.python.org/3/library/contextvars.html)를 통해 추적됩니다. 이는 동시성 환경에서도 자동으로 작동함을 의미합니다. 트레이스를 수동으로 시작/종료하는 경우 현재 트레이스를 갱신하기 위해 `start()`/`finish()`에 `mark_as_current`와 `reset_current`를 전달해야 합니다. ## 스팬 생성 -다양한 [`*_span()`][agents.tracing.create] 메서드를 사용해 스팬을 생성할 수 있습니다. 일반적으로 스팬을 수동으로 만들 필요는 없습니다. 사용자 정의 스팬 정보를 추적하기 위해 [`custom_span()`][agents.tracing.custom_span] 함수를 사용할 수 있습니다. +다양한 [`*_span()`][agents.tracing.create] 메서드를 사용해 스팬을 생성할 수 있습니다. 일반적으로는 스팬을 수동으로 만들 필요가 없습니다. 사용자 정의 스팬 정보를 추적하기 위한 [`custom_span()`][agents.tracing.custom_span] 함수도 제공됩니다. -스팬은 자동으로 현재 트레이스에 속하며, Python [`contextvar`](https://docs.python.org/3/library/contextvars.html)로 추적되는 가장 가까운 현재 스팬 아래에 중첩됩니다. +스팬은 자동으로 현재 트레이스의 일부가 되며, Python [`contextvar`](https://docs.python.org/3/library/contextvars.html)로 추적되는 가장 가까운 현재 스팬 아래에 중첩됩니다. ## 민감한 데이터 -일부 스팬은 잠재적으로 민감한 데이터를 캡처할 수 있습니다. +일부 스팬은 잠재적으로 민감한 데이터를 수집할 수 있습니다. -`generation_span()`은 LLM 생성의 입력/출력을 저장하고, `function_span()`은 함수 호출의 입력/출력을 저장합니다. 여기에 민감한 데이터가 포함될 수 있으므로 [`RunConfig.trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data]를 통해 해당 데이터 캡처를 비활성화할 수 있습니다. +`generation_span()`은 LLM 생성의 입력/출력을 저장하고, `function_span()`은 함수 호출의 입력/출력을 저장합니다. 여기에는 민감한 데이터가 포함될 수 있으므로 [`RunConfig.trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data]를 통해 해당 데이터 수집을 비활성화할 수 있습니다. -마찬가지로 오디오 스팬에는 기본적으로 입력/출력 오디오에 대한 base64 인코딩 PCM 데이터가 포함됩니다. [`VoicePipelineConfig.trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data]를 구성해 이 오디오 데이터 캡처를 비활성화할 수 있습니다. +마찬가지로 오디오 스팬에는 기본적으로 입력/출력 오디오용 base64 인코딩 PCM 데이터가 포함됩니다. [`VoicePipelineConfig.trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data]를 구성해 이 오디오 데이터 수집을 비활성화할 수 있습니다. -기본적으로 `trace_include_sensitive_data`는 `True`입니다. 코드를 변경하지 않고도 앱 실행 전에 `OPENAI_AGENTS_TRACE_INCLUDE_SENSITIVE_DATA` 환경 변수를 `true/1` 또는 `false/0`으로 내보내 기본값을 설정할 수 있습니다. +기본적으로 `trace_include_sensitive_data`는 `True`입니다. 앱 실행 전에 `OPENAI_AGENTS_TRACE_INCLUDE_SENSITIVE_DATA` 환경 변수를 `true/1` 또는 `false/0`으로 내보내 코드 없이 기본값을 설정할 수 있습니다. ## 사용자 정의 트레이싱 프로세서 트레이싱의 상위 수준 아키텍처는 다음과 같습니다: -- 초기화 시 트레이스 생성을 담당하는 전역 [`TraceProvider`][agents.tracing.setup.TraceProvider]를 생성합니다 -- `TraceProvider`를 [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor]로 구성하고, 이 프로세서는 트레이스/스팬을 배치로 [`BackendSpanExporter`][agents.tracing.processors.BackendSpanExporter]에 전송하며, `BackendSpanExporter`는 스팬과 트레이스를 배치로 OpenAI 백엔드에 내보냅니다 +- 초기화 시 전역 [`TraceProvider`][agents.tracing.setup.TraceProvider]를 생성하며, 이는 트레이스 생성을 담당합니다 +- `TraceProvider`는 [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor]로 구성되며, 이 프로세서는 트레이스/스팬을 배치로 [`BackendSpanExporter`][agents.tracing.processors.BackendSpanExporter]에 전송하고, 해당 exporter는 스팬과 트레이스를 배치로 OpenAI 백엔드에 내보냅니다 -이 기본 구성을 사용자 지정하여 대체 또는 추가 백엔드로 트레이스를 보내거나 exporter 동작을 수정하려면 두 가지 방법이 있습니다: +기본 구성을 사용자 지정해 대체 또는 추가 백엔드로 트레이스를 전송하거나 exporter 동작을 수정하려면 두 가지 옵션이 있습니다: -1. [`add_trace_processor()`][agents.tracing.add_trace_processor]를 사용하면 준비된 트레이스와 스팬을 수신하는 **추가** 트레이스 프로세서를 더할 수 있습니다. 이를 통해 OpenAI 백엔드 전송과 더불어 자체 처리를 수행할 수 있습니다 -2. [`set_trace_processors()`][agents.tracing.set_trace_processors]를 사용하면 기본 프로세서를 사용자 정의 트레이스 프로세서로 **교체**할 수 있습니다. 이 경우 해당 동작을 수행하는 `TracingProcessor`를 포함하지 않으면 트레이스가 OpenAI 백엔드로 전송되지 않습니다 +1. [`add_trace_processor()`][agents.tracing.add_trace_processor]를 사용하면 준비된 트레이스와 스팬을 수신할 **추가** 트레이스 프로세서를 추가할 수 있습니다. 이를 통해 OpenAI 백엔드로 전송하는 것에 더해 자체 처리를 수행할 수 있습니다 +2. [`set_trace_processors()`][agents.tracing.set_trace_processors]를 사용하면 기본 프로세서를 사용자 정의 트레이스 프로세서로 **대체**할 수 있습니다. 이 경우 해당 작업을 수행하는 `TracingProcessor`를 포함하지 않으면 트레이스가 OpenAI 백엔드로 전송되지 않습니다 -## non-OpenAI 모델에서의 트레이싱 +## 비 OpenAI 모델에서의 트레이싱 -트레이싱을 비활성화할 필요 없이 OpenAI Traces 대시보드에서 무료 트레이싱을 활성화하려면 non-OpenAI 모델에 OpenAI API 키를 사용할 수 있습니다. 어댑터 선택 및 설정 시 주의사항은 Models 가이드의 [Third-party adapters](models/index.md#third-party-adapters) 섹션을 참조하세요. +트레이싱을 비활성화할 필요 없이 OpenAI Traces 대시보드에서 무료 트레이싱을 활성화하기 위해 비 OpenAI 모델과 함께 OpenAI API 키를 사용할 수 있습니다. 어댑터 선택과 설정 시 주의사항은 Models 가이드의 [서드파티 어댑터](models/index.md#third-party-adapters) 섹션을 참고하세요. ```python import os @@ -171,7 +180,7 @@ agent = Agent( ) ``` -단일 실행에만 다른 트레이싱 키가 필요하다면 전역 exporter를 변경하는 대신 `RunConfig`로 전달하세요. +단일 실행에 대해서만 다른 트레이싱 키가 필요하다면 전역 exporter를 변경하는 대신 `RunConfig`를 통해 전달하세요. ```python from agents import Runner, RunConfig @@ -184,7 +193,7 @@ await Runner.run( ``` ## 추가 참고 사항 -- Openai Traces dashboard에서 무료 트레이스를 확인하세요 +- Openai Traces 대시보드에서 무료 트레이스를 확인하세요 ## 에코시스템 통합 @@ -215,4 +224,5 @@ await Runner.run( - [Agenta](https://docs.agenta.ai/observability/integrations/openai-agents) - [PostHog](https://posthog.com/docs/llm-analytics/installation/openai-agents) - [Traccia](https://traccia.ai/docs/integrations/openai-agents) -- [PromptLayer](https://docs.promptlayer.com/languages/integrations#openai-agents-sdk) \ No newline at end of file +- [PromptLayer](https://docs.promptlayer.com/languages/integrations#openai-agents-sdk) +- [HoneyHive](https://docs.honeyhive.ai/v2/integrations/openai-agents) \ No newline at end of file diff --git a/docs/zh/tracing.md b/docs/zh/tracing.md index fc8173fd39..df6ddc8761 100644 --- a/docs/zh/tracing.md +++ b/docs/zh/tracing.md @@ -4,60 +4,60 @@ search: --- # 追踪 -Agents SDK 包含内置追踪功能,可在智能体运行期间收集完整的事件记录:LLM 生成、工具调用、任务转移、安全防护措施,甚至发生的自定义事件。使用[Traces 控制台](https://platform.openai.com/traces),你可以在开发和生产阶段调试、可视化并监控你的工作流。 +Agents SDK 包含内置追踪,可在智能体运行期间收集完整的事件记录:LLM 生成、工具调用、任务转移、安全防护措施,甚至发生的自定义事件。使用[Traces 控制台](https://platform.openai.com/traces),你可以在开发和生产中调试、可视化并监控工作流。 !!!note - 追踪默认启用。你可以通过三种常见方式禁用: + 默认启用追踪。你可以通过三种常见方式禁用它: 1. 你可以通过设置环境变量 `OPENAI_AGENTS_DISABLE_TRACING=1` 全局禁用追踪 - 2. 你可以在代码中通过 [`set_tracing_disabled(True)`][agents.set_tracing_disabled] 全局禁用追踪 + 2. 你可以在代码中使用 [`set_tracing_disabled(True)`][agents.set_tracing_disabled] 全局禁用追踪 3. 你可以通过将 [`agents.run.RunConfig.tracing_disabled`][] 设为 `True` 来禁用单次运行的追踪 -***对于在 OpenAI API 下使用零数据保留(ZDR)策略的组织,追踪功能不可用。*** +***对于在 OpenAI API 下使用零数据保留(ZDR)策略的组织,追踪不可用。*** ## Traces 与 spans -- **Traces** 表示“工作流”的一次端到端操作。它们由 Span 组成。Traces 具有以下属性: +- **Traces** 表示“工作流”的一次端到端操作。它由 Span 组成。Traces 具有以下属性: - `workflow_name`:这是逻辑工作流或应用。例如“代码生成”或“客户服务”。 - - `trace_id`:Trace 的唯一 ID。如果你未传入则自动生成。格式必须为 `trace_<32_alphanumeric>`。 - - `group_id`:可选的组 ID,用于关联同一会话中的多个 Trace。例如,你可以使用聊天线程 ID。 - - `disabled`:如果为 True,则不会记录该 Trace。 - - `metadata`:Trace 的可选元数据。 -- **Spans** 表示具有开始和结束时间的操作。Spans 包含: + - `trace_id`:trace 的唯一 ID。如果你未传入,会自动生成。格式必须为 `trace_<32_alphanumeric>`。 + - `group_id`:可选的组 ID,用于关联同一会话中的多个 traces。例如,你可以使用聊天线程 ID。 + - `disabled`:若为 True,则不会记录该 trace。 + - `metadata`:trace 的可选元数据。 +- **Spans** 表示具有开始和结束时间的操作。Spans 具有: - `started_at` 和 `ended_at` 时间戳。 - - `trace_id`,表示其所属的 Trace - - `parent_id`,指向该 Span 的父 Span(如果有) - - `span_data`,即 Span 的信息。例如,`AgentSpanData` 包含智能体信息,`GenerationSpanData` 包含 LLM 生成信息,等等。 + - `trace_id`,表示其所属的 trace + - `parent_id`,指向该 Span 的父 Span(如有) + - `span_data`,即该 Span 的信息。例如,`AgentSpanData` 包含智能体信息,`GenerationSpanData` 包含 LLM 生成信息等。 ## 默认追踪 默认情况下,SDK 会追踪以下内容: -- 整个 `Runner.{run, run_sync, run_streamed}()` 会包裹在 `trace()` 中。 -- 每次智能体运行都会包裹在 `agent_span()` 中 -- LLM 生成会包裹在 `generation_span()` 中 -- 每次工具调用都会分别包裹在 `function_span()` 中 -- 安全防护措施会包裹在 `guardrail_span()` 中 -- 任务转移会包裹在 `handoff_span()` 中 -- 音频输入(语音转文本)会包裹在 `transcription_span()` 中 -- 音频输出(文本转语音)会包裹在 `speech_span()` 中 -- 相关音频 Span 可能作为 `speech_group_span()` 的子级 +- 整个 `Runner.{run, run_sync, run_streamed}()` 都包裹在 `trace()` 中。 +- 每次智能体运行都包裹在 `agent_span()` 中 +- LLM 生成包裹在 `generation_span()` 中 +- 每次工具调用都分别包裹在 `function_span()` 中 +- 安全防护措施包裹在 `guardrail_span()` 中 +- 任务转移包裹在 `handoff_span()` 中 +- 音频输入(语音转文本)包裹在 `transcription_span()` 中 +- 音频输出(文本转语音)包裹在 `speech_span()` 中 +- 相关音频 spans 可能作为 `speech_group_span()` 的子级 -默认情况下,Trace 名称为“Agent workflow”。如果你使用 `trace`,可以设置该名称;你也可以通过 [`RunConfig`][agents.run.RunConfig] 配置名称和其他属性。 +默认情况下,trace 名称为“Agent workflow”。如果你使用 `trace`,可以设置该名称;你也可以通过 [`RunConfig`][agents.run.RunConfig] 配置名称和其他属性。 -此外,你还可以设置[自定义追踪进程](#custom-tracing-processors),将 Trace 推送到其他目标(作为替代或次级目标)。 +此外,你可以设置[自定义追踪进程](#custom-tracing-processors),将 traces 推送到其他目的地(作为替代或次要目的地)。 -## 长时间运行的 worker 与立即导出 +## 长时间运行的 worker 与即时导出 -默认的 [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor] 会在后台每隔几秒导出一次追踪, -或在内存队列达到大小触发阈值时更早导出, -并且在进程退出时执行最终 flush。在 Celery、 -RQ、Dramatiq 或 FastAPI 后台任务等长时间运行的 worker 中,这意味着追踪通常会自动导出, -无需额外代码,但每个任务刚结束后它们可能不会立即出现在 Traces 控制台中。 +默认的 [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor] 会在后台每隔几秒导出一次 traces, +或者在内存队列达到大小触发条件时更早导出, +并且在进程退出时执行最终刷新。在 Celery、 +RQ、Dramatiq 或 FastAPI 后台任务等长时间运行的 worker 中,这意味着 traces 通常会自动导出, +无需额外代码,但它们可能不会在每个任务结束后立即出现在 Traces 控制台中。 -如果你需要在一个工作单元结束时立即送达的保证,请调用 -[`flush_traces()`][agents.tracing.flush_traces](在 trace 上下文退出后)。 +如果你需要在一个工作单元结束时立即交付的保证,请在 +trace 上下文退出后调用 [`flush_traces()`][agents.tracing.flush_traces]。 ```python from agents import Runner, flush_traces, trace @@ -94,13 +94,13 @@ async def run(prompt: str, background_tasks: BackgroundTasks): return {"status": "queued"} ``` -[`flush_traces()`][agents.tracing.flush_traces] 会阻塞直到当前缓冲的 traces 和 spans -导出完成,因此应在 `trace()` 关闭后调用,以避免刷新到部分构建的 trace。若默认 -导出延迟可接受,则可跳过此调用。 +[`flush_traces()`][agents.tracing.flush_traces] 会阻塞,直到当前缓冲的 traces 和 spans +导出完成,因此请在 `trace()` 关闭后调用,以避免刷新尚未完整构建的 trace。若可接受 +默认导出延迟,则可跳过此调用。 -## 更高层级的 traces +## 高层级 traces -有时你可能希望多次调用 `run()` 属于同一个 trace。你可以通过将整段代码包裹在 `trace()` 中来实现。 +有时你可能希望多次 `run()` 调用属于同一个 trace。你可以通过将整段代码包裹在 `trace()` 中来实现。 ```python from agents import Agent, Runner, trace @@ -115,49 +115,49 @@ async def main(): print(f"Rating: {second_result.final_output}") ``` -1. 由于两次对 `Runner.run` 的调用都包裹在 `with trace()` 中,单次运行将归入整体 trace,而不是创建两个 trace。 +1. 由于两次 `Runner.run` 调用都包裹在 `with trace()` 中,单次运行将成为整体 trace 的一部分,而不是创建两个 trace。 ## 创建 traces -你可以使用 [`trace()`][agents.tracing.trace] 函数创建 trace。Trace 需要被启动和结束。你有两种方式: +你可以使用 [`trace()`][agents.tracing.trace] 函数来创建 trace。trace 需要被启动和结束。你有两个选项: -1. **推荐**:将 trace 用作上下文管理器,即 `with trace(...) as my_trace`。这样会在正确的时间自动启动和结束 trace。 +1. **推荐**:将 trace 用作上下文管理器,即 `with trace(...) as my_trace`。这会在正确时间自动启动并结束 trace。 2. 你也可以手动调用 [`trace.start()`][agents.tracing.Trace.start] 和 [`trace.finish()`][agents.tracing.Trace.finish]。 -当前 trace 通过 Python 的 [`contextvar`](https://docs.python.org/3/library/contextvars.html) 跟踪。这意味着它可自动适配并发场景。如果你手动启动/结束 trace,则需要向 `start()`/`finish()` 传入 `mark_as_current` 和 `reset_current` 以更新当前 trace。 +当前 trace 通过 Python 的 [`contextvar`](https://docs.python.org/3/library/contextvars.html) 跟踪。这意味着它可自动适配并发。若你手动启动/结束 trace,则需要向 `start()`/`finish()` 传入 `mark_as_current` 和 `reset_current` 来更新当前 trace。 ## 创建 spans -你可以使用各种 [`*_span()`][agents.tracing.create] 方法创建 span。通常你不需要手动创建 span。可使用 [`custom_span()`][agents.tracing.custom_span] 函数跟踪自定义 span 信息。 +你可以使用各种 [`*_span()`][agents.tracing.create] 方法来创建 span。通常,你不需要手动创建 spans。也提供了 [`custom_span()`][agents.tracing.custom_span] 函数来跟踪自定义 span 信息。 -Spans 会自动归属于当前 trace,并嵌套在最近的当前 span 下,该状态通过 Python 的 [`contextvar`](https://docs.python.org/3/library/contextvars.html) 跟踪。 +Spans 会自动归属于当前 trace,并嵌套在最近的当前 span 下,而该状态通过 Python 的 [`contextvar`](https://docs.python.org/3/library/contextvars.html) 跟踪。 ## 敏感数据 -某些 span 可能会捕获潜在的敏感数据。 +某些 spans 可能会捕获潜在敏感数据。 -`generation_span()` 会存储 LLM 生成的输入/输出,`function_span()` 会存储函数调用的输入/输出。这些可能包含敏感数据,因此你可以通过 [`RunConfig.trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data] 禁用这类数据的采集。 +`generation_span()` 会存储 LLM 生成的输入/输出,`function_span()` 会存储函数调用的输入/输出。这些内容可能包含敏感数据,因此你可以通过 [`RunConfig.trace_include_sensitive_data`][agents.run.RunConfig.trace_include_sensitive_data] 禁用这些数据的捕获。 -同样,音频 span 默认会包含输入和输出音频的 base64 编码 PCM 数据。你可以通过配置 [`VoicePipelineConfig.trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data] 禁用该音频数据采集。 +类似地,音频 spans 默认包含输入与输出音频的 base64 编码 PCM 数据。你可以通过配置 [`VoicePipelineConfig.trace_include_sensitive_audio_data`][agents.voice.pipeline_config.VoicePipelineConfig.trace_include_sensitive_audio_data] 禁用这些音频数据的捕获。 -默认情况下,`trace_include_sensitive_data` 为 `True`。你可以在运行应用前导出 `OPENAI_AGENTS_TRACE_INCLUDE_SENSITIVE_DATA` 环境变量并设为 `true/1` 或 `false/0`,从而无需改代码设置默认值。 +默认情况下,`trace_include_sensitive_data` 为 `True`。你可以在运行应用前通过导出 `OPENAI_AGENTS_TRACE_INCLUDE_SENSITIVE_DATA` 环境变量并设为 `true/1` 或 `false/0` 来在无代码情况下设置默认值。 ## 自定义追踪进程 -追踪的高层架构如下: +追踪的高层架构为: -- 在初始化时,我们会创建全局 [`TraceProvider`][agents.tracing.setup.TraceProvider],负责创建 traces。 -- 我们为 `TraceProvider` 配置 [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor],它将 traces/spans 批量发送到 [`BackendSpanExporter`][agents.tracing.processors.BackendSpanExporter],后者再将 spans 和 traces 批量导出到 OpenAI 后端。 +- 初始化时,我们会创建全局 [`TraceProvider`][agents.tracing.setup.TraceProvider],其负责创建 traces。 +- 我们将 `TraceProvider` 配置为使用 [`BatchTraceProcessor`][agents.tracing.processors.BatchTraceProcessor],该进程会将 traces/spans 批量发送到 [`BackendSpanExporter`][agents.tracing.processors.BackendSpanExporter],后者再将 spans 和 traces 批量导出到 OpenAI 后端。 -若要自定义此默认设置,将 traces 发送到替代或附加后端,或修改导出器行为,你有两种选择: +要自定义此默认设置,以将 traces 发送到替代或附加后端,或修改导出器行为,你有两个选项: -1. [`add_trace_processor()`][agents.tracing.add_trace_processor] 允许你添加一个**额外的**追踪进程,在 traces 和 spans 就绪时接收它们。这使你可以在发送 traces 到 OpenAI 后端之外执行自己的处理。 -2. [`set_trace_processors()`][agents.tracing.set_trace_processors] 允许你用自己的追踪进程**替换**默认进程。这意味着除非你包含可执行该发送行为的 `TracingProcessor`,否则 traces 不会发送到 OpenAI 后端。 +1. [`add_trace_processor()`][agents.tracing.add_trace_processor] 允许你添加**额外的**追踪进程,它会在 traces 和 spans 就绪时接收它们。这使你可以在发送到 OpenAI 后端之外执行自己的处理。 +2. [`set_trace_processors()`][agents.tracing.set_trace_processors] 允许你用自己的追踪进程**替换**默认进程。这意味着除非你包含一个会这样做的 `TracingProcessor`,否则 traces 不会发送到 OpenAI 后端。 ## 使用非 OpenAI 模型进行追踪 -你可以将 OpenAI API key 与非 OpenAI 模型一起使用,以在 OpenAI Traces 控制台中启用免费追踪,而无需禁用追踪。有关适配器选择与设置注意事项,请参阅 Models 指南中的[第三方适配器](models/index.md#third-party-adapters)部分。 +你可以将 OpenAI API key 与非 OpenAI 模型一起使用,以在 OpenAI Traces 控制台中启用免费追踪,而无需禁用追踪。有关适配器选择和设置注意事项,请参阅 Models 指南中的[第三方适配器](models/index.md#third-party-adapters)部分。 ```python import os @@ -178,7 +178,7 @@ agent = Agent( ) ``` -如果你只需要为单次运行使用不同的追踪 key,请通过 `RunConfig` 传入,而不是修改全局导出器。 +如果你仅需为单次运行使用不同的追踪密钥,请通过 `RunConfig` 传入,而不是更改全局导出器。 ```python from agents import Runner, RunConfig @@ -191,12 +191,12 @@ await Runner.run( ``` ## 附加说明 -- 在 Openai Traces 控制台查看免费追踪。 +- 在 Openai Traces 控制台查看免费 traces。 -## 生态集成 +## 生态系统集成 -以下社区与厂商集成支持 OpenAI Agents SDK 的追踪接口。 +以下社区与供应商集成支持 OpenAI Agents SDK 追踪接口。 ### 外部追踪进程列表 @@ -222,4 +222,5 @@ await Runner.run( - [Agenta](https://docs.agenta.ai/observability/integrations/openai-agents) - [PostHog](https://posthog.com/docs/llm-analytics/installation/openai-agents) - [Traccia](https://traccia.ai/docs/integrations/openai-agents) -- [PromptLayer](https://docs.promptlayer.com/languages/integrations#openai-agents-sdk) \ No newline at end of file +- [PromptLayer](https://docs.promptlayer.com/languages/integrations#openai-agents-sdk) +- [HoneyHive](https://docs.honeyhive.ai/v2/integrations/openai-agents) \ No newline at end of file From c06cd45004f63bd33ef9dd66bdc2b4d0cdc82e1b Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Wed, 8 Apr 2026 08:21:37 +0900 Subject: [PATCH 40/40] fix: harden SQLAlchemySession against transient SQLite locks (#2854) --- .../extensions/memory/sqlalchemy_session.py | 109 +++++++++++++----- .../memory/test_sqlalchemy_session.py | 25 ++++ 2 files changed, 106 insertions(+), 28 deletions(-) diff --git a/src/agents/extensions/memory/sqlalchemy_session.py b/src/agents/extensions/memory/sqlalchemy_session.py index 759ddaf5d5..d84f2c78fb 100644 --- a/src/agents/extensions/memory/sqlalchemy_session.py +++ b/src/agents/extensions/memory/sqlalchemy_session.py @@ -39,12 +39,13 @@ Table, Text, delete, + event, insert, select, text as sql_text, update, ) -from sqlalchemy.exc import IntegrityError +from sqlalchemy.exc import IntegrityError, OperationalError from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine from ...items import TResponseInputItem @@ -57,6 +58,10 @@ class SQLAlchemySession(SessionABC): _table_init_locks: ClassVar[dict[tuple[str, str, str], threading.Lock]] = {} _table_init_locks_guard: ClassVar[threading.Lock] = threading.Lock() + _sqlite_configured_engines: ClassVar[set[int]] = set() + _sqlite_configured_engines_guard: ClassVar[threading.Lock] = threading.Lock() + _SQLITE_BUSY_TIMEOUT_MS: ClassVar[int] = 5000 + _SQLITE_LOCK_RETRY_DELAYS: ClassVar[tuple[float, ...]] = (0.05, 0.1, 0.2, 0.4, 0.8) _metadata: MetaData _sessions: Table _messages: Table @@ -78,6 +83,50 @@ def _get_table_init_lock( cls._table_init_locks[lock_key] = lock return lock + @classmethod + def _configure_sqlite_engine(cls, engine: AsyncEngine) -> None: + """Apply SQLite settings that reduce transient lock failures.""" + if engine.dialect.name != "sqlite": + return + + engine_key = id(engine.sync_engine) + with cls._sqlite_configured_engines_guard: + if engine_key in cls._sqlite_configured_engines: + return + + @event.listens_for(engine.sync_engine, "connect") + def _configure_sqlite_connection(dbapi_connection: Any, _: Any) -> None: + cursor = dbapi_connection.cursor() + try: + cursor.execute(f"PRAGMA busy_timeout = {cls._SQLITE_BUSY_TIMEOUT_MS}") + cursor.execute("PRAGMA journal_mode = WAL") + finally: + cursor.close() + + cls._sqlite_configured_engines.add(engine_key) + + @staticmethod + def _is_sqlite_lock_error(exc: OperationalError) -> bool: + return "database is locked" in str(exc).lower() + + async def _run_sqlite_write_with_retry(self, operation: Any) -> None: + """Retry transient SQLite write lock failures with bounded backoff.""" + if self._engine.dialect.name != "sqlite": + await operation() + return + + for attempt, delay in enumerate((0.0, *self._SQLITE_LOCK_RETRY_DELAYS)): + if delay: + await asyncio.sleep(delay) + try: + await operation() + return + except OperationalError as exc: + if not self._is_sqlite_lock_error(exc): + raise + if attempt == len(self._SQLITE_LOCK_RETRY_DELAYS): + raise + def __init__( self, session_id: str, @@ -105,6 +154,7 @@ def __init__( self.session_id = session_id self.session_settings = session_settings or SessionSettings() self._engine = engine + self._configure_sqlite_engine(engine) self._init_lock = ( self._get_table_init_lock(engine, sessions_table, messages_table) if create_tables @@ -294,34 +344,37 @@ async def add_items(self, items: list[TResponseInputItem]) -> None: for item in items ] - async with self._session_factory() as sess: - async with sess.begin(): - # Avoid check-then-insert races on the first write while keeping - # the common path free of avoidable integrity exceptions. - existing = await sess.execute( - select(self._sessions.c.session_id).where( - self._sessions.c.session_id == self.session_id + async def _write_items() -> None: + async with self._session_factory() as sess: + async with sess.begin(): + # Avoid check-then-insert races on the first write while keeping + # the common path free of avoidable integrity exceptions. + existing = await sess.execute( + select(self._sessions.c.session_id).where( + self._sessions.c.session_id == self.session_id + ) ) - ) - if not existing.scalar_one_or_none(): - try: - async with sess.begin_nested(): - await sess.execute( - insert(self._sessions).values({"session_id": self.session_id}) - ) - except IntegrityError: - # Another concurrent writer created the parent row first. - pass - - # Insert messages in bulk - await sess.execute(insert(self._messages), payload) - - # Touch updated_at column - await sess.execute( - update(self._sessions) - .where(self._sessions.c.session_id == self.session_id) - .values(updated_at=sql_text("CURRENT_TIMESTAMP")) - ) + if not existing.scalar_one_or_none(): + try: + async with sess.begin_nested(): + await sess.execute( + insert(self._sessions).values({"session_id": self.session_id}) + ) + except IntegrityError: + # Another concurrent writer created the parent row first. + pass + + # Insert messages in bulk + await sess.execute(insert(self._messages), payload) + + # Touch updated_at column + await sess.execute( + update(self._sessions) + .where(self._sessions.c.session_id == self.session_id) + .values(updated_at=sql_text("CURRENT_TIMESTAMP")) + ) + + await self._run_sqlite_write_with_retry(_write_items) async def pop_item(self) -> TResponseInputItem | None: """Remove and return the most recent item from the session. diff --git a/tests/extensions/memory/test_sqlalchemy_session.py b/tests/extensions/memory/test_sqlalchemy_session.py index 0365cc72cc..3919ada9b6 100644 --- a/tests/extensions/memory/test_sqlalchemy_session.py +++ b/tests/extensions/memory/test_sqlalchemy_session.py @@ -272,6 +272,31 @@ async def worker(content: str) -> None: assert sorted(stored_contents) == sorted(submitted) +async def test_add_items_waits_for_transient_sqlite_write_lock(tmp_path): + """SQLite writes should wait briefly for a transient lock instead of failing.""" + db_url = f"sqlite+aiosqlite:///{tmp_path / 'sqlite_write_lock_retry.db'}" + session = SQLAlchemySession.from_url( + "sqlite_write_lock_retry", + url=db_url, + create_tables=True, + ) + await session.get_items() + + async with session.engine.connect() as conn: + await conn.execute(text("BEGIN IMMEDIATE")) + blocked_write = asyncio.create_task( + session.add_items([{"role": "user", "content": "after-lock"}]) + ) + await asyncio.sleep(0.1) + await conn.rollback() + + await asyncio.wait_for(blocked_write, timeout=5) + + stored = await session.get_items() + assert len(stored) == 1 + assert stored[0].get("content") == "after-lock" + + async def test_add_items_concurrent_first_access_across_sessions_with_shared_engine(tmp_path): """Concurrent first writes should not race table creation across session instances.""" db_url = f"sqlite+aiosqlite:///{tmp_path / 'concurrent_shared_engine.db'}"