Skip to content

Commit 7ec89b1

Browse files
committed
DevUI: Use metadata.entity_id for agent/workflow name instead of model field
1 parent cfcec83 commit 7ec89b1

20 files changed

Lines changed: 683 additions & 374 deletions

File tree

dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentRunResponseExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public static Response ToResponse(
5656
MaxOutputTokens = request.MaxOutputTokens,
5757
MaxToolCalls = request.MaxToolCalls,
5858
Metadata = request.Metadata is IReadOnlyDictionary<string, string> metadata ? new Dictionary<string, string>(metadata) : [],
59-
Model = request.Agent?.Name ?? request.Model,
59+
Model = request.Model,
6060
Output = output,
6161
ParallelToolCalls = request.ParallelToolCalls ?? true,
6262
PreviousResponseId = request.PreviousResponseId,

dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentRunResponseUpdateExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ Response CreateResponse(ResponseStatus status = ResponseStatus.Completed, IEnume
165165
MaxOutputTokens = request.MaxOutputTokens,
166166
MaxToolCalls = request.MaxToolCalls,
167167
Metadata = request.Metadata != null ? new Dictionary<string, string>(request.Metadata) : [],
168-
Model = request.Agent?.Name ?? request.Model,
168+
Model = request.Model,
169169
Output = outputs?.ToList() ?? [],
170170
ParallelToolCalls = request.ParallelToolCalls ?? true,
171171
PreviousResponseId = request.PreviousResponseId,

dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/HostedAgentResponseExecutor.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
1414

1515
/// <summary>
16-
/// Response executor that routes requests to hosted AIAgent services based on the model or agent.name parameter.
16+
/// Response executor that routes requests to hosted AIAgent services based on agent.name or metadata["entity_id"].
1717
/// This executor resolves agents from keyed services registered via AddAIAgent().
18+
/// The model field is reserved for actual model names and is never used for entity/agent identification.
1819
/// </summary>
1920
internal sealed class HostedAgentResponseExecutor : IResponseExecutor
2021
{
@@ -76,17 +77,26 @@ public async IAsyncEnumerable<StreamingResponseEvent> ExecuteAsync(
7677

7778
/// <summary>
7879
/// Resolves an agent from the service provider based on the request.
80+
/// Checks agent.name first, then metadata["entity_id"].
7981
/// </summary>
8082
/// <param name="request">The create response request.</param>
8183
/// <returns>The resolved AIAgent instance.</returns>
8284
/// <exception cref="InvalidOperationException">Thrown when the agent cannot be resolved.</exception>
8385
private AIAgent ResolveAgent(CreateResponse request)
8486
{
85-
// Extract agent name from agent.name or model parameter
86-
var agentName = request.Agent?.Name ?? request.Model;
87+
// Extract agent name from agent.name first (highest priority)
88+
string? agentName = request.Agent?.Name;
89+
90+
// Fall back to metadata["entity_id"] if agent.name is not present
91+
if (string.IsNullOrEmpty(agentName) && request.Metadata?.TryGetValue("entity_id", out string? entityId) == true)
92+
{
93+
agentName = entityId;
94+
}
95+
96+
// Never use model field for entity ID - it's for the actual model name
8797
if (string.IsNullOrEmpty(agentName))
8898
{
89-
throw new InvalidOperationException("No 'agent.name' or 'model' specified in the request.");
99+
throw new InvalidOperationException("No 'agent.name' or 'metadata.entity_id' specified in the request.");
90100
}
91101

92102
// Resolve the keyed agent service

dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/InMemoryResponsesService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ private ResponseState InitializeResponse(string responseId, CreateResponse reque
371371
MaxOutputTokens = request.MaxOutputTokens,
372372
MaxToolCalls = request.MaxToolCalls,
373373
Metadata = metadata,
374-
Model = request.Model ?? "default",
374+
Model = request.Model,
375375
Output = [],
376376
ParallelToolCalls = request.ParallelToolCalls ?? true,
377377
PreviousResponseId = request.PreviousResponseId,

dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/ResponsesHttpHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public async Task<IResult> CreateResponseAsync(
8282
}
8383
});
8484
}
85-
catch (InvalidOperationException ex) when (ex.Message.Contains("No 'agent.name' or 'model' specified"))
85+
catch (InvalidOperationException ex) when (ex.Message.Contains("No 'agent.name'"))
8686
{
8787
// Return OpenAI-style error for missing required parameters
8888
return Results.BadRequest(new ErrorResponse

dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIHttpApiIntegrationTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public async Task CreateConversationAndResponse_NonStreaming_NonBackground_Updat
4545
// Act - Create response (non-streaming, non-background)
4646
var createResponseRequest = new
4747
{
48-
model = AgentName,
48+
metadata = new { entity_id = AgentName },
4949
conversation = conversationId,
5050
input = UserMessage,
5151
stream = false
@@ -122,7 +122,7 @@ public async Task CreateConversationAndResponse_Streaming_NonBackground_UpdatesC
122122
// Act - Create response (streaming, non-background)
123123
var createResponseRequest = new
124124
{
125-
model = AgentName,
125+
metadata = new { entity_id = AgentName },
126126
conversation = conversationId,
127127
input = UserMessage,
128128
stream = true
@@ -196,7 +196,7 @@ public async Task CreateConversationAndResponse_NonStreaming_Background_UpdatesC
196196
// Act - Create response (non-streaming, background)
197197
var createResponseRequest = new
198198
{
199-
model = AgentName,
199+
metadata = new { entity_id = AgentName },
200200
conversation = conversationId,
201201
input = UserMessage,
202202
stream = false,

dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesAgentResolutionIntegrationTests.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -168,23 +168,23 @@ public async Task CreateResponse_WithMultipleAgents_ResolvesCorrectAgentAsync()
168168
}
169169

170170
/// <summary>
171-
/// Verifies that agent resolution using the model property works correctly.
171+
/// Verifies that agent resolution using the metadata.entity_id property works correctly.
172172
/// </summary>
173173
[Fact]
174-
public async Task CreateResponse_WithModelProperty_ResolvesCorrectAgentAsync()
174+
public async Task CreateResponse_WithMetadataEntityId_ResolvesCorrectAgentAsync()
175175
{
176176
// Arrange
177-
const string AgentName = "model-agent";
177+
const string AgentName = "metadata-agent";
178178
const string Instructions = "You are a helpful assistant.";
179-
const string ExpectedResponse = "Response via model property";
179+
const string ExpectedResponse = "Response via metadata.entity_id";
180180

181181
this._httpClient = await this.CreateTestServerWithAgentResolutionAsync(
182182
(AgentName, Instructions, ExpectedResponse));
183183

184-
// Act - Use raw HTTP request to control the model property
184+
// Act - Use raw HTTP request with metadata.entity_id
185185
using StringContent requestContent = new(JsonSerializer.Serialize(new
186186
{
187-
model = AgentName,
187+
metadata = new { entity_id = AgentName },
188188
input = new[]
189189
{
190190
new { type = "message", role = "user", content = "Test message" }
@@ -268,7 +268,6 @@ public async Task CreateResponse_WithoutAgentOrModel_ReturnsBadRequestAsync()
268268

269269
string responseJson = await httpResponse.Content.ReadAsStringAsync();
270270
Assert.Contains("agent.name", responseJson, StringComparison.OrdinalIgnoreCase);
271-
Assert.Contains("model", responseJson, StringComparison.OrdinalIgnoreCase);
272271
}
273272

274273
/// <summary>

python/packages/devui/README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,15 +91,15 @@ devui ./agents --tracing framework
9191

9292
## OpenAI-Compatible API
9393

94-
For convenience, DevUI provides an OpenAI Responses backend API. This means you can run the backend and also use the OpenAI client sdk to connect to it. Use **agent/workflow name as the model**, and set streaming to `True` as needed.
94+
For convenience, DevUI provides an OpenAI Responses backend API. This means you can run the backend and also use the OpenAI client sdk to connect to it. Use **agent/workflow name as the entity_id in metadata**, and set streaming to `True` as needed.
9595

9696
```bash
97-
# Simple - use your entity name as the model
97+
# Simple - use your entity name as the entity_id in metadata
9898
curl -X POST http://localhost:8080/v1/responses \
9999
-H "Content-Type: application/json" \
100100
-d @- << 'EOF'
101101
{
102-
"model": "weather_agent",
102+
"metadata": {"entity_id": "weather_agent"},
103103
"input": "Hello world"
104104
}
105105
```
@@ -115,7 +115,7 @@ client = OpenAI(
115115
)
116116
117117
response = client.responses.create(
118-
model="weather_agent", # Your agent/workflow name
118+
metadata={"entity_id": "weather_agent"}, # Your agent/workflow name
119119
input="What's the weather in Seattle?"
120120
)
121121
@@ -136,13 +136,13 @@ conversation = client.conversations.create(
136136
137137
# Use it across multiple turns
138138
response1 = client.responses.create(
139-
model="weather_agent",
139+
metadata={"entity_id": "weather_agent"},
140140
input="What's the weather in Seattle?",
141141
conversation=conversation.id
142142
)
143143
144144
response2 = client.responses.create(
145-
model="weather_agent",
145+
metadata={"entity_id": "weather_agent"},
146146
input="How about tomorrow?",
147147
conversation=conversation.id # Continues the conversation!
148148
)

python/packages/devui/agent_framework_devui/_mapper.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ async def aggregate_to_response(self, events: Sequence[Any], request: AgentFrame
273273
id=f"resp_{uuid.uuid4().hex[:12]}",
274274
object="response",
275275
created_at=datetime.now().timestamp(),
276-
model=request.model,
276+
model=request.model or "devui",
277277
output=[response_output_message],
278278
usage=usage,
279279
parallel_tool_calls=False,
@@ -495,8 +495,9 @@ async def _convert_agent_lifecycle_event(self, event: Any, context: dict[str, An
495495
from .models._openai_custom import AgentCompletedEvent, AgentFailedEvent, AgentStartedEvent
496496

497497
try:
498-
# Get model name from context (the agent name)
499-
model_name = context.get("request", {}).model if context.get("request") else "agent"
498+
# Get model name from request or use 'devui' as default
499+
request_obj = context.get("request")
500+
model_name = request_obj.model if request_obj and request_obj.model else "devui"
500501

501502
if isinstance(event, AgentStartedEvent):
502503
execution_id = f"agent_{uuid4().hex[:12]}"
@@ -603,16 +604,16 @@ async def _convert_workflow_event(self, event: Any, context: dict[str, Any]) ->
603604
# Return proper OpenAI event objects
604605
events: list[Any] = []
605606

606-
# Determine the model name - use request model or default to "workflow"
607-
# The request model will be the agent name for agents, workflow name for workflows
608-
model_name = context.get("request", {}).model if context.get("request") else "workflow"
607+
# Get model name from request or use 'devui' as default
608+
request_obj = context.get("request")
609+
model_name = request_obj.model if request_obj and request_obj.model else "devui"
609610

610611
# Create a full Response object with all required fields
611612
response_obj = Response(
612613
id=f"resp_{workflow_id}",
613614
object="response",
614615
created_at=float(time.time()),
615-
model=model_name, # Use the actual model/agent name
616+
model=model_name,
616617
output=[], # Empty output list initially
617618
status="in_progress",
618619
# Required fields with safe defaults
@@ -643,8 +644,9 @@ async def _convert_workflow_event(self, event: Any, context: dict[str, Any]) ->
643644
# Import Response type for proper construction
644645
from openai.types.responses import Response
645646

646-
# Get model name from context
647-
model_name = context.get("request", {}).model if context.get("request") else "workflow"
647+
# Get model name from request or use 'devui' as default
648+
request_obj = context.get("request")
649+
model_name = request_obj.model if request_obj and request_obj.model else "devui"
648650

649651
# Create a full Response object for completed state
650652
response_obj = Response(
@@ -672,8 +674,9 @@ async def _convert_workflow_event(self, event: Any, context: dict[str, Any]) ->
672674
# Import Response and ResponseError types
673675
from openai.types.responses import Response, ResponseError
674676

675-
# Get model name from context
676-
model_name = context.get("request", {}).model if context.get("request") else "workflow"
677+
# Get model name from request or use 'devui' as default
678+
request_obj = context.get("request")
679+
model_name = request_obj.model if request_obj and request_obj.model else "devui"
677680

678681
# Create error object
679682
error_message = str(error_info) if error_info else "Unknown error"
@@ -1208,7 +1211,7 @@ async def _create_error_response(self, error_message: str, request: AgentFramewo
12081211
id=f"resp_{uuid.uuid4().hex[:12]}",
12091212
object="response",
12101213
created_at=datetime.now().timestamp(),
1211-
model=request.model,
1214+
model=request.model or "devui",
12121215
output=[response_output_message],
12131216
usage=usage,
12141217
parallel_tool_calls=False,

python/packages/devui/agent_framework_devui/_server.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -359,14 +359,14 @@ async def create_response(request: AgentFrameworkRequest, raw_request: Request)
359359
try:
360360
raw_body = await raw_request.body()
361361
logger.info(f"Raw request body: {raw_body.decode()}")
362-
logger.info(f"Parsed request: model={request.model}, extra_body={request.extra_body}")
362+
logger.info(f"Parsed request: metadata={request.metadata}")
363363

364-
# Get entity_id using the new method
364+
# Get entity_id from metadata
365365
entity_id = request.get_entity_id()
366366
logger.info(f"Extracted entity_id: {entity_id}")
367367

368368
if not entity_id:
369-
error = OpenAIError.create(f"Missing entity_id. Request extra_body: {request.extra_body}")
369+
error = OpenAIError.create("Missing entity_id in metadata. Provide metadata.entity_id in request.")
370370
return JSONResponse(status_code=400, content=error.to_dict())
371371

372372
# Get executor and validate entity exists

0 commit comments

Comments
 (0)