1616
1717def 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_root = (DOCS_ROOT / page .file .src_uri ).parent
20+ markdown = inject_snippets (markdown , relative_path_root )
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_root )
2325 return markdown
2426
2527
@@ -39,6 +41,7 @@ def on_env(env: Environment, config: Config, files: Files) -> Environment:
3941
4042def 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_root : 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_root ),
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_root : 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+ relative_path = docs_path .relative_to (relative_path_root , walk_up = True )
190+ link = f"<a href='{ relative_path } ' style='float: right;'>Learn about Gateway</a>"
191+
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 } """
0 commit comments