diff --git a/docs/_static/llama-stack-spec.html b/docs/_static/llama-stack-spec.html
index 4020dc4cd..00d762316 100644
--- a/docs/_static/llama-stack-spec.html
+++ b/docs/_static/llama-stack-spec.html
@@ -6466,54 +6466,15 @@
],
"title": "AgentTurnResponseTurnStartPayload"
},
- "OpenAIResponseInputMessage": {
- "type": "object",
- "properties": {
- "content": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/OpenAIResponseInputMessageContent"
- }
- }
- ]
+ "OpenAIResponseInput": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/OpenAIResponseOutputMessageWebSearchToolCall"
},
- "role": {
- "oneOf": [
- {
- "type": "string",
- "const": "system"
- },
- {
- "type": "string",
- "const": "developer"
- },
- {
- "type": "string",
- "const": "user"
- },
- {
- "type": "string",
- "const": "assistant"
- }
- ]
- },
- "type": {
- "type": "string",
- "const": "message",
- "default": "message"
+ {
+ "$ref": "#/components/schemas/OpenAIResponseMessage"
}
- },
- "additionalProperties": false,
- "required": [
- "content",
- "role"
- ],
- "title": "OpenAIResponseInputMessage"
+ ]
},
"OpenAIResponseInputMessageContent": {
"oneOf": [
@@ -6614,6 +6575,111 @@
],
"title": "OpenAIResponseInputToolWebSearch"
},
+ "OpenAIResponseMessage": {
+ "type": "object",
+ "properties": {
+ "content": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/OpenAIResponseInputMessageContent"
+ }
+ },
+ {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/OpenAIResponseOutputMessageContent"
+ }
+ }
+ ]
+ },
+ "role": {
+ "oneOf": [
+ {
+ "type": "string",
+ "const": "system"
+ },
+ {
+ "type": "string",
+ "const": "developer"
+ },
+ {
+ "type": "string",
+ "const": "user"
+ },
+ {
+ "type": "string",
+ "const": "assistant"
+ }
+ ]
+ },
+ "type": {
+ "type": "string",
+ "const": "message",
+ "default": "message"
+ },
+ "id": {
+ "type": "string"
+ },
+ "status": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "content",
+ "role",
+ "type"
+ ],
+ "title": "OpenAIResponseMessage",
+ "description": "Corresponds to the various Message types in the Responses API. They are all under one type because the Responses API gives them all the same \"type\" value, and there is no way to tell them apart in certain scenarios."
+ },
+ "OpenAIResponseOutputMessageContent": {
+ "type": "object",
+ "properties": {
+ "text": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "const": "output_text",
+ "default": "output_text"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "text",
+ "type"
+ ],
+ "title": "OpenAIResponseOutputMessageContentOutputText"
+ },
+ "OpenAIResponseOutputMessageWebSearchToolCall": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "status": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "const": "web_search_call",
+ "default": "web_search_call"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "status",
+ "type"
+ ],
+ "title": "OpenAIResponseOutputMessageWebSearchToolCall"
+ },
"CreateOpenaiResponseRequest": {
"type": "object",
"properties": {
@@ -6625,7 +6691,7 @@
{
"type": "array",
"items": {
- "$ref": "#/components/schemas/OpenAIResponseInputMessage"
+ "$ref": "#/components/schemas/OpenAIResponseInput"
}
}
],
@@ -6743,7 +6809,7 @@
"OpenAIResponseOutput": {
"oneOf": [
{
- "$ref": "#/components/schemas/OpenAIResponseOutputMessage"
+ "$ref": "#/components/schemas/OpenAIResponseMessage"
},
{
"$ref": "#/components/schemas/OpenAIResponseOutputMessageWebSearchToolCall"
@@ -6752,89 +6818,11 @@
"discriminator": {
"propertyName": "type",
"mapping": {
- "message": "#/components/schemas/OpenAIResponseOutputMessage",
+ "message": "#/components/schemas/OpenAIResponseMessage",
"web_search_call": "#/components/schemas/OpenAIResponseOutputMessageWebSearchToolCall"
}
}
},
- "OpenAIResponseOutputMessage": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "content": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/OpenAIResponseOutputMessageContent"
- }
- },
- "role": {
- "type": "string",
- "const": "assistant",
- "default": "assistant"
- },
- "status": {
- "type": "string"
- },
- "type": {
- "type": "string",
- "const": "message",
- "default": "message"
- }
- },
- "additionalProperties": false,
- "required": [
- "id",
- "content",
- "role",
- "status",
- "type"
- ],
- "title": "OpenAIResponseOutputMessage"
- },
- "OpenAIResponseOutputMessageContent": {
- "type": "object",
- "properties": {
- "text": {
- "type": "string"
- },
- "type": {
- "type": "string",
- "const": "output_text",
- "default": "output_text"
- }
- },
- "additionalProperties": false,
- "required": [
- "text",
- "type"
- ],
- "title": "OpenAIResponseOutputMessageContentOutputText"
- },
- "OpenAIResponseOutputMessageWebSearchToolCall": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "status": {
- "type": "string"
- },
- "type": {
- "type": "string",
- "const": "web_search_call",
- "default": "web_search_call"
- }
- },
- "additionalProperties": false,
- "required": [
- "id",
- "status",
- "type"
- ],
- "title": "OpenAIResponseOutputMessageWebSearchToolCall"
- },
"OpenAIResponseObjectStream": {
"oneOf": [
{
diff --git a/docs/_static/llama-stack-spec.yaml b/docs/_static/llama-stack-spec.yaml
index 62e3ca85c..05e642662 100644
--- a/docs/_static/llama-stack-spec.yaml
+++ b/docs/_static/llama-stack-spec.yaml
@@ -4534,34 +4534,10 @@ components:
- event_type
- turn_id
title: AgentTurnResponseTurnStartPayload
- OpenAIResponseInputMessage:
- type: object
- properties:
- content:
- oneOf:
- - type: string
- - type: array
- items:
- $ref: '#/components/schemas/OpenAIResponseInputMessageContent'
- role:
- oneOf:
- - type: string
- const: system
- - type: string
- const: developer
- - type: string
- const: user
- - type: string
- const: assistant
- type:
- type: string
- const: message
- default: message
- additionalProperties: false
- required:
- - content
- - role
- title: OpenAIResponseInputMessage
+ OpenAIResponseInput:
+ oneOf:
+ - $ref: '#/components/schemas/OpenAIResponseOutputMessageWebSearchToolCall'
+ - $ref: '#/components/schemas/OpenAIResponseMessage'
OpenAIResponseInputMessageContent:
oneOf:
- $ref: '#/components/schemas/OpenAIResponseInputMessageContentText'
@@ -4625,6 +4601,79 @@ components:
required:
- type
title: OpenAIResponseInputToolWebSearch
+ OpenAIResponseMessage:
+ type: object
+ properties:
+ content:
+ oneOf:
+ - type: string
+ - type: array
+ items:
+ $ref: '#/components/schemas/OpenAIResponseInputMessageContent'
+ - type: array
+ items:
+ $ref: '#/components/schemas/OpenAIResponseOutputMessageContent'
+ role:
+ oneOf:
+ - type: string
+ const: system
+ - type: string
+ const: developer
+ - type: string
+ const: user
+ - type: string
+ const: assistant
+ type:
+ type: string
+ const: message
+ default: message
+ id:
+ type: string
+ status:
+ type: string
+ additionalProperties: false
+ required:
+ - content
+ - role
+ - type
+ title: OpenAIResponseMessage
+ description: >-
+ Corresponds to the various Message types in the Responses API. They are all
+ under one type because the Responses API gives them all the same "type" value,
+ and there is no way to tell them apart in certain scenarios.
+ OpenAIResponseOutputMessageContent:
+ type: object
+ properties:
+ text:
+ type: string
+ type:
+ type: string
+ const: output_text
+ default: output_text
+ additionalProperties: false
+ required:
+ - text
+ - type
+ title: >-
+ OpenAIResponseOutputMessageContentOutputText
+ "OpenAIResponseOutputMessageWebSearchToolCall":
+ type: object
+ properties:
+ id:
+ type: string
+ status:
+ type: string
+ type:
+ type: string
+ const: web_search_call
+ default: web_search_call
+ additionalProperties: false
+ required:
+ - id
+ - status
+ - type
+ title: >-
+ OpenAIResponseOutputMessageWebSearchToolCall
CreateOpenaiResponseRequest:
type: object
properties:
@@ -4633,7 +4682,7 @@ components:
- type: string
- type: array
items:
- $ref: '#/components/schemas/OpenAIResponseInputMessage'
+ $ref: '#/components/schemas/OpenAIResponseInput'
description: Input message(s) to create the response.
model:
type: string
@@ -4717,73 +4766,13 @@ components:
title: OpenAIResponseObject
OpenAIResponseOutput:
oneOf:
- - $ref: '#/components/schemas/OpenAIResponseOutputMessage'
+ - $ref: '#/components/schemas/OpenAIResponseMessage'
- $ref: '#/components/schemas/OpenAIResponseOutputMessageWebSearchToolCall'
discriminator:
propertyName: type
mapping:
- message: '#/components/schemas/OpenAIResponseOutputMessage'
+ message: '#/components/schemas/OpenAIResponseMessage'
web_search_call: '#/components/schemas/OpenAIResponseOutputMessageWebSearchToolCall'
- OpenAIResponseOutputMessage:
- type: object
- properties:
- id:
- type: string
- content:
- type: array
- items:
- $ref: '#/components/schemas/OpenAIResponseOutputMessageContent'
- role:
- type: string
- const: assistant
- default: assistant
- status:
- type: string
- type:
- type: string
- const: message
- default: message
- additionalProperties: false
- required:
- - id
- - content
- - role
- - status
- - type
- title: OpenAIResponseOutputMessage
- OpenAIResponseOutputMessageContent:
- type: object
- properties:
- text:
- type: string
- type:
- type: string
- const: output_text
- default: output_text
- additionalProperties: false
- required:
- - text
- - type
- title: >-
- OpenAIResponseOutputMessageContentOutputText
- "OpenAIResponseOutputMessageWebSearchToolCall":
- type: object
- properties:
- id:
- type: string
- status:
- type: string
- type:
- type: string
- const: web_search_call
- default: web_search_call
- additionalProperties: false
- required:
- - id
- - status
- - type
- title: >-
- OpenAIResponseOutputMessageWebSearchToolCall
OpenAIResponseObjectStream:
oneOf:
- $ref: '#/components/schemas/OpenAIResponseObjectStreamResponseCreated'
diff --git a/llama_stack/apis/agents/agents.py b/llama_stack/apis/agents/agents.py
index 91e57dbbe..00d036f9f 100644
--- a/llama_stack/apis/agents/agents.py
+++ b/llama_stack/apis/agents/agents.py
@@ -31,7 +31,7 @@ from llama_stack.apis.tools import ToolDef
from llama_stack.schema_utils import json_schema_type, register_schema, webmethod
from .openai_responses import (
- OpenAIResponseInputMessage,
+ OpenAIResponseInput,
OpenAIResponseInputTool,
OpenAIResponseObject,
OpenAIResponseObjectStream,
@@ -592,7 +592,7 @@ class Agents(Protocol):
@webmethod(route="/openai/v1/responses", method="POST")
async def create_openai_response(
self,
- input: str | list[OpenAIResponseInputMessage],
+ input: str | list[OpenAIResponseInput],
model: str,
previous_response_id: str | None = None,
store: bool | None = True,
diff --git a/llama_stack/apis/agents/openai_responses.py b/llama_stack/apis/agents/openai_responses.py
index 6a300e490..ddceb1531 100644
--- a/llama_stack/apis/agents/openai_responses.py
+++ b/llama_stack/apis/agents/openai_responses.py
@@ -17,6 +17,28 @@ class OpenAIResponseError(BaseModel):
message: str
+@json_schema_type
+class OpenAIResponseInputMessageContentText(BaseModel):
+ text: str
+ type: Literal["input_text"] = "input_text"
+
+
+@json_schema_type
+class OpenAIResponseInputMessageContentImage(BaseModel):
+ detail: Literal["low"] | Literal["high"] | Literal["auto"] = "auto"
+ type: Literal["input_image"] = "input_image"
+ # TODO: handle file_id
+ image_url: str | None = None
+
+
+# TODO: handle file content types
+OpenAIResponseInputMessageContent = Annotated[
+ OpenAIResponseInputMessageContentText | OpenAIResponseInputMessageContentImage,
+ Field(discriminator="type"),
+]
+register_schema(OpenAIResponseInputMessageContent, name="OpenAIResponseInputMessageContent")
+
+
@json_schema_type
class OpenAIResponseOutputMessageContentOutputText(BaseModel):
text: str
@@ -31,13 +53,22 @@ register_schema(OpenAIResponseOutputMessageContent, name="OpenAIResponseOutputMe
@json_schema_type
-class OpenAIResponseOutputMessage(BaseModel):
- id: str
- content: list[OpenAIResponseOutputMessageContent]
- role: Literal["assistant"] = "assistant"
- status: str
+class OpenAIResponseMessage(BaseModel):
+ """
+ Corresponds to the various Message types in the Responses API.
+ They are all under one type because the Responses API gives them all
+ the same "type" value, and there is no way to tell them apart in certain
+ scenarios.
+ """
+
+ content: str | list[OpenAIResponseInputMessageContent] | list[OpenAIResponseOutputMessageContent]
+ role: Literal["system"] | Literal["developer"] | Literal["user"] | Literal["assistant"]
type: Literal["message"] = "message"
+ # The fields below are not used in all scenarios, but are required in others.
+ id: str | None = None
+ status: str | None = None
+
@json_schema_type
class OpenAIResponseOutputMessageWebSearchToolCall(BaseModel):
@@ -47,7 +78,7 @@ class OpenAIResponseOutputMessageWebSearchToolCall(BaseModel):
OpenAIResponseOutput = Annotated[
- OpenAIResponseOutputMessage | OpenAIResponseOutputMessageWebSearchToolCall,
+ OpenAIResponseMessage | OpenAIResponseOutputMessageWebSearchToolCall,
Field(discriminator="type"),
]
register_schema(OpenAIResponseOutput, name="OpenAIResponseOutput")
@@ -89,33 +120,15 @@ OpenAIResponseObjectStream = Annotated[
register_schema(OpenAIResponseObjectStream, name="OpenAIResponseObjectStream")
-@json_schema_type
-class OpenAIResponseInputMessageContentText(BaseModel):
- text: str
- type: Literal["input_text"] = "input_text"
-
-
-@json_schema_type
-class OpenAIResponseInputMessageContentImage(BaseModel):
- detail: Literal["low"] | Literal["high"] | Literal["auto"] = "auto"
- type: Literal["input_image"] = "input_image"
- # TODO: handle file_id
- image_url: str | None = None
-
-
-# TODO: handle file content types
-OpenAIResponseInputMessageContent = Annotated[
- OpenAIResponseInputMessageContentText | OpenAIResponseInputMessageContentImage,
- Field(discriminator="type"),
+OpenAIResponseInput = Annotated[
+ # Responses API allows output messages to be passed in as input
+ OpenAIResponseOutputMessageWebSearchToolCall
+ |
+ # Fallback to the generic message type as a last resort
+ OpenAIResponseMessage,
+ Field(union_mode="left_to_right"),
]
-register_schema(OpenAIResponseInputMessageContent, name="OpenAIResponseInputMessageContent")
-
-
-@json_schema_type
-class OpenAIResponseInputMessage(BaseModel):
- content: str | list[OpenAIResponseInputMessageContent]
- role: Literal["system"] | Literal["developer"] | Literal["user"] | Literal["assistant"]
- type: Literal["message"] | None = "message"
+register_schema(OpenAIResponseInput, name="OpenAIResponseInput")
@json_schema_type
@@ -133,18 +146,11 @@ OpenAIResponseInputTool = Annotated[
register_schema(OpenAIResponseInputTool, name="OpenAIResponseInputTool")
-@json_schema_type
-class OpenAIResponseInputItemMessage(OpenAIResponseInputMessage):
- id: str
-
-
-@json_schema_type
class OpenAIResponseInputItemList(BaseModel):
- data: list[OpenAIResponseInputItemMessage]
+ data: list[OpenAIResponseInput]
object: Literal["list"] = "list"
-@json_schema_type
class OpenAIResponsePreviousResponseWithInputItems(BaseModel):
input_items: OpenAIResponseInputItemList
response: OpenAIResponseObject
diff --git a/llama_stack/providers/inline/agents/meta_reference/agents.py b/llama_stack/providers/inline/agents/meta_reference/agents.py
index 19d60c816..244af8f03 100644
--- a/llama_stack/providers/inline/agents/meta_reference/agents.py
+++ b/llama_stack/providers/inline/agents/meta_reference/agents.py
@@ -20,7 +20,7 @@ from llama_stack.apis.agents import (
AgentTurnCreateRequest,
AgentTurnResumeRequest,
Document,
- OpenAIResponseInputMessage,
+ OpenAIResponseInput,
OpenAIResponseInputTool,
OpenAIResponseObject,
Session,
@@ -311,7 +311,7 @@ class MetaReferenceAgentsImpl(Agents):
async def create_openai_response(
self,
- input: str | list[OpenAIResponseInputMessage],
+ input: str | list[OpenAIResponseInput],
model: str,
previous_response_id: str | None = None,
store: bool | None = True,
diff --git a/llama_stack/providers/inline/agents/meta_reference/openai_responses.py b/llama_stack/providers/inline/agents/meta_reference/openai_responses.py
index 6be6513fb..148ac474b 100644
--- a/llama_stack/providers/inline/agents/meta_reference/openai_responses.py
+++ b/llama_stack/providers/inline/agents/meta_reference/openai_responses.py
@@ -12,19 +12,17 @@ from typing import cast
from openai.types.chat import ChatCompletionToolParam
from llama_stack.apis.agents.openai_responses import (
+ OpenAIResponseInput,
OpenAIResponseInputItemList,
- OpenAIResponseInputItemMessage,
- OpenAIResponseInputMessage,
- OpenAIResponseInputMessageContent,
OpenAIResponseInputMessageContentImage,
OpenAIResponseInputMessageContentText,
OpenAIResponseInputTool,
+ OpenAIResponseMessage,
OpenAIResponseObject,
OpenAIResponseObjectStream,
OpenAIResponseObjectStreamResponseCompleted,
OpenAIResponseObjectStreamResponseCreated,
OpenAIResponseOutput,
- OpenAIResponseOutputMessage,
OpenAIResponseOutputMessageContentOutputText,
OpenAIResponseOutputMessageWebSearchToolCall,
OpenAIResponsePreviousResponseWithInputItems,
@@ -56,62 +54,38 @@ logger = get_logger(name=__name__, category="openai_responses")
OPENAI_RESPONSES_PREFIX = "openai_responses:"
-async def _convert_response_input_content_to_chat_content_parts(
- input_content: list[OpenAIResponseInputMessageContent],
-) -> list[OpenAIChatCompletionContentPartParam]:
- """
- Convert a list of input content items to a list of chat completion content parts
- """
- content_parts = []
- for input_content_part in input_content:
- if isinstance(input_content_part, OpenAIResponseInputMessageContentText):
- content_parts.append(OpenAIChatCompletionContentPartTextParam(text=input_content_part.text))
- elif isinstance(input_content_part, OpenAIResponseInputMessageContentImage):
- if input_content_part.image_url:
- image_url = OpenAIImageURL(url=input_content_part.image_url, detail=input_content_part.detail)
- content_parts.append(OpenAIChatCompletionContentPartImageParam(image_url=image_url))
- return content_parts
-
-
-async def _convert_response_input_to_chat_user_content(
- input: str | list[OpenAIResponseInputMessage],
-) -> str | list[OpenAIChatCompletionContentPartParam]:
- user_content: str | list[OpenAIChatCompletionContentPartParam] = ""
- if isinstance(input, list):
- user_content = []
- for user_input in input:
- if isinstance(user_input.content, list):
- user_content.extend(await _convert_response_input_content_to_chat_content_parts(user_input.content))
- else:
- user_content.append(OpenAIChatCompletionContentPartTextParam(text=user_input.content))
- else:
- user_content = input
- return user_content
-
-
-async def _previous_response_to_messages(
- previous_response: OpenAIResponsePreviousResponseWithInputItems,
+async def _convert_response_input_to_chat_messages(
+ input: str | list[OpenAIResponseInput],
) -> list[OpenAIMessageParam]:
messages: list[OpenAIMessageParam] = []
- for previous_message in previous_response.input_items.data:
- previous_content = await _convert_response_input_content_to_chat_content_parts(previous_message.content)
- if previous_message.role == "user":
- converted_message = OpenAIUserMessageParam(content=previous_content)
- elif previous_message.role == "assistant":
- converted_message = OpenAIAssistantMessageParam(content=previous_content)
- else:
- # TODO: handle other message roles? unclear if system/developer roles are
- # used in previous responses
- continue
- messages.append(converted_message)
-
- for output_message in previous_response.response.output:
- if isinstance(output_message, OpenAIResponseOutputMessage):
- messages.append(OpenAIAssistantMessageParam(content=output_message.content[0].text))
+ content: str | list[OpenAIChatCompletionContentPartParam] = ""
+ if isinstance(input, list):
+ for input_message in input:
+ if isinstance(input_message.content, list):
+ content = []
+ for input_message_content in input_message.content:
+ if isinstance(input_message_content, OpenAIResponseInputMessageContentText):
+ content.append(OpenAIChatCompletionContentPartTextParam(text=input_message_content.text))
+ elif isinstance(input_message_content, OpenAIResponseInputMessageContentImage):
+ if input_message_content.image_url:
+ image_url = OpenAIImageURL(
+ url=input_message_content.image_url, detail=input_message_content.detail
+ )
+ content.append(OpenAIChatCompletionContentPartImageParam(image_url=image_url))
+ else:
+ content = input_message.content
+ message_type = await _get_message_type_by_role(input_message.role)
+ if message_type is None:
+ raise ValueError(
+ f"Llama Stack OpenAI Responses does not yet support message role '{input_message.role}' in this context"
+ )
+ messages.append(message_type(content=content))
+ else:
+ messages.append(OpenAIUserMessageParam(content=input))
return messages
-async def _openai_choices_to_output_messages(choices: list[OpenAIChoice]) -> list[OpenAIResponseOutputMessage]:
+async def _openai_choices_to_output_messages(choices: list[OpenAIChoice]) -> list[OpenAIResponseMessage]:
output_messages = []
for choice in choices:
output_content = ""
@@ -121,10 +95,11 @@ async def _openai_choices_to_output_messages(choices: list[OpenAIChoice]) -> lis
output_content = choice.message.content.text
# TODO: handle image content
output_messages.append(
- OpenAIResponseOutputMessage(
+ OpenAIResponseMessage(
id=f"msg_{uuid.uuid4()}",
content=[OpenAIResponseOutputMessageContentOutputText(text=output_content)],
status="completed",
+ role="assistant",
)
)
return output_messages
@@ -160,6 +135,27 @@ class OpenAIResponsesImpl:
raise ValueError(f"OpenAI response with id '{id}' not found")
return OpenAIResponsePreviousResponseWithInputItems.model_validate_json(response_json)
+ async def _prepend_previous_response(
+ self, input: str | list[OpenAIResponseInput], previous_response_id: str | None = None
+ ):
+ if previous_response_id:
+ previous_response_with_input = await self._get_previous_response_with_input(previous_response_id)
+
+ # previous response input items
+ new_input_items = previous_response_with_input.input_items.data
+
+ # previous response output items
+ new_input_items.extend(previous_response_with_input.response.output)
+
+ # new input items from the current request
+ if isinstance(input, str):
+ # Normalize input to a list of OpenAIResponseInputMessage objects
+ input = [OpenAIResponseMessage(content=input, role="user")]
+ new_input_items.extend(input)
+ input = new_input_items
+
+ return input
+
async def get_openai_response(
self,
id: str,
@@ -169,7 +165,7 @@ class OpenAIResponsesImpl:
async def create_openai_response(
self,
- input: str | list[OpenAIResponseInputMessage],
+ input: str | list[OpenAIResponseInput],
model: str,
previous_response_id: str | None = None,
store: bool | None = True,
@@ -179,37 +175,8 @@ class OpenAIResponsesImpl:
):
stream = False if stream is None else stream
- messages: list[OpenAIMessageParam] = []
- if previous_response_id:
- previous_response_with_input = await self._get_previous_response_with_input(previous_response_id)
- messages.extend(await _previous_response_to_messages(previous_response_with_input))
-
- # TODO: refactor this user_content parsing out into a separate method
- content: str | list[OpenAIChatCompletionContentPartParam] = ""
- if isinstance(input, list):
- for input_message in input:
- if isinstance(input_message.content, list):
- content = []
- for input_message_content in input_message.content:
- if isinstance(input_message_content, OpenAIResponseInputMessageContentText):
- content.append(OpenAIChatCompletionContentPartTextParam(text=input_message_content.text))
- elif isinstance(input_message_content, OpenAIResponseInputMessageContentImage):
- if input_message_content.image_url:
- image_url = OpenAIImageURL(
- url=input_message_content.image_url, detail=input_message_content.detail
- )
- content.append(OpenAIChatCompletionContentPartImageParam(image_url=image_url))
- else:
- content = input_message.content
- message_type = await _get_message_type_by_role(input_message.role)
- if message_type is None:
- raise ValueError(
- f"Llama Stack OpenAI Responses does not yet support message role '{input_message.role}' in this context"
- )
- messages.append(message_type(content=content))
- else:
- messages.append(OpenAIUserMessageParam(content=input))
-
+ input = await self._prepend_previous_response(input, previous_response_id)
+ messages = await _convert_response_input_to_chat_messages(input)
chat_tools = await self._convert_response_tools_to_chat_tools(tools) if tools else None
chat_response = await self.inference_api.openai_chat_completion(
model=model,
@@ -272,22 +239,29 @@ class OpenAIResponsesImpl:
if store:
# Store in kvstore
+ new_input_id = f"msg_{uuid.uuid4()}"
if isinstance(input, str):
# synthesize a message from the input string
input_content = OpenAIResponseInputMessageContentText(text=input)
- input_content_item = OpenAIResponseInputItemMessage(
+ input_content_item = OpenAIResponseMessage(
role="user",
content=[input_content],
- id=f"msg_{uuid.uuid4()}",
+ id=new_input_id,
)
input_items_data = [input_content_item]
else:
# we already have a list of messages
input_items_data = []
for input_item in input:
- input_items_data.append(
- OpenAIResponseInputItemMessage(id=f"msg_{uuid.uuid4()}", **input_item.model_dump())
- )
+ if isinstance(input_item, OpenAIResponseMessage):
+ # These may or may not already have an id, so dump to dict, check for id, and add if missing
+ input_item_dict = input_item.model_dump()
+ if "id" not in input_item_dict:
+ input_item_dict["id"] = new_input_id
+ input_items_data.append(OpenAIResponseMessage(**input_item_dict))
+ else:
+ input_items_data.append(input_item)
+
input_items = OpenAIResponseInputItemList(data=input_items_data)
prev_response = OpenAIResponsePreviousResponseWithInputItems(
input_items=input_items,
diff --git a/tests/unit/providers/agents/meta_reference/test_openai_responses.py b/tests/unit/providers/agents/meta_reference/test_openai_responses.py
index e0acac643..6cba7401e 100644
--- a/tests/unit/providers/agents/meta_reference/test_openai_responses.py
+++ b/tests/unit/providers/agents/meta_reference/test_openai_responses.py
@@ -4,15 +4,19 @@
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.
-from unittest.mock import AsyncMock
+from unittest.mock import AsyncMock, patch
import pytest
from llama_stack.apis.agents.openai_responses import (
- OpenAIResponseInputMessage,
+ OpenAIResponseInputItemList,
OpenAIResponseInputMessageContentText,
OpenAIResponseInputToolWebSearch,
- OpenAIResponseOutputMessage,
+ OpenAIResponseMessage,
+ OpenAIResponseObject,
+ OpenAIResponseOutputMessageContentOutputText,
+ OpenAIResponseOutputMessageWebSearchToolCall,
+ OpenAIResponsePreviousResponseWithInputItems,
)
from llama_stack.apis.inference.inference import (
OpenAIAssistantMessageParam,
@@ -91,7 +95,7 @@ async def test_create_openai_response_with_string_input(openai_responses_impl, m
openai_responses_impl.persistence_store.set.assert_called_once()
assert result.model == model
assert len(result.output) == 1
- assert isinstance(result.output[0], OpenAIResponseOutputMessage)
+ assert isinstance(result.output[0], OpenAIResponseMessage)
assert result.output[0].content[0].text == "Dublin"
@@ -159,7 +163,7 @@ async def test_create_openai_response_with_string_input_with_tools(openai_respon
# Check that we got the content from our mocked tool execution result
assert len(result.output) >= 1
- assert isinstance(result.output[1], OpenAIResponseOutputMessage)
+ assert isinstance(result.output[1], OpenAIResponseMessage)
assert result.output[1].content[0].text == "Dublin"
@@ -168,9 +172,9 @@ async def test_create_openai_response_with_multiple_messages(openai_responses_im
"""Test creating an OpenAI response with multiple messages."""
# Setup
input_messages = [
- OpenAIResponseInputMessage(role="developer", content="You are a helpful assistant", name=None),
- OpenAIResponseInputMessage(role="user", content="Name some towns in Ireland", name=None),
- OpenAIResponseInputMessage(
+ OpenAIResponseMessage(role="developer", content="You are a helpful assistant", name=None),
+ OpenAIResponseMessage(role="user", content="Name some towns in Ireland", name=None),
+ OpenAIResponseMessage(
role="assistant",
content=[
OpenAIResponseInputMessageContentText(text="Galway, Longford, Sligo"),
@@ -178,7 +182,7 @@ async def test_create_openai_response_with_multiple_messages(openai_responses_im
],
name=None,
),
- OpenAIResponseInputMessage(role="user", content="Which is the largest town in Ireland?", name=None),
+ OpenAIResponseMessage(role="user", content="Which is the largest town in Ireland?", name=None),
]
model = "meta-llama/Llama-3.1-8B-Instruct"
@@ -207,3 +211,106 @@ async def test_create_openai_response_with_multiple_messages(openai_responses_im
assert isinstance(inference_messages[i], OpenAIAssistantMessageParam)
else:
assert isinstance(inference_messages[i], OpenAIDeveloperMessageParam)
+
+
+@pytest.mark.asyncio
+async def test_prepend_previous_response_none(openai_responses_impl):
+ """Test prepending no previous response to a new response."""
+
+ input = await openai_responses_impl._prepend_previous_response("fake_input", None)
+ assert input == "fake_input"
+
+
+@pytest.mark.asyncio
+@patch.object(OpenAIResponsesImpl, "_get_previous_response_with_input")
+async def test_prepend_previous_response_basic(get_previous_response_with_input, openai_responses_impl):
+ """Test prepending a basic previous response to a new response."""
+
+ input_item_message = OpenAIResponseMessage(
+ id="123",
+ content=[OpenAIResponseInputMessageContentText(text="fake_previous_input")],
+ role="user",
+ )
+ input_items = OpenAIResponseInputItemList(data=[input_item_message])
+ response_output_message = OpenAIResponseMessage(
+ id="123",
+ content=[OpenAIResponseOutputMessageContentOutputText(text="fake_response")],
+ status="completed",
+ role="assistant",
+ )
+ response = OpenAIResponseObject(
+ created_at=1,
+ id="resp_123",
+ model="fake_model",
+ output=[response_output_message],
+ status="completed",
+ )
+ previous_response = OpenAIResponsePreviousResponseWithInputItems(
+ input_items=input_items,
+ response=response,
+ )
+ get_previous_response_with_input.return_value = previous_response
+
+ input = await openai_responses_impl._prepend_previous_response("fake_input", "resp_123")
+
+ assert len(input) == 3
+ # Check for previous input
+ assert isinstance(input[0], OpenAIResponseMessage)
+ assert input[0].content[0].text == "fake_previous_input"
+ # Check for previous output
+ assert isinstance(input[1], OpenAIResponseMessage)
+ assert input[1].content[0].text == "fake_response"
+ # Check for new input
+ assert isinstance(input[2], OpenAIResponseMessage)
+ assert input[2].content == "fake_input"
+
+
+@pytest.mark.asyncio
+@patch.object(OpenAIResponsesImpl, "_get_previous_response_with_input")
+async def test_prepend_previous_response_web_search(get_previous_response_with_input, openai_responses_impl):
+ """Test prepending a web search previous response to a new response."""
+
+ input_item_message = OpenAIResponseMessage(
+ id="123",
+ content=[OpenAIResponseInputMessageContentText(text="fake_previous_input")],
+ role="user",
+ )
+ input_items = OpenAIResponseInputItemList(data=[input_item_message])
+ output_web_search = OpenAIResponseOutputMessageWebSearchToolCall(
+ id="ws_123",
+ status="completed",
+ )
+ output_message = OpenAIResponseMessage(
+ id="123",
+ content=[OpenAIResponseOutputMessageContentOutputText(text="fake_web_search_response")],
+ status="completed",
+ role="assistant",
+ )
+ response = OpenAIResponseObject(
+ created_at=1,
+ id="resp_123",
+ model="fake_model",
+ output=[output_web_search, output_message],
+ status="completed",
+ )
+ previous_response = OpenAIResponsePreviousResponseWithInputItems(
+ input_items=input_items,
+ response=response,
+ )
+ get_previous_response_with_input.return_value = previous_response
+
+ input_messages = [OpenAIResponseMessage(content="fake_input", role="user")]
+ input = await openai_responses_impl._prepend_previous_response(input_messages, "resp_123")
+
+ assert len(input) == 4
+ # Check for previous input
+ assert isinstance(input[0], OpenAIResponseMessage)
+ assert input[0].content[0].text == "fake_previous_input"
+ # Check for previous output web search tool call
+ assert isinstance(input[1], OpenAIResponseOutputMessageWebSearchToolCall)
+ # Check for previous output web search response
+ assert isinstance(input[2], OpenAIResponseMessage)
+ assert input[2].content[0].text == "fake_web_search_response"
+ # Check for new input
+ assert isinstance(input[3], OpenAIResponseMessage)
+ assert input[3].content == "fake_input"