-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Python: Inject user agent header at runtime #5435
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -915,3 +915,76 @@ def test_unsupported_type_raises(self) -> None: | |
|
|
||
|
|
||
| # endregion | ||
|
|
||
|
|
||
| # region User Agent Prefix | ||
|
|
||
|
|
||
| class TestUserAgentPrefix: | ||
| """Tests that the user_agent_prefix context manager is active during agent execution.""" | ||
|
|
||
| async def test_user_agent_prefix_set_during_non_streaming(self) -> None: | ||
| """The user agent should contain the foundry-hosting prefix in non-streaming mode.""" | ||
| from agent_framework._telemetry import _get_user_agent # type: ignore | ||
|
|
||
| captured_user_agent: list[str] = [] | ||
|
|
||
| async def run_and_capture(*args: Any, **kwargs: Any) -> AgentResponse: | ||
| captured_user_agent.append(_get_user_agent()) | ||
| return AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("ok")])]) | ||
|
|
||
| agent = _make_agent() | ||
| agent.run = AsyncMock(side_effect=run_and_capture) | ||
| server = _make_server(agent) | ||
| resp = await _post(server, input_text="Hi", stream=False) | ||
|
|
||
| assert resp.status_code == 200 | ||
| assert len(captured_user_agent) == 1 | ||
| assert "foundry-hosting" in captured_user_agent[0] | ||
|
|
||
| async def test_user_agent_prefix_set_during_streaming(self) -> None: | ||
| """The user agent should contain the foundry-hosting prefix in streaming mode.""" | ||
| from agent_framework._telemetry import _get_user_agent # type: ignore | ||
|
|
||
| captured_user_agent: list[str] = [] | ||
|
|
||
| async def _stream_gen() -> AsyncIterator[AgentResponseUpdate]: | ||
| captured_user_agent.append(_get_user_agent()) | ||
| yield AgentResponseUpdate(contents=[Content.from_text("hello")], role="assistant") | ||
|
Comment on lines
+947
to
+953
|
||
|
|
||
| def run_streaming(*args: Any, **kwargs: Any) -> Any: | ||
| if kwargs.get("stream"): | ||
| return ResponseStream(_stream_gen()) # type: ignore | ||
| raise NotImplementedError | ||
|
|
||
| agent = _make_agent() | ||
| agent.run = MagicMock(side_effect=run_streaming) | ||
| server = _make_server(agent) | ||
| resp = await _post(server, stream=True) | ||
|
|
||
| assert resp.status_code == 200 | ||
| assert len(captured_user_agent) == 1 | ||
| assert "foundry-hosting" in captured_user_agent[0] | ||
|
|
||
| async def test_user_agent_extra_headers_during_run(self) -> None: | ||
| """get_user_agent_extra_headers() should include the prefix during a request.""" | ||
| from agent_framework._telemetry import get_user_agent_extra_headers | ||
|
|
||
| captured_headers: list[dict[str, str]] = [] | ||
|
|
||
| async def run_and_capture(*args: Any, **kwargs: Any) -> AgentResponse: | ||
| captured_headers.append(get_user_agent_extra_headers()) | ||
| return AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("ok")])]) | ||
|
|
||
| agent = _make_agent() | ||
| agent.run = AsyncMock(side_effect=run_and_capture) | ||
| server = _make_server(agent) | ||
| resp = await _post(server, input_text="Hi", stream=False) | ||
|
|
||
| assert resp.status_code == 200 | ||
| assert len(captured_headers) == 1 | ||
| assert "User-Agent" in captured_headers[0] | ||
| assert "foundry-hosting" in captured_headers[0]["User-Agent"] | ||
|
|
||
|
|
||
| # endregion | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,7 +32,7 @@ | |
| from agent_framework._compaction import CompactionStrategy, TokenizerProtocol | ||
| from agent_framework._middleware import ChatAndFunctionMiddlewareTypes, ChatMiddlewareLayer | ||
| from agent_framework._settings import SecretString | ||
| from agent_framework._telemetry import USER_AGENT_KEY | ||
| from agent_framework._telemetry import USER_AGENT_KEY, get_user_agent_extra_headers | ||
| from agent_framework._tools import ( | ||
| SHELL_TOOL_KIND_VALUE, | ||
| FunctionInvocationConfiguration, | ||
|
|
@@ -482,6 +482,13 @@ async def _prepare_request( | |
| client = self.client | ||
| validated_options = await self._validate_options(options) | ||
| run_options = await self._prepare_options(messages, validated_options) | ||
| ua_headers = get_user_agent_extra_headers() | ||
| if ua_headers: | ||
| existing = run_options.get("extra_headers") | ||
| if existing is None: | ||
| run_options["extra_headers"] = ua_headers | ||
| elif USER_AGENT_KEY not in existing: | ||
| run_options["extra_headers"] = {**existing, **ua_headers} | ||
|
Comment on lines
+485
to
+491
|
||
| return client, run_options, validated_options | ||
|
|
||
| def _handle_request_error(self, ex: Exception) -> NoReturn: | ||
|
|
@@ -525,6 +532,7 @@ async def _stream() -> AsyncIterable[ChatResponseUpdate]: | |
| stream_response = await client.responses.retrieve( | ||
| continuation_token["response_id"], | ||
| stream=True, | ||
| extra_headers=get_user_agent_extra_headers(), | ||
| ) | ||
|
Comment on lines
532
to
536
|
||
| async for chunk in stream_response: | ||
| yield self._parse_chunk_from_openai( | ||
|
|
@@ -572,7 +580,10 @@ async def _get_response() -> ChatResponse: | |
| client = self.client | ||
| validated_options = await self._validate_options(options) | ||
| try: | ||
| response = await client.responses.retrieve(continuation_token["response_id"]) | ||
| response = await client.responses.retrieve( | ||
| continuation_token["response_id"], | ||
| extra_headers=get_user_agent_extra_headers(), | ||
| ) | ||
|
Comment on lines
+583
to
+586
|
||
| except Exception as ex: | ||
| self._handle_request_error(ex) | ||
| return self._parse_response_from_openai(response, options=validated_options) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -22,7 +22,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||
| from agent_framework._docstrings import apply_layered_docstring | ||||||||||||||||||||||||||||||||||||||||||||||||||
| from agent_framework._middleware import ChatAndFunctionMiddlewareTypes, ChatMiddlewareLayer | ||||||||||||||||||||||||||||||||||||||||||||||||||
| from agent_framework._settings import SecretString | ||||||||||||||||||||||||||||||||||||||||||||||||||
| from agent_framework._telemetry import USER_AGENT_KEY | ||||||||||||||||||||||||||||||||||||||||||||||||||
| from agent_framework._telemetry import USER_AGENT_KEY, get_user_agent_extra_headers | ||||||||||||||||||||||||||||||||||||||||||||||||||
| from agent_framework._tools import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| FunctionInvocationConfiguration, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| FunctionInvocationLayer, | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -671,6 +671,16 @@ def _prepare_options(self, messages: Sequence[Message], options: Mapping[str, An | |||||||||||||||||||||||||||||||||||||||||||||||||
| run_options["response_format"] = response_format | ||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| run_options["response_format"] = type_to_response_format_param(response_format) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| # runtime user-agent header | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ua_headers = get_user_agent_extra_headers() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if ua_headers: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| existing = run_options.get("extra_headers") | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if existing is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| run_options["extra_headers"] = ua_headers | ||||||||||||||||||||||||||||||||||||||||||||||||||
| elif USER_AGENT_KEY not in existing: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| run_options["extra_headers"] = {**existing, **ua_headers} | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+678
to
+683
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| existing = run_options.get("extra_headers") | |
| if existing is None: | |
| run_options["extra_headers"] = ua_headers | |
| elif USER_AGENT_KEY not in existing: | |
| run_options["extra_headers"] = {**existing, **ua_headers} | |
| existing_extra_headers = run_options.get("extra_headers") | |
| existing_default_headers = run_options.get("default_headers") | |
| merged_extra_headers = ( | |
| dict(existing_extra_headers) if existing_extra_headers is not None else {} | |
| ) | |
| merged_extra_headers.update(ua_headers) | |
| runtime_user_agent = ua_headers.get(USER_AGENT_KEY) | |
| existing_user_agent = None | |
| if existing_extra_headers is not None: | |
| existing_user_agent = existing_extra_headers.get(USER_AGENT_KEY) | |
| if existing_user_agent is None and existing_default_headers is not None: | |
| existing_user_agent = existing_default_headers.get(USER_AGENT_KEY) | |
| if runtime_user_agent and existing_user_agent: | |
| merged_extra_headers[USER_AGENT_KEY] = f"{runtime_user_agent} {existing_user_agent}" | |
| run_options["extra_headers"] = merged_extra_headers |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,7 +10,7 @@ | |
|
|
||
| from agent_framework._clients import BaseEmbeddingClient | ||
| from agent_framework._settings import SecretString | ||
| from agent_framework._telemetry import USER_AGENT_KEY | ||
| from agent_framework._telemetry import USER_AGENT_KEY, get_user_agent_extra_headers | ||
| from agent_framework._types import Embedding, EmbeddingGenerationOptions, GeneratedEmbeddings, UsageDetails | ||
| from agent_framework.observability import EmbeddingTelemetryLayer | ||
| from openai import AsyncAzureOpenAI, AsyncOpenAI | ||
|
|
@@ -282,6 +282,13 @@ async def get_embeddings( | |
| kwargs["encoding_format"] = encoding_format | ||
| if user := opts.get("user"): | ||
| kwargs["user"] = user | ||
| ua_headers = get_user_agent_extra_headers() | ||
| if ua_headers: | ||
| existing = kwargs.get("extra_headers") | ||
| if existing is None: | ||
| kwargs["extra_headers"] = ua_headers | ||
| elif USER_AGENT_KEY not in existing: | ||
| kwargs["extra_headers"] = {**existing, **ua_headers} | ||
|
Comment on lines
+285
to
+291
|
||
|
|
||
| response = await self.client.embeddings.create(**kwargs) # type: ignore[union-attr] | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These tests import the private
_get_user_agentsymbol (and suppress typing) even though there is now a publicget_user_agent_extra_headers()API that reflects the runtime prefix behavior. Consider asserting againstget_user_agent_extra_headers()["User-Agent"]in the first two tests to avoid coupling tests to a private helper.