Skip to content

Commit ab2ff90

Browse files
committed
Merge remote-tracking branch 'upstream/main' into xai-sdk
2 parents afd8a4e + e7b2f82 commit ab2ff90

File tree

106 files changed

+6503
-1415
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

106 files changed

+6503
-1415
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,8 @@ jobs:
202202
strategy:
203203
fail-fast: false
204204
matrix:
205-
python-version: ["3.10", "3.11", "3.12", "3.13"]
205+
# TODO(Marcelo): Enable 3.11 again.
206+
python-version: ["3.10", "3.12", "3.13"]
206207
env:
207208
CI: true
208209
COVERAGE_PROCESS_START: ./pyproject.toml

docs/.hooks/main.py

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616

1717
def on_page_markdown(markdown: str, page: Page, config: Config, files: Files) -> str:
1818
"""Called on each file after it is read and before it is converted to HTML."""
19-
markdown = inject_snippets(markdown, (DOCS_ROOT / page.file.src_uri).parent)
19+
relative_path = DOCS_ROOT / page.file.src_uri
20+
markdown = inject_snippets(markdown, relative_path.parent)
2021
markdown = replace_uv_python_run(markdown)
2122
markdown = render_examples(markdown)
2223
markdown = render_video(markdown)
24+
markdown = create_gateway_toggle(markdown, relative_path)
2325
return markdown
2426

2527

@@ -39,6 +41,7 @@ def on_env(env: Environment, config: Config, files: Files) -> Environment:
3941

4042
def on_post_build(config: Config) -> None:
4143
"""Inject extra CSS into mermaid styles to avoid titles being the same color as the background in dark mode."""
44+
assert bundle_path is not None
4245
if bundle_path.exists():
4346
content = bundle_path.read_text()
4447
content, _ = re.subn(r'}(\.statediagram)', '}.statediagramTitleText{fill:#888}\1', content, count=1)
@@ -115,3 +118,109 @@ def sub_cf_video(m: re.Match[str]) -> str:
115118
></iframe>
116119
</div>
117120
"""
121+
122+
123+
def create_gateway_toggle(markdown: str, relative_path: Path) -> str:
124+
"""Transform Python code blocks with Agent() calls to show both Pydantic AI and Gateway versions."""
125+
# Pattern matches Python code blocks with or without attributes, and optional annotation definitions after
126+
# Annotation definitions are numbered list items like "1. Some text" that follow the code block
127+
return re.sub(
128+
r'```py(?:thon)?(?: *\{?([^}\n]*)\}?)?\n(.*?)\n```(\n\n(?:\d+\..+?\n)+?\n)?',
129+
lambda m: transform_gateway_code_block(m, relative_path),
130+
markdown,
131+
flags=re.MULTILINE | re.DOTALL,
132+
)
133+
134+
135+
# Models that should get gateway transformation
136+
GATEWAY_MODELS = ('anthropic', 'openai', 'openai-responses', 'openai-chat', 'bedrock', 'google-vertex', 'groq')
137+
138+
139+
def transform_gateway_code_block(m: re.Match[str], relative_path: Path) -> str:
140+
"""Transform a single code block to show both versions if it contains Agent() calls."""
141+
attrs = m.group(1) or ''
142+
code = m.group(2)
143+
annotations = m.group(3) or '' # Capture annotation definitions if present
144+
145+
# Simple check: does the code contain both "Agent(" and a quoted string?
146+
if 'Agent(' not in code:
147+
attrs_str = f' {{{attrs}}}' if attrs else ''
148+
return f'```python{attrs_str}\n{code}\n```{annotations}'
149+
150+
# Check if code contains Agent() with a model that should be transformed
151+
# Look for Agent(...'model:...' or Agent(..."model:..."
152+
agent_pattern = r'Agent\((?:(?!["\']).)*([\"\'])([^"\']+)\1'
153+
agent_match = re.search(agent_pattern, code, flags=re.DOTALL)
154+
155+
if not agent_match:
156+
# No Agent() with string literal found
157+
attrs_str = f' {{{attrs}}}' if attrs else ''
158+
return f'```python{attrs_str}\n{code}\n```{annotations}'
159+
160+
model_string = agent_match.group(2)
161+
# Check if model starts with one of the gateway-supported models
162+
should_transform = any(model_string.startswith(f'{model}:') for model in GATEWAY_MODELS)
163+
164+
if not should_transform:
165+
# Model doesn't match gateway models, return original
166+
attrs_str = f' {{{attrs}}}' if attrs else ''
167+
return f'```python{attrs_str}\n{code}\n```{annotations}'
168+
169+
# Transform the code for gateway version
170+
def replace_agent_model(match: re.Match[str]) -> str:
171+
"""Replace model string with gateway/ prefix."""
172+
full_match = match.group(0)
173+
quote = match.group(1)
174+
model = match.group(2)
175+
176+
# Replace the model string while preserving the rest
177+
return full_match.replace(f'{quote}{model}{quote}', f'{quote}gateway/{model}{quote}', 1)
178+
179+
# This pattern finds: "Agent(" followed by anything (lazy), then the first quoted string
180+
gateway_code = re.sub(
181+
agent_pattern,
182+
replace_agent_model,
183+
code,
184+
flags=re.DOTALL,
185+
)
186+
187+
# Build attributes string
188+
docs_path = DOCS_ROOT / 'gateway'
189+
190+
relative_path_to_gateway = docs_path.relative_to(relative_path, walk_up=True)
191+
link = f"<a href='{relative_path_to_gateway}' style='float: right;'>Learn about Gateway</a>"
192+
attrs_str = f' {{{attrs}}}' if attrs else ''
193+
194+
if 'title="' in attrs:
195+
gateway_attrs = attrs.replace('title="', f'title="{link} ', 1)
196+
else:
197+
gateway_attrs = attrs + f' title="{link}"'
198+
gateway_attrs_str = f' {{{gateway_attrs}}}'
199+
200+
# Indent code lines for proper markdown formatting within tabs
201+
# Always add 4 spaces to every line (even empty ones) to preserve annotations
202+
code_lines = code.split('\n')
203+
indented_code = '\n'.join(' ' + line for line in code_lines)
204+
205+
gateway_code_lines = gateway_code.split('\n')
206+
indented_gateway_code = '\n'.join(' ' + line for line in gateway_code_lines)
207+
208+
# Indent annotation definitions if present (need to be inside tabs for Material to work)
209+
indented_annotations = ''
210+
if annotations:
211+
# Remove surrounding newlines and indent each line with 4 spaces
212+
annotation_lines = annotations.strip().split('\n')
213+
indented_annotations = '\n\n' + '\n'.join(' ' + line for line in annotation_lines) + '\n\n'
214+
215+
return f"""\
216+
=== "With Pydantic AI Gateway"
217+
218+
```python{gateway_attrs_str}
219+
{indented_gateway_code}
220+
```{indented_annotations}
221+
222+
=== "Directly to Provider API"
223+
224+
```python{attrs_str}
225+
{indented_code}
226+
```{indented_annotations}"""

docs/.overrides/main.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{% extends "base.html" %}
2+
3+
{% block announce %}
4+
<strong>
5+
<a href="/gateway">Pydantic AI Gateway</a> is now available! 🚀
6+
Enterprise-ready AI model routing: One key for all your models with real-time monitoring and budget control that works.
7+
</strong>
8+
{% endblock %}

docs/agents.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,8 @@ async def main():
320320
content='What is the capital of France?',
321321
timestamp=datetime.datetime(...),
322322
)
323-
]
323+
],
324+
run_id='...',
324325
)
325326
),
326327
CallToolsNode(
@@ -329,6 +330,7 @@ async def main():
329330
usage=RequestUsage(input_tokens=56, output_tokens=7),
330331
model_name='gpt-5',
331332
timestamp=datetime.datetime(...),
333+
run_id='...',
332334
)
333335
),
334336
End(data=FinalResult(output='The capital of France is Paris.')),
@@ -382,7 +384,8 @@ async def main():
382384
content='What is the capital of France?',
383385
timestamp=datetime.datetime(...),
384386
)
385-
]
387+
],
388+
run_id='...',
386389
)
387390
),
388391
CallToolsNode(
@@ -391,6 +394,7 @@ async def main():
391394
usage=RequestUsage(input_tokens=56, output_tokens=7),
392395
model_name='gpt-5',
393396
timestamp=datetime.datetime(...),
397+
run_id='...',
394398
)
395399
),
396400
End(data=FinalResult(output='The capital of France is Paris.')),
@@ -1044,7 +1048,8 @@ with capture_run_messages() as messages: # (2)!
10441048
content='Please get me the volume of a box with size 6.',
10451049
timestamp=datetime.datetime(...),
10461050
)
1047-
]
1051+
],
1052+
run_id='...',
10481053
),
10491054
ModelResponse(
10501055
parts=[
@@ -1057,6 +1062,7 @@ with capture_run_messages() as messages: # (2)!
10571062
usage=RequestUsage(input_tokens=62, output_tokens=4),
10581063
model_name='gpt-5',
10591064
timestamp=datetime.datetime(...),
1065+
run_id='...',
10601066
),
10611067
ModelRequest(
10621068
parts=[
@@ -1066,7 +1072,8 @@ with capture_run_messages() as messages: # (2)!
10661072
tool_call_id='pyd_ai_tool_call_id',
10671073
timestamp=datetime.datetime(...),
10681074
)
1069-
]
1075+
],
1076+
run_id='...',
10701077
),
10711078
ModelResponse(
10721079
parts=[
@@ -1079,6 +1086,7 @@ with capture_run_messages() as messages: # (2)!
10791086
usage=RequestUsage(input_tokens=72, output_tokens=8),
10801087
model_name='gpt-5',
10811088
timestamp=datetime.datetime(...),
1089+
run_id='...',
10821090
),
10831091
]
10841092
"""

docs/api/models/function.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ async def model_function(
2929
content='Testing my agent...',
3030
timestamp=datetime.datetime(...),
3131
)
32-
]
32+
],
33+
run_id='...',
3334
)
3435
]
3536
"""

0 commit comments

Comments
 (0)