amd-gaia 0.14.3__py3-none-any.whl → 0.15.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/METADATA +223 -223
  2. amd_gaia-0.15.1.dist-info/RECORD +178 -0
  3. {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/entry_points.txt +1 -0
  4. {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/licenses/LICENSE.md +20 -20
  5. gaia/__init__.py +29 -29
  6. gaia/agents/__init__.py +19 -19
  7. gaia/agents/base/__init__.py +9 -9
  8. gaia/agents/base/agent.py +2177 -2177
  9. gaia/agents/base/api_agent.py +120 -120
  10. gaia/agents/base/console.py +1841 -1841
  11. gaia/agents/base/errors.py +237 -237
  12. gaia/agents/base/mcp_agent.py +86 -86
  13. gaia/agents/base/tools.py +83 -83
  14. gaia/agents/blender/agent.py +556 -556
  15. gaia/agents/blender/agent_simple.py +133 -135
  16. gaia/agents/blender/app.py +211 -211
  17. gaia/agents/blender/app_simple.py +41 -41
  18. gaia/agents/blender/core/__init__.py +16 -16
  19. gaia/agents/blender/core/materials.py +506 -506
  20. gaia/agents/blender/core/objects.py +316 -316
  21. gaia/agents/blender/core/rendering.py +225 -225
  22. gaia/agents/blender/core/scene.py +220 -220
  23. gaia/agents/blender/core/view.py +146 -146
  24. gaia/agents/chat/__init__.py +9 -9
  25. gaia/agents/chat/agent.py +835 -835
  26. gaia/agents/chat/app.py +1058 -1058
  27. gaia/agents/chat/session.py +508 -508
  28. gaia/agents/chat/tools/__init__.py +15 -15
  29. gaia/agents/chat/tools/file_tools.py +96 -96
  30. gaia/agents/chat/tools/rag_tools.py +1729 -1729
  31. gaia/agents/chat/tools/shell_tools.py +436 -436
  32. gaia/agents/code/__init__.py +7 -7
  33. gaia/agents/code/agent.py +549 -549
  34. gaia/agents/code/cli.py +377 -0
  35. gaia/agents/code/models.py +135 -135
  36. gaia/agents/code/orchestration/__init__.py +24 -24
  37. gaia/agents/code/orchestration/checklist_executor.py +1763 -1763
  38. gaia/agents/code/orchestration/checklist_generator.py +713 -713
  39. gaia/agents/code/orchestration/factories/__init__.py +9 -9
  40. gaia/agents/code/orchestration/factories/base.py +63 -63
  41. gaia/agents/code/orchestration/factories/nextjs_factory.py +118 -118
  42. gaia/agents/code/orchestration/factories/python_factory.py +106 -106
  43. gaia/agents/code/orchestration/orchestrator.py +841 -841
  44. gaia/agents/code/orchestration/project_analyzer.py +391 -391
  45. gaia/agents/code/orchestration/steps/__init__.py +67 -67
  46. gaia/agents/code/orchestration/steps/base.py +188 -188
  47. gaia/agents/code/orchestration/steps/error_handler.py +314 -314
  48. gaia/agents/code/orchestration/steps/nextjs.py +828 -828
  49. gaia/agents/code/orchestration/steps/python.py +307 -307
  50. gaia/agents/code/orchestration/template_catalog.py +469 -469
  51. gaia/agents/code/orchestration/workflows/__init__.py +14 -14
  52. gaia/agents/code/orchestration/workflows/base.py +80 -80
  53. gaia/agents/code/orchestration/workflows/nextjs.py +186 -186
  54. gaia/agents/code/orchestration/workflows/python.py +94 -94
  55. gaia/agents/code/prompts/__init__.py +11 -11
  56. gaia/agents/code/prompts/base_prompt.py +77 -77
  57. gaia/agents/code/prompts/code_patterns.py +2036 -2036
  58. gaia/agents/code/prompts/nextjs_prompt.py +40 -40
  59. gaia/agents/code/prompts/python_prompt.py +109 -109
  60. gaia/agents/code/schema_inference.py +365 -365
  61. gaia/agents/code/system_prompt.py +41 -41
  62. gaia/agents/code/tools/__init__.py +42 -42
  63. gaia/agents/code/tools/cli_tools.py +1138 -1138
  64. gaia/agents/code/tools/code_formatting.py +319 -319
  65. gaia/agents/code/tools/code_tools.py +769 -769
  66. gaia/agents/code/tools/error_fixing.py +1347 -1347
  67. gaia/agents/code/tools/external_tools.py +180 -180
  68. gaia/agents/code/tools/file_io.py +845 -845
  69. gaia/agents/code/tools/prisma_tools.py +190 -190
  70. gaia/agents/code/tools/project_management.py +1016 -1016
  71. gaia/agents/code/tools/testing.py +321 -321
  72. gaia/agents/code/tools/typescript_tools.py +122 -122
  73. gaia/agents/code/tools/validation_parsing.py +461 -461
  74. gaia/agents/code/tools/validation_tools.py +806 -806
  75. gaia/agents/code/tools/web_dev_tools.py +1758 -1758
  76. gaia/agents/code/validators/__init__.py +16 -16
  77. gaia/agents/code/validators/antipattern_checker.py +241 -241
  78. gaia/agents/code/validators/ast_analyzer.py +197 -197
  79. gaia/agents/code/validators/requirements_validator.py +145 -145
  80. gaia/agents/code/validators/syntax_validator.py +171 -171
  81. gaia/agents/docker/__init__.py +7 -7
  82. gaia/agents/docker/agent.py +642 -642
  83. gaia/agents/emr/__init__.py +8 -8
  84. gaia/agents/emr/agent.py +1506 -1506
  85. gaia/agents/emr/cli.py +1322 -1322
  86. gaia/agents/emr/constants.py +475 -475
  87. gaia/agents/emr/dashboard/__init__.py +4 -4
  88. gaia/agents/emr/dashboard/server.py +1974 -1974
  89. gaia/agents/jira/__init__.py +11 -11
  90. gaia/agents/jira/agent.py +894 -894
  91. gaia/agents/jira/jql_templates.py +299 -299
  92. gaia/agents/routing/__init__.py +7 -7
  93. gaia/agents/routing/agent.py +567 -570
  94. gaia/agents/routing/system_prompt.py +75 -75
  95. gaia/agents/summarize/__init__.py +11 -0
  96. gaia/agents/summarize/agent.py +885 -0
  97. gaia/agents/summarize/prompts.py +129 -0
  98. gaia/api/__init__.py +23 -23
  99. gaia/api/agent_registry.py +238 -238
  100. gaia/api/app.py +305 -305
  101. gaia/api/openai_server.py +575 -575
  102. gaia/api/schemas.py +186 -186
  103. gaia/api/sse_handler.py +373 -373
  104. gaia/apps/__init__.py +4 -4
  105. gaia/apps/llm/__init__.py +6 -6
  106. gaia/apps/llm/app.py +173 -169
  107. gaia/apps/summarize/app.py +116 -633
  108. gaia/apps/summarize/html_viewer.py +133 -133
  109. gaia/apps/summarize/pdf_formatter.py +284 -284
  110. gaia/audio/__init__.py +2 -2
  111. gaia/audio/audio_client.py +439 -439
  112. gaia/audio/audio_recorder.py +269 -269
  113. gaia/audio/kokoro_tts.py +599 -599
  114. gaia/audio/whisper_asr.py +432 -432
  115. gaia/chat/__init__.py +16 -16
  116. gaia/chat/app.py +430 -430
  117. gaia/chat/prompts.py +522 -522
  118. gaia/chat/sdk.py +1228 -1225
  119. gaia/cli.py +5481 -5621
  120. gaia/database/__init__.py +10 -10
  121. gaia/database/agent.py +176 -176
  122. gaia/database/mixin.py +290 -290
  123. gaia/database/testing.py +64 -64
  124. gaia/eval/batch_experiment.py +2332 -2332
  125. gaia/eval/claude.py +542 -542
  126. gaia/eval/config.py +37 -37
  127. gaia/eval/email_generator.py +512 -512
  128. gaia/eval/eval.py +3179 -3179
  129. gaia/eval/groundtruth.py +1130 -1130
  130. gaia/eval/transcript_generator.py +582 -582
  131. gaia/eval/webapp/README.md +167 -167
  132. gaia/eval/webapp/package-lock.json +875 -875
  133. gaia/eval/webapp/package.json +20 -20
  134. gaia/eval/webapp/public/app.js +3402 -3402
  135. gaia/eval/webapp/public/index.html +87 -87
  136. gaia/eval/webapp/public/styles.css +3661 -3661
  137. gaia/eval/webapp/server.js +415 -415
  138. gaia/eval/webapp/test-setup.js +72 -72
  139. gaia/llm/__init__.py +9 -2
  140. gaia/llm/base_client.py +60 -0
  141. gaia/llm/exceptions.py +12 -0
  142. gaia/llm/factory.py +70 -0
  143. gaia/llm/lemonade_client.py +3236 -3221
  144. gaia/llm/lemonade_manager.py +294 -294
  145. gaia/llm/providers/__init__.py +9 -0
  146. gaia/llm/providers/claude.py +108 -0
  147. gaia/llm/providers/lemonade.py +120 -0
  148. gaia/llm/providers/openai_provider.py +79 -0
  149. gaia/llm/vlm_client.py +382 -382
  150. gaia/logger.py +189 -189
  151. gaia/mcp/agent_mcp_server.py +245 -245
  152. gaia/mcp/blender_mcp_client.py +138 -138
  153. gaia/mcp/blender_mcp_server.py +648 -648
  154. gaia/mcp/context7_cache.py +332 -332
  155. gaia/mcp/external_services.py +518 -518
  156. gaia/mcp/mcp_bridge.py +811 -550
  157. gaia/mcp/servers/__init__.py +6 -6
  158. gaia/mcp/servers/docker_mcp.py +83 -83
  159. gaia/perf_analysis.py +361 -0
  160. gaia/rag/__init__.py +10 -10
  161. gaia/rag/app.py +293 -293
  162. gaia/rag/demo.py +304 -304
  163. gaia/rag/pdf_utils.py +235 -235
  164. gaia/rag/sdk.py +2194 -2194
  165. gaia/security.py +163 -163
  166. gaia/talk/app.py +289 -289
  167. gaia/talk/sdk.py +538 -538
  168. gaia/testing/__init__.py +87 -87
  169. gaia/testing/assertions.py +330 -330
  170. gaia/testing/fixtures.py +333 -333
  171. gaia/testing/mocks.py +493 -493
  172. gaia/util.py +46 -46
  173. gaia/utils/__init__.py +33 -33
  174. gaia/utils/file_watcher.py +675 -675
  175. gaia/utils/parsing.py +223 -223
  176. gaia/version.py +100 -100
  177. amd_gaia-0.14.3.dist-info/RECORD +0 -168
  178. gaia/agents/code/app.py +0 -266
  179. gaia/llm/llm_client.py +0 -729
  180. {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/WHEEL +0 -0
  181. {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/top_level.txt +0 -0
gaia/api/openai_server.py CHANGED
@@ -1,575 +1,575 @@
1
- # Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2
- # SPDX-License-Identifier: MIT
3
- """
4
- OpenAI-compatible API server for GAIA
5
-
6
- This module provides a FastAPI server that exposes GAIA agents via
7
- OpenAI-compatible endpoints, allowing VSCode and other tools to use
8
- GAIA agents as if they were OpenAI models.
9
-
10
- Endpoints:
11
- POST /v1/chat/completions - Create chat completion (streaming and non-streaming)
12
- GET /v1/models - List available models (GAIA agents)
13
- GET /health - Health check
14
- """
15
-
16
- import asyncio
17
- import json
18
- import logging
19
- import os
20
- import time
21
- import uuid
22
- from typing import AsyncGenerator
23
-
24
- from fastapi import FastAPI, HTTPException, Request
25
- from fastapi.middleware.cors import CORSMiddleware
26
- from fastapi.responses import StreamingResponse
27
-
28
- from gaia.agents.base.api_agent import ApiAgent
29
-
30
- from .agent_registry import registry
31
- from .schemas import (
32
- ChatCompletionChoice,
33
- ChatCompletionRequest,
34
- ChatCompletionResponse,
35
- ChatCompletionResponseMessage,
36
- ModelListResponse,
37
- UsageInfo,
38
- )
39
-
40
- # Configure logging
41
- logger = logging.getLogger(__name__)
42
-
43
- # Set logger level based on debug flag
44
- if os.environ.get("GAIA_API_DEBUG") == "1":
45
- logger.setLevel(logging.DEBUG)
46
- logger.info("Debug logging enabled for API server")
47
-
48
-
49
- def extract_workspace_root(messages):
50
- """
51
- Extract workspace root path from GitHub Copilot messages.
52
-
53
- GitHub Copilot includes workspace info in messages like:
54
- <workspace_info>
55
- I am working in a workspace with the following folders:
56
- - /Users/username/path/to/workspace
57
- </workspace_info>
58
-
59
- Args:
60
- messages: List of ChatMessage objects
61
-
62
- Returns:
63
- str: Workspace root path, or None if not found
64
- """
65
- import re
66
-
67
- for msg in messages:
68
- if msg.role == "user" and msg.content:
69
- # Look for workspace_info section
70
- workspace_match = re.search(
71
- r"<workspace_info>.*?following folders:\s*\n\s*-\s*([^\s\n]+)",
72
- msg.content,
73
- re.DOTALL,
74
- )
75
- if workspace_match:
76
- return workspace_match.group(1).strip()
77
-
78
- return None
79
-
80
-
81
- # Initialize FastAPI app
82
- app = FastAPI(
83
- title="GAIA OpenAI-Compatible API",
84
- description="OpenAI-compatible API for GAIA agents",
85
- version="1.0.0",
86
- )
87
-
88
- # CORS middleware - allow all origins for development
89
- app.add_middleware(
90
- CORSMiddleware,
91
- allow_origins=["*"],
92
- allow_credentials=True,
93
- allow_methods=["*"],
94
- allow_headers=["*"],
95
- )
96
-
97
-
98
- # Raw request logging middleware (debug mode only)
99
- @app.middleware("http")
100
- async def log_raw_requests(request: Request, call_next):
101
- """
102
- Middleware to log raw HTTP requests when debug mode is enabled.
103
- For streaming endpoints, only log headers to avoid breaking SSE.
104
- """
105
- if os.environ.get("GAIA_API_DEBUG") == "1":
106
- logger.debug("=" * 80)
107
- logger.debug("📥 RAW HTTP REQUEST")
108
- logger.debug("=" * 80)
109
- logger.debug(f"Path: {request.url.path}")
110
- logger.debug(f"Method: {request.method}")
111
- logger.debug("Headers:")
112
- for name, value in request.headers.items():
113
- logger.debug(f" {name}: {value}")
114
-
115
- # DON'T read body for streaming endpoints - it breaks ASGI message flow
116
- # Per FastAPI docs: "Never read the request body in middleware for streaming responses"
117
- if request.url.path == "/v1/chat/completions" and request.method == "POST":
118
- logger.debug(
119
- "Body: [Skipped for streaming endpoint - prevents ASGI message flow disruption]"
120
- )
121
- else:
122
- # Safe to read body for non-streaming endpoints
123
- body_bytes = await request.body()
124
- logger.debug(f"Body (raw bytes length): {len(body_bytes)}")
125
- if body_bytes:
126
- try:
127
- body_str = body_bytes.decode("utf-8")
128
- logger.debug("Body (decoded UTF-8):")
129
- logger.debug(body_str)
130
- # Try to pretty-print JSON
131
- try:
132
- body_json = json.loads(body_str)
133
- logger.debug("Body (parsed JSON):")
134
- logger.debug(json.dumps(body_json, indent=2))
135
- except json.JSONDecodeError:
136
- pass
137
- except UnicodeDecodeError:
138
- logger.debug("Body contains non-UTF-8 data")
139
-
140
- logger.debug("=" * 80)
141
-
142
- response = await call_next(request)
143
- return response
144
-
145
-
146
- @app.post("/v1/chat/completions")
147
- async def create_chat_completion(request: ChatCompletionRequest):
148
- """
149
- Create chat completion (OpenAI-compatible endpoint).
150
-
151
- Supports both streaming (SSE) and non-streaming responses.
152
-
153
- Args:
154
- request: Chat completion request with model, messages, and options
155
-
156
- Returns:
157
- For non-streaming: ChatCompletionResponse
158
- For streaming: StreamingResponse with SSE chunks
159
-
160
- Raises:
161
- HTTPException 404: Model not found
162
- HTTPException 400: No user message in request
163
-
164
- Example:
165
- Non-streaming:
166
- ```
167
- POST /v1/chat/completions
168
- {
169
- "model": "gaia-code",
170
- "messages": [{"role": "user", "content": "Write hello world"}],
171
- "stream": false
172
- }
173
- ```
174
-
175
- Streaming:
176
- ```
177
- POST /v1/chat/completions
178
- {
179
- "model": "gaia-code",
180
- "messages": [{"role": "user", "content": "Write hello world"}],
181
- "stream": true
182
- }
183
- ```
184
- """
185
- # Debug logging: trace incoming request
186
- if os.environ.get("GAIA_API_DEBUG") == "1":
187
- logger.debug("=" * 80)
188
- logger.debug("📥 INCOMING CHAT COMPLETION REQUEST")
189
- logger.debug("=" * 80)
190
- logger.debug(f"Model: {request.model}")
191
- logger.debug(f"Stream: {request.stream}")
192
- logger.debug(f"Message count: {len(request.messages)}")
193
- logger.debug("-" * 80)
194
-
195
- for i, msg in enumerate(request.messages):
196
- logger.debug(f"Message {i}:")
197
- logger.debug(f" Role: {msg.role}")
198
- # Preview content (truncate if too long)
199
- content_preview = (
200
- msg.content[:500] if len(msg.content) > 500 else msg.content
201
- )
202
- if len(msg.content) > 500:
203
- content_preview += (
204
- f"\n ... (truncated, total length: {len(msg.content)} chars)"
205
- )
206
- logger.debug(f" Content:\n{content_preview}")
207
- logger.debug("-" * 40)
208
-
209
- # Log additional request parameters
210
- logger.debug("Request parameters:")
211
- logger.debug(f" temperature: {getattr(request, 'temperature', 'not set')}")
212
- logger.debug(f" max_tokens: {getattr(request, 'max_tokens', 'not set')}")
213
- logger.debug(f" top_p: {getattr(request, 'top_p', 'not set')}")
214
- logger.debug("=" * 80)
215
-
216
- # Validate model exists
217
- if not registry.model_exists(request.model):
218
- raise HTTPException(
219
- status_code=404, detail=f"Model '{request.model}' not found"
220
- )
221
-
222
- # Extract workspace root from messages (for converting relative paths to absolute)
223
- workspace_root = extract_workspace_root(request.messages)
224
- if os.environ.get("GAIA_API_DEBUG") == "1" and workspace_root:
225
- logger.debug(f"📁 Extracted workspace root: {workspace_root}")
226
-
227
- # Extract user query from messages (get last user message)
228
- user_message = next(
229
- (m.content for m in reversed(request.messages) if m.role == "user"), None
230
- )
231
-
232
- if not user_message:
233
- raise HTTPException(
234
- status_code=400, detail="No user message found in messages array"
235
- )
236
-
237
- # Debug logging: show what we're passing to the agent
238
- if os.environ.get("GAIA_API_DEBUG") == "1":
239
- logger.debug("🔄 EXTRACTED FOR AGENT:")
240
- logger.debug(f"Passing to agent: {user_message[:500]}...")
241
- if len(user_message) > 500:
242
- logger.debug(f"(Total length: {len(user_message)} chars)")
243
- logger.debug("=" * 80)
244
-
245
- # Get agent instance for this model
246
- try:
247
- agent = registry.get_agent(request.model)
248
- except ValueError as e:
249
- raise HTTPException(status_code=404, detail=str(e))
250
-
251
- # Handle streaming vs non-streaming
252
- if request.stream:
253
- # Debug logging for streaming mode
254
- if os.environ.get("GAIA_API_DEBUG") == "1":
255
- logger.debug("🌊 Using STREAMING mode")
256
-
257
- return StreamingResponse(
258
- create_sse_stream(
259
- agent, user_message, request.model, workspace_root=workspace_root
260
- ),
261
- media_type="text/event-stream",
262
- headers={
263
- "Cache-Control": "no-cache",
264
- "Connection": "keep-alive",
265
- "X-Accel-Buffering": "no", # Disable proxy buffering
266
- },
267
- )
268
- else:
269
- # Debug logging for non-streaming mode
270
- if os.environ.get("GAIA_API_DEBUG") == "1":
271
- logger.debug("📦 Using NON-STREAMING mode")
272
-
273
- # Process query synchronously with workspace root
274
- result = agent.process_query(user_message, workspace_root=workspace_root)
275
-
276
- # Debug logging: show what agent returned
277
- if os.environ.get("GAIA_API_DEBUG") == "1":
278
- logger.debug("=" * 80)
279
- logger.debug("📤 AGENT RESPONSE (NON-STREAMING)")
280
- logger.debug("=" * 80)
281
- logger.debug(f"Result type: {type(result)}")
282
- logger.debug(
283
- f"Result keys: {list(result.keys()) if isinstance(result, dict) else 'N/A'}"
284
- )
285
- logger.debug(
286
- f"Status: {result.get('status') if isinstance(result, dict) else 'N/A'}"
287
- )
288
- logger.debug(
289
- f"Steps taken: {result.get('steps_taken') if isinstance(result, dict) else 'N/A'}"
290
- )
291
- result_preview = (
292
- str(result.get("result", ""))[:200]
293
- if isinstance(result, dict)
294
- else str(result)[:200]
295
- )
296
- logger.debug(f"Result preview: {result_preview}...")
297
- logger.debug("=" * 80)
298
-
299
- # Extract content from result
300
- content = result.get("result", str(result))
301
-
302
- # Estimate tokens
303
- if isinstance(agent, ApiAgent):
304
- prompt_tokens = agent.estimate_tokens(user_message)
305
- completion_tokens = agent.estimate_tokens(content)
306
- else:
307
- prompt_tokens = len(user_message) // 4
308
- completion_tokens = len(content) // 4
309
-
310
- return ChatCompletionResponse(
311
- id=f"chatcmpl-{uuid.uuid4().hex[:24]}",
312
- object="chat.completion",
313
- created=int(time.time()),
314
- model=request.model,
315
- choices=[
316
- ChatCompletionChoice(
317
- index=0,
318
- message=ChatCompletionResponseMessage(
319
- role="assistant",
320
- content=content,
321
- ),
322
- finish_reason="stop",
323
- )
324
- ],
325
- usage=UsageInfo(
326
- prompt_tokens=prompt_tokens,
327
- completion_tokens=completion_tokens,
328
- total_tokens=prompt_tokens + completion_tokens,
329
- ),
330
- )
331
-
332
-
333
- async def create_sse_stream(
334
- agent, query: str, model: str, workspace_root: str = None
335
- ) -> AsyncGenerator[str, None]:
336
- """
337
- Create Server-Sent Events stream for chat completion.
338
-
339
- This function processes the agent query in a thread pool (to avoid blocking)
340
- and streams agent progress events in real-time via the SSEOutputHandler.
341
-
342
- Args:
343
- agent: Agent instance (with SSEOutputHandler)
344
- query: User query string
345
- model: Model ID
346
- workspace_root: Optional workspace root path for absolute file paths
347
-
348
- Yields:
349
- SSE-formatted chunks with "data: " prefix
350
-
351
- Example output:
352
- data: {"id":"chatcmpl-123","object":"chat.completion.chunk",...}
353
- data: {"id":"chatcmpl-123","object":"chat.completion.chunk",...}
354
- data: [DONE]
355
- """
356
- # Debug logging - FIRST LINE to confirm generator starts
357
- if os.environ.get("GAIA_API_DEBUG") == "1":
358
- logger.debug("🎬 Generator started! Client is consuming the stream.")
359
-
360
- completion_id = f"chatcmpl-{uuid.uuid4().hex[:24]}"
361
- created = int(time.time())
362
-
363
- # First chunk with role
364
- first_chunk = {
365
- "id": completion_id,
366
- "object": "chat.completion.chunk",
367
- "created": created,
368
- "model": model,
369
- "choices": [
370
- {
371
- "index": 0,
372
- "delta": {"role": "assistant", "content": ""},
373
- "finish_reason": None,
374
- }
375
- ],
376
- }
377
- if os.environ.get("GAIA_API_DEBUG") == "1":
378
- logger.debug(f"📤 Sending first chunk: {json.dumps(first_chunk)}")
379
- yield f"data: {json.dumps(first_chunk)}\n\n"
380
-
381
- # Debug logging
382
- if os.environ.get("GAIA_API_DEBUG") == "1":
383
- logger.debug("🔄 Starting agent query processing in thread pool...")
384
-
385
- # Process query in thread pool to avoid blocking event loop
386
- loop = asyncio.get_event_loop()
387
-
388
- # Get the SSEOutputHandler from the agent (try output_handler first, fall back to console)
389
- output_handler = getattr(agent, "output_handler", None) or getattr(
390
- agent, "console", None
391
- )
392
-
393
- try:
394
- # Start processing in background
395
- task = loop.run_in_executor(
396
- None, lambda: agent.process_query(query, workspace_root=workspace_root)
397
- )
398
-
399
- # Stream events as they are generated
400
- while not task.done():
401
- # Check for new events from the output handler
402
- if hasattr(output_handler, "has_events") and output_handler.has_events():
403
- events = output_handler.get_events()
404
-
405
- for event in events:
406
- event_type = event.get("type", "message")
407
-
408
- # Check if this event should be streamed to client
409
- if not output_handler.should_stream_as_content(event_type):
410
- # Still log it in debug mode
411
- if os.environ.get("GAIA_API_DEBUG") == "1":
412
- logger.debug(f"📝 Skipping event: {event_type}")
413
- continue
414
-
415
- # Format event as clean content
416
- content_text = output_handler.format_event_as_content(event)
417
-
418
- # Skip empty content (filtered events)
419
- if not content_text:
420
- continue
421
-
422
- content_chunk = {
423
- "id": completion_id,
424
- "object": "chat.completion.chunk",
425
- "created": created,
426
- "model": model,
427
- "choices": [
428
- {
429
- "index": 0,
430
- "delta": {"content": content_text},
431
- "finish_reason": None,
432
- }
433
- ],
434
- }
435
-
436
- if os.environ.get("GAIA_API_DEBUG") == "1":
437
- logger.debug(
438
- f"📤 Streaming event: {event_type} -> {content_text[:100]}"
439
- )
440
-
441
- yield f"data: {json.dumps(content_chunk)}\n\n"
442
-
443
- # Small delay to avoid busy waiting
444
- await asyncio.sleep(0.1)
445
-
446
- # Get the final result
447
- result = await task
448
-
449
- # Get any remaining events
450
- if hasattr(output_handler, "has_events") and output_handler.has_events():
451
- events = output_handler.get_events()
452
- for event in events:
453
- event_type = event.get("type", "message")
454
-
455
- # Check if this event should be streamed
456
- if not output_handler.should_stream_as_content(event_type):
457
- continue
458
-
459
- # Format event as clean content
460
- content_text = output_handler.format_event_as_content(event)
461
-
462
- # Skip empty content
463
- if not content_text:
464
- continue
465
-
466
- content_chunk = {
467
- "id": completion_id,
468
- "object": "chat.completion.chunk",
469
- "created": created,
470
- "model": model,
471
- "choices": [
472
- {
473
- "index": 0,
474
- "delta": {"content": content_text},
475
- "finish_reason": None,
476
- }
477
- ],
478
- }
479
- yield f"data: {json.dumps(content_chunk)}\n\n"
480
-
481
- # Debug logging: show what agent returned
482
- if os.environ.get("GAIA_API_DEBUG") == "1":
483
- logger.debug("=" * 80)
484
- logger.debug("📤 AGENT RESPONSE (STREAMING)")
485
- logger.debug("=" * 80)
486
- logger.debug(f"Result type: {type(result)}")
487
- logger.debug(
488
- f"Result keys: {list(result.keys()) if isinstance(result, dict) else 'N/A'}"
489
- )
490
- logger.debug(
491
- f"Status: {result.get('status') if isinstance(result, dict) else 'N/A'}"
492
- )
493
- logger.debug(
494
- f"Steps taken: {result.get('steps_taken') if isinstance(result, dict) else 'N/A'}"
495
- )
496
- result_preview = (
497
- str(result.get("result", ""))[:200]
498
- if isinstance(result, dict)
499
- else str(result)[:200]
500
- )
501
- logger.debug(f"Result preview: {result_preview}...")
502
- logger.debug("=" * 80)
503
-
504
- except Exception as e:
505
- # Log and re-raise errors
506
- logger.error(f"❌ Agent query processing failed: {e}", exc_info=True)
507
- raise
508
-
509
- # Final chunk with finish_reason
510
- final_chunk = {
511
- "id": completion_id,
512
- "object": "chat.completion.chunk",
513
- "created": created,
514
- "model": model,
515
- "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
516
- }
517
- if os.environ.get("GAIA_API_DEBUG") == "1":
518
- logger.debug("📤 Sending final chunk with finish_reason=stop")
519
- yield f"data: {json.dumps(final_chunk)}\n\n"
520
-
521
- # Done marker
522
- if os.environ.get("GAIA_API_DEBUG") == "1":
523
- logger.debug("✅ SSE stream complete. Sending [DONE] marker.")
524
- yield "data: [DONE]\n\n"
525
-
526
-
527
- @app.get("/v1/models")
528
- async def list_models() -> ModelListResponse:
529
- """
530
- List available models (OpenAI-compatible endpoint).
531
-
532
- Note: These are GAIA agents exposed as "models", not LLM models.
533
- Lemonade manages the actual LLM models underneath.
534
-
535
- Returns:
536
- ModelListResponse with list of available agent "models"
537
-
538
- Example:
539
- ```
540
- GET /v1/models
541
- {
542
- "object": "list",
543
- "data": [
544
- {
545
- "id": "gaia-code",
546
- "object": "model",
547
- "created": 1234567890,
548
- "owned_by": "amd-gaia"
549
- },
550
- ...
551
- ]
552
- }
553
- ```
554
- """
555
- return ModelListResponse(object="list", data=registry.list_models())
556
-
557
-
558
- @app.get("/health")
559
- async def health_check():
560
- """
561
- Health check endpoint.
562
-
563
- Returns:
564
- Status and service name
565
-
566
- Example:
567
- ```
568
- GET /health
569
- {
570
- "status": "ok",
571
- "service": "gaia-api"
572
- }
573
- ```
574
- """
575
- return {"status": "ok", "service": "gaia-api"}
1
+ # Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2
+ # SPDX-License-Identifier: MIT
3
+ """
4
+ OpenAI-compatible API server for GAIA
5
+
6
+ This module provides a FastAPI server that exposes GAIA agents via
7
+ OpenAI-compatible endpoints, allowing VSCode and other tools to use
8
+ GAIA agents as if they were OpenAI models.
9
+
10
+ Endpoints:
11
+ POST /v1/chat/completions - Create chat completion (streaming and non-streaming)
12
+ GET /v1/models - List available models (GAIA agents)
13
+ GET /health - Health check
14
+ """
15
+
16
+ import asyncio
17
+ import json
18
+ import logging
19
+ import os
20
+ import time
21
+ import uuid
22
+ from typing import AsyncGenerator
23
+
24
+ from fastapi import FastAPI, HTTPException, Request
25
+ from fastapi.middleware.cors import CORSMiddleware
26
+ from fastapi.responses import StreamingResponse
27
+
28
+ from gaia.agents.base.api_agent import ApiAgent
29
+
30
+ from .agent_registry import registry
31
+ from .schemas import (
32
+ ChatCompletionChoice,
33
+ ChatCompletionRequest,
34
+ ChatCompletionResponse,
35
+ ChatCompletionResponseMessage,
36
+ ModelListResponse,
37
+ UsageInfo,
38
+ )
39
+
40
+ # Configure logging
41
+ logger = logging.getLogger(__name__)
42
+
43
+ # Set logger level based on debug flag
44
+ if os.environ.get("GAIA_API_DEBUG") == "1":
45
+ logger.setLevel(logging.DEBUG)
46
+ logger.info("Debug logging enabled for API server")
47
+
48
+
49
+ def extract_workspace_root(messages):
50
+ """
51
+ Extract workspace root path from GitHub Copilot messages.
52
+
53
+ GitHub Copilot includes workspace info in messages like:
54
+ <workspace_info>
55
+ I am working in a workspace with the following folders:
56
+ - /Users/username/path/to/workspace
57
+ </workspace_info>
58
+
59
+ Args:
60
+ messages: List of ChatMessage objects
61
+
62
+ Returns:
63
+ str: Workspace root path, or None if not found
64
+ """
65
+ import re
66
+
67
+ for msg in messages:
68
+ if msg.role == "user" and msg.content:
69
+ # Look for workspace_info section
70
+ workspace_match = re.search(
71
+ r"<workspace_info>.*?following folders:\s*\n\s*-\s*([^\s\n]+)",
72
+ msg.content,
73
+ re.DOTALL,
74
+ )
75
+ if workspace_match:
76
+ return workspace_match.group(1).strip()
77
+
78
+ return None
79
+
80
+
81
+ # Initialize FastAPI app
82
+ app = FastAPI(
83
+ title="GAIA OpenAI-Compatible API",
84
+ description="OpenAI-compatible API for GAIA agents",
85
+ version="1.0.0",
86
+ )
87
+
88
+ # CORS middleware - allow all origins for development
89
+ app.add_middleware(
90
+ CORSMiddleware,
91
+ allow_origins=["*"],
92
+ allow_credentials=True,
93
+ allow_methods=["*"],
94
+ allow_headers=["*"],
95
+ )
96
+
97
+
98
+ # Raw request logging middleware (debug mode only)
99
+ @app.middleware("http")
100
+ async def log_raw_requests(request: Request, call_next):
101
+ """
102
+ Middleware to log raw HTTP requests when debug mode is enabled.
103
+ For streaming endpoints, only log headers to avoid breaking SSE.
104
+ """
105
+ if os.environ.get("GAIA_API_DEBUG") == "1":
106
+ logger.debug("=" * 80)
107
+ logger.debug("📥 RAW HTTP REQUEST")
108
+ logger.debug("=" * 80)
109
+ logger.debug(f"Path: {request.url.path}")
110
+ logger.debug(f"Method: {request.method}")
111
+ logger.debug("Headers:")
112
+ for name, value in request.headers.items():
113
+ logger.debug(f" {name}: {value}")
114
+
115
+ # DON'T read body for streaming endpoints - it breaks ASGI message flow
116
+ # Per FastAPI docs: "Never read the request body in middleware for streaming responses"
117
+ if request.url.path == "/v1/chat/completions" and request.method == "POST":
118
+ logger.debug(
119
+ "Body: [Skipped for streaming endpoint - prevents ASGI message flow disruption]"
120
+ )
121
+ else:
122
+ # Safe to read body for non-streaming endpoints
123
+ body_bytes = await request.body()
124
+ logger.debug(f"Body (raw bytes length): {len(body_bytes)}")
125
+ if body_bytes:
126
+ try:
127
+ body_str = body_bytes.decode("utf-8")
128
+ logger.debug("Body (decoded UTF-8):")
129
+ logger.debug(body_str)
130
+ # Try to pretty-print JSON
131
+ try:
132
+ body_json = json.loads(body_str)
133
+ logger.debug("Body (parsed JSON):")
134
+ logger.debug(json.dumps(body_json, indent=2))
135
+ except json.JSONDecodeError:
136
+ pass
137
+ except UnicodeDecodeError:
138
+ logger.debug("Body contains non-UTF-8 data")
139
+
140
+ logger.debug("=" * 80)
141
+
142
+ response = await call_next(request)
143
+ return response
144
+
145
+
146
+ @app.post("/v1/chat/completions")
147
+ async def create_chat_completion(request: ChatCompletionRequest):
148
+ """
149
+ Create chat completion (OpenAI-compatible endpoint).
150
+
151
+ Supports both streaming (SSE) and non-streaming responses.
152
+
153
+ Args:
154
+ request: Chat completion request with model, messages, and options
155
+
156
+ Returns:
157
+ For non-streaming: ChatCompletionResponse
158
+ For streaming: StreamingResponse with SSE chunks
159
+
160
+ Raises:
161
+ HTTPException 404: Model not found
162
+ HTTPException 400: No user message in request
163
+
164
+ Example:
165
+ Non-streaming:
166
+ ```
167
+ POST /v1/chat/completions
168
+ {
169
+ "model": "gaia-code",
170
+ "messages": [{"role": "user", "content": "Write hello world"}],
171
+ "stream": false
172
+ }
173
+ ```
174
+
175
+ Streaming:
176
+ ```
177
+ POST /v1/chat/completions
178
+ {
179
+ "model": "gaia-code",
180
+ "messages": [{"role": "user", "content": "Write hello world"}],
181
+ "stream": true
182
+ }
183
+ ```
184
+ """
185
+ # Debug logging: trace incoming request
186
+ if os.environ.get("GAIA_API_DEBUG") == "1":
187
+ logger.debug("=" * 80)
188
+ logger.debug("📥 INCOMING CHAT COMPLETION REQUEST")
189
+ logger.debug("=" * 80)
190
+ logger.debug(f"Model: {request.model}")
191
+ logger.debug(f"Stream: {request.stream}")
192
+ logger.debug(f"Message count: {len(request.messages)}")
193
+ logger.debug("-" * 80)
194
+
195
+ for i, msg in enumerate(request.messages):
196
+ logger.debug(f"Message {i}:")
197
+ logger.debug(f" Role: {msg.role}")
198
+ # Preview content (truncate if too long)
199
+ content_preview = (
200
+ msg.content[:500] if len(msg.content) > 500 else msg.content
201
+ )
202
+ if len(msg.content) > 500:
203
+ content_preview += (
204
+ f"\n ... (truncated, total length: {len(msg.content)} chars)"
205
+ )
206
+ logger.debug(f" Content:\n{content_preview}")
207
+ logger.debug("-" * 40)
208
+
209
+ # Log additional request parameters
210
+ logger.debug("Request parameters:")
211
+ logger.debug(f" temperature: {getattr(request, 'temperature', 'not set')}")
212
+ logger.debug(f" max_tokens: {getattr(request, 'max_tokens', 'not set')}")
213
+ logger.debug(f" top_p: {getattr(request, 'top_p', 'not set')}")
214
+ logger.debug("=" * 80)
215
+
216
+ # Validate model exists
217
+ if not registry.model_exists(request.model):
218
+ raise HTTPException(
219
+ status_code=404, detail=f"Model '{request.model}' not found"
220
+ )
221
+
222
+ # Extract workspace root from messages (for converting relative paths to absolute)
223
+ workspace_root = extract_workspace_root(request.messages)
224
+ if os.environ.get("GAIA_API_DEBUG") == "1" and workspace_root:
225
+ logger.debug(f"📁 Extracted workspace root: {workspace_root}")
226
+
227
+ # Extract user query from messages (get last user message)
228
+ user_message = next(
229
+ (m.content for m in reversed(request.messages) if m.role == "user"), None
230
+ )
231
+
232
+ if not user_message:
233
+ raise HTTPException(
234
+ status_code=400, detail="No user message found in messages array"
235
+ )
236
+
237
+ # Debug logging: show what we're passing to the agent
238
+ if os.environ.get("GAIA_API_DEBUG") == "1":
239
+ logger.debug("🔄 EXTRACTED FOR AGENT:")
240
+ logger.debug(f"Passing to agent: {user_message[:500]}...")
241
+ if len(user_message) > 500:
242
+ logger.debug(f"(Total length: {len(user_message)} chars)")
243
+ logger.debug("=" * 80)
244
+
245
+ # Get agent instance for this model
246
+ try:
247
+ agent = registry.get_agent(request.model)
248
+ except ValueError as e:
249
+ raise HTTPException(status_code=404, detail=str(e))
250
+
251
+ # Handle streaming vs non-streaming
252
+ if request.stream:
253
+ # Debug logging for streaming mode
254
+ if os.environ.get("GAIA_API_DEBUG") == "1":
255
+ logger.debug("🌊 Using STREAMING mode")
256
+
257
+ return StreamingResponse(
258
+ create_sse_stream(
259
+ agent, user_message, request.model, workspace_root=workspace_root
260
+ ),
261
+ media_type="text/event-stream",
262
+ headers={
263
+ "Cache-Control": "no-cache",
264
+ "Connection": "keep-alive",
265
+ "X-Accel-Buffering": "no", # Disable proxy buffering
266
+ },
267
+ )
268
+ else:
269
+ # Debug logging for non-streaming mode
270
+ if os.environ.get("GAIA_API_DEBUG") == "1":
271
+ logger.debug("📦 Using NON-STREAMING mode")
272
+
273
+ # Process query synchronously with workspace root
274
+ result = agent.process_query(user_message, workspace_root=workspace_root)
275
+
276
+ # Debug logging: show what agent returned
277
+ if os.environ.get("GAIA_API_DEBUG") == "1":
278
+ logger.debug("=" * 80)
279
+ logger.debug("📤 AGENT RESPONSE (NON-STREAMING)")
280
+ logger.debug("=" * 80)
281
+ logger.debug(f"Result type: {type(result)}")
282
+ logger.debug(
283
+ f"Result keys: {list(result.keys()) if isinstance(result, dict) else 'N/A'}"
284
+ )
285
+ logger.debug(
286
+ f"Status: {result.get('status') if isinstance(result, dict) else 'N/A'}"
287
+ )
288
+ logger.debug(
289
+ f"Steps taken: {result.get('steps_taken') if isinstance(result, dict) else 'N/A'}"
290
+ )
291
+ result_preview = (
292
+ str(result.get("result", ""))[:200]
293
+ if isinstance(result, dict)
294
+ else str(result)[:200]
295
+ )
296
+ logger.debug(f"Result preview: {result_preview}...")
297
+ logger.debug("=" * 80)
298
+
299
+ # Extract content from result
300
+ content = result.get("result", str(result))
301
+
302
+ # Estimate tokens
303
+ if isinstance(agent, ApiAgent):
304
+ prompt_tokens = agent.estimate_tokens(user_message)
305
+ completion_tokens = agent.estimate_tokens(content)
306
+ else:
307
+ prompt_tokens = len(user_message) // 4
308
+ completion_tokens = len(content) // 4
309
+
310
+ return ChatCompletionResponse(
311
+ id=f"chatcmpl-{uuid.uuid4().hex[:24]}",
312
+ object="chat.completion",
313
+ created=int(time.time()),
314
+ model=request.model,
315
+ choices=[
316
+ ChatCompletionChoice(
317
+ index=0,
318
+ message=ChatCompletionResponseMessage(
319
+ role="assistant",
320
+ content=content,
321
+ ),
322
+ finish_reason="stop",
323
+ )
324
+ ],
325
+ usage=UsageInfo(
326
+ prompt_tokens=prompt_tokens,
327
+ completion_tokens=completion_tokens,
328
+ total_tokens=prompt_tokens + completion_tokens,
329
+ ),
330
+ )
331
+
332
+
333
+ async def create_sse_stream(
334
+ agent, query: str, model: str, workspace_root: str = None
335
+ ) -> AsyncGenerator[str, None]:
336
+ """
337
+ Create Server-Sent Events stream for chat completion.
338
+
339
+ This function processes the agent query in a thread pool (to avoid blocking)
340
+ and streams agent progress events in real-time via the SSEOutputHandler.
341
+
342
+ Args:
343
+ agent: Agent instance (with SSEOutputHandler)
344
+ query: User query string
345
+ model: Model ID
346
+ workspace_root: Optional workspace root path for absolute file paths
347
+
348
+ Yields:
349
+ SSE-formatted chunks with "data: " prefix
350
+
351
+ Example output:
352
+ data: {"id":"chatcmpl-123","object":"chat.completion.chunk",...}
353
+ data: {"id":"chatcmpl-123","object":"chat.completion.chunk",...}
354
+ data: [DONE]
355
+ """
356
+ # Debug logging - FIRST LINE to confirm generator starts
357
+ if os.environ.get("GAIA_API_DEBUG") == "1":
358
+ logger.debug("🎬 Generator started! Client is consuming the stream.")
359
+
360
+ completion_id = f"chatcmpl-{uuid.uuid4().hex[:24]}"
361
+ created = int(time.time())
362
+
363
+ # First chunk with role
364
+ first_chunk = {
365
+ "id": completion_id,
366
+ "object": "chat.completion.chunk",
367
+ "created": created,
368
+ "model": model,
369
+ "choices": [
370
+ {
371
+ "index": 0,
372
+ "delta": {"role": "assistant", "content": ""},
373
+ "finish_reason": None,
374
+ }
375
+ ],
376
+ }
377
+ if os.environ.get("GAIA_API_DEBUG") == "1":
378
+ logger.debug(f"📤 Sending first chunk: {json.dumps(first_chunk)}")
379
+ yield f"data: {json.dumps(first_chunk)}\n\n"
380
+
381
+ # Debug logging
382
+ if os.environ.get("GAIA_API_DEBUG") == "1":
383
+ logger.debug("🔄 Starting agent query processing in thread pool...")
384
+
385
+ # Process query in thread pool to avoid blocking event loop
386
+ loop = asyncio.get_event_loop()
387
+
388
+ # Get the SSEOutputHandler from the agent (try output_handler first, fall back to console)
389
+ output_handler = getattr(agent, "output_handler", None) or getattr(
390
+ agent, "console", None
391
+ )
392
+
393
+ try:
394
+ # Start processing in background
395
+ task = loop.run_in_executor(
396
+ None, lambda: agent.process_query(query, workspace_root=workspace_root)
397
+ )
398
+
399
+ # Stream events as they are generated
400
+ while not task.done():
401
+ # Check for new events from the output handler
402
+ if hasattr(output_handler, "has_events") and output_handler.has_events():
403
+ events = output_handler.get_events()
404
+
405
+ for event in events:
406
+ event_type = event.get("type", "message")
407
+
408
+ # Check if this event should be streamed to client
409
+ if not output_handler.should_stream_as_content(event_type):
410
+ # Still log it in debug mode
411
+ if os.environ.get("GAIA_API_DEBUG") == "1":
412
+ logger.debug(f"📝 Skipping event: {event_type}")
413
+ continue
414
+
415
+ # Format event as clean content
416
+ content_text = output_handler.format_event_as_content(event)
417
+
418
+ # Skip empty content (filtered events)
419
+ if not content_text:
420
+ continue
421
+
422
+ content_chunk = {
423
+ "id": completion_id,
424
+ "object": "chat.completion.chunk",
425
+ "created": created,
426
+ "model": model,
427
+ "choices": [
428
+ {
429
+ "index": 0,
430
+ "delta": {"content": content_text},
431
+ "finish_reason": None,
432
+ }
433
+ ],
434
+ }
435
+
436
+ if os.environ.get("GAIA_API_DEBUG") == "1":
437
+ logger.debug(
438
+ f"📤 Streaming event: {event_type} -> {content_text[:100]}"
439
+ )
440
+
441
+ yield f"data: {json.dumps(content_chunk)}\n\n"
442
+
443
+ # Small delay to avoid busy waiting
444
+ await asyncio.sleep(0.1)
445
+
446
+ # Get the final result
447
+ result = await task
448
+
449
+ # Get any remaining events
450
+ if hasattr(output_handler, "has_events") and output_handler.has_events():
451
+ events = output_handler.get_events()
452
+ for event in events:
453
+ event_type = event.get("type", "message")
454
+
455
+ # Check if this event should be streamed
456
+ if not output_handler.should_stream_as_content(event_type):
457
+ continue
458
+
459
+ # Format event as clean content
460
+ content_text = output_handler.format_event_as_content(event)
461
+
462
+ # Skip empty content
463
+ if not content_text:
464
+ continue
465
+
466
+ content_chunk = {
467
+ "id": completion_id,
468
+ "object": "chat.completion.chunk",
469
+ "created": created,
470
+ "model": model,
471
+ "choices": [
472
+ {
473
+ "index": 0,
474
+ "delta": {"content": content_text},
475
+ "finish_reason": None,
476
+ }
477
+ ],
478
+ }
479
+ yield f"data: {json.dumps(content_chunk)}\n\n"
480
+
481
+ # Debug logging: show what agent returned
482
+ if os.environ.get("GAIA_API_DEBUG") == "1":
483
+ logger.debug("=" * 80)
484
+ logger.debug("📤 AGENT RESPONSE (STREAMING)")
485
+ logger.debug("=" * 80)
486
+ logger.debug(f"Result type: {type(result)}")
487
+ logger.debug(
488
+ f"Result keys: {list(result.keys()) if isinstance(result, dict) else 'N/A'}"
489
+ )
490
+ logger.debug(
491
+ f"Status: {result.get('status') if isinstance(result, dict) else 'N/A'}"
492
+ )
493
+ logger.debug(
494
+ f"Steps taken: {result.get('steps_taken') if isinstance(result, dict) else 'N/A'}"
495
+ )
496
+ result_preview = (
497
+ str(result.get("result", ""))[:200]
498
+ if isinstance(result, dict)
499
+ else str(result)[:200]
500
+ )
501
+ logger.debug(f"Result preview: {result_preview}...")
502
+ logger.debug("=" * 80)
503
+
504
+ except Exception as e:
505
+ # Log and re-raise errors
506
+ logger.error(f"❌ Agent query processing failed: {e}", exc_info=True)
507
+ raise
508
+
509
+ # Final chunk with finish_reason
510
+ final_chunk = {
511
+ "id": completion_id,
512
+ "object": "chat.completion.chunk",
513
+ "created": created,
514
+ "model": model,
515
+ "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
516
+ }
517
+ if os.environ.get("GAIA_API_DEBUG") == "1":
518
+ logger.debug("📤 Sending final chunk with finish_reason=stop")
519
+ yield f"data: {json.dumps(final_chunk)}\n\n"
520
+
521
+ # Done marker
522
+ if os.environ.get("GAIA_API_DEBUG") == "1":
523
+ logger.debug("✅ SSE stream complete. Sending [DONE] marker.")
524
+ yield "data: [DONE]\n\n"
525
+
526
+
527
+ @app.get("/v1/models")
528
+ async def list_models() -> ModelListResponse:
529
+ """
530
+ List available models (OpenAI-compatible endpoint).
531
+
532
+ Note: These are GAIA agents exposed as "models", not LLM models.
533
+ Lemonade manages the actual LLM models underneath.
534
+
535
+ Returns:
536
+ ModelListResponse with list of available agent "models"
537
+
538
+ Example:
539
+ ```
540
+ GET /v1/models
541
+ {
542
+ "object": "list",
543
+ "data": [
544
+ {
545
+ "id": "gaia-code",
546
+ "object": "model",
547
+ "created": 1234567890,
548
+ "owned_by": "amd-gaia"
549
+ },
550
+ ...
551
+ ]
552
+ }
553
+ ```
554
+ """
555
+ return ModelListResponse(object="list", data=registry.list_models())
556
+
557
+
558
+ @app.get("/health")
559
+ async def health_check():
560
+ """
561
+ Health check endpoint.
562
+
563
+ Returns:
564
+ Status and service name
565
+
566
+ Example:
567
+ ```
568
+ GET /health
569
+ {
570
+ "status": "ok",
571
+ "service": "gaia-api"
572
+ }
573
+ ```
574
+ """
575
+ return {"status": "ok", "service": "gaia-api"}