feat(responses)!: add support for OpenAI compatible Prompts in Responses API

This commit is contained in:
r3v5 2025-09-21 13:52:55 +01:00
parent bd3c473208
commit 59169bfd25
No known key found for this signature in database
GPG key ID: C7611ACB4FECAD54
33 changed files with 1667 additions and 34 deletions

View file

@ -5574,11 +5574,44 @@ components:
oneOf: oneOf:
- $ref: '#/components/schemas/OpenAIResponseInputMessageContentText' - $ref: '#/components/schemas/OpenAIResponseInputMessageContentText'
- $ref: '#/components/schemas/OpenAIResponseInputMessageContentImage' - $ref: '#/components/schemas/OpenAIResponseInputMessageContentImage'
- $ref: '#/components/schemas/OpenAIResponseInputMessageContentFile'
discriminator: discriminator:
propertyName: type propertyName: type
mapping: mapping:
input_text: '#/components/schemas/OpenAIResponseInputMessageContentText' input_text: '#/components/schemas/OpenAIResponseInputMessageContentText'
input_image: '#/components/schemas/OpenAIResponseInputMessageContentImage' input_image: '#/components/schemas/OpenAIResponseInputMessageContentImage'
input_file: '#/components/schemas/OpenAIResponseInputMessageContentFile'
OpenAIResponseInputMessageContentFile:
type: object
properties:
type:
type: string
const: input_file
default: input_file
description: >-
The type of the input item. Always `input_file`.
file_data:
type: string
description: >-
The data of the file to be sent to the model.
file_id:
type: string
description: >-
(Optional) The ID of the file to be sent to the model.
file_url:
type: string
description: >-
The URL of the file to be sent to the model.
filename:
type: string
description: >-
The name of the file to be sent to the model.
additionalProperties: false
required:
- type
title: OpenAIResponseInputMessageContentFile
description: >-
File content for input messages in OpenAI response format.
OpenAIResponseInputMessageContentImage: OpenAIResponseInputMessageContentImage:
type: object type: object
properties: properties:
@ -5599,6 +5632,10 @@ components:
default: input_image default: input_image
description: >- description: >-
Content type identifier, always "input_image" Content type identifier, always "input_image"
file_id:
type: string
description: >-
(Optional) The ID of the file to be sent to the model.
image_url: image_url:
type: string type: string
description: (Optional) URL of the image content description: (Optional) URL of the image content
@ -6998,6 +7035,10 @@ components:
type: string type: string
description: >- description: >-
(Optional) ID of the previous response in a conversation (Optional) ID of the previous response in a conversation
prompt:
$ref: '#/components/schemas/Prompt'
description: >-
(Optional) Prompt object with ID, version, and variables
status: status:
type: string type: string
description: >- description: >-
@ -7315,6 +7356,29 @@ components:
title: OpenAIResponseInputToolMCP title: OpenAIResponseInputToolMCP
description: >- description: >-
Model Context Protocol (MCP) tool configuration for OpenAI response inputs. Model Context Protocol (MCP) tool configuration for OpenAI response inputs.
OpenAIResponsePromptParam:
type: object
properties:
id:
type: string
description: Unique identifier of the prompt template
variables:
type: object
additionalProperties:
$ref: '#/components/schemas/OpenAIResponseInputMessageContent'
description: >-
Dictionary of variable names to OpenAIResponseInputMessageContent structure
for template substitution
version:
type: string
description: >-
Version number of the prompt to use (defaults to latest if not specified)
additionalProperties: false
required:
- id
title: OpenAIResponsePromptParam
description: >-
Prompt object that is used for OpenAI responses.
CreateOpenaiResponseRequest: CreateOpenaiResponseRequest:
type: object type: object
properties: properties:
@ -7328,6 +7392,10 @@ components:
model: model:
type: string type: string
description: The underlying LLM used for completions. description: The underlying LLM used for completions.
prompt:
$ref: '#/components/schemas/OpenAIResponsePromptParam'
description: >-
Prompt object with ID, version, and variables.
instructions: instructions:
type: string type: string
previous_response_id: previous_response_id:
@ -7405,6 +7473,10 @@ components:
type: string type: string
description: >- description: >-
(Optional) ID of the previous response in a conversation (Optional) ID of the previous response in a conversation
prompt:
$ref: '#/components/schemas/Prompt'
description: >-
(Optional) Prompt object with ID, version, and variables
status: status:
type: string type: string
description: >- description: >-

View file

@ -8593,16 +8593,53 @@
}, },
{ {
"$ref": "#/components/schemas/OpenAIResponseInputMessageContentImage" "$ref": "#/components/schemas/OpenAIResponseInputMessageContentImage"
},
{
"$ref": "#/components/schemas/OpenAIResponseInputMessageContentFile"
} }
], ],
"discriminator": { "discriminator": {
"propertyName": "type", "propertyName": "type",
"mapping": { "mapping": {
"input_text": "#/components/schemas/OpenAIResponseInputMessageContentText", "input_text": "#/components/schemas/OpenAIResponseInputMessageContentText",
"input_image": "#/components/schemas/OpenAIResponseInputMessageContentImage" "input_image": "#/components/schemas/OpenAIResponseInputMessageContentImage",
"input_file": "#/components/schemas/OpenAIResponseInputMessageContentFile"
} }
} }
}, },
"OpenAIResponseInputMessageContentFile": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "input_file",
"default": "input_file",
"description": "The type of the input item. Always `input_file`."
},
"file_data": {
"type": "string",
"description": "The data of the file to be sent to the model."
},
"file_id": {
"type": "string",
"description": "(Optional) The ID of the file to be sent to the model."
},
"file_url": {
"type": "string",
"description": "The URL of the file to be sent to the model."
},
"filename": {
"type": "string",
"description": "The name of the file to be sent to the model."
}
},
"additionalProperties": false,
"required": [
"type"
],
"title": "OpenAIResponseInputMessageContentFile",
"description": "File content for input messages in OpenAI response format."
},
"OpenAIResponseInputMessageContentImage": { "OpenAIResponseInputMessageContentImage": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -8630,6 +8667,10 @@
"default": "input_image", "default": "input_image",
"description": "Content type identifier, always \"input_image\"" "description": "Content type identifier, always \"input_image\""
}, },
"file_id": {
"type": "string",
"description": "(Optional) The ID of the file to be sent to the model."
},
"image_url": { "image_url": {
"type": "string", "type": "string",
"description": "(Optional) URL of the image content" "description": "(Optional) URL of the image content"
@ -8993,6 +9034,10 @@
"type": "string", "type": "string",
"description": "(Optional) ID of the previous response in a conversation" "description": "(Optional) ID of the previous response in a conversation"
}, },
"prompt": {
"$ref": "#/components/schemas/Prompt",
"description": "(Optional) Prompt object with ID, version, and variables"
},
"status": { "status": {
"type": "string", "type": "string",
"description": "Current status of the response generation" "description": "Current status of the response generation"
@ -9610,6 +9655,44 @@
"title": "OpenAIResponseUsage", "title": "OpenAIResponseUsage",
"description": "Usage information for OpenAI response." "description": "Usage information for OpenAI response."
}, },
"Prompt": {
"type": "object",
"properties": {
"prompt": {
"type": "string",
"description": "The system prompt text with variable placeholders. Variables are only supported when using the Responses API."
},
"version": {
"type": "integer",
"description": "Version (integer starting at 1, incremented on save)"
},
"prompt_id": {
"type": "string",
"description": "Unique identifier formatted as 'pmpt_<48-digit-hash>'"
},
"variables": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of prompt variable names that can be used in the prompt template"
},
"is_default": {
"type": "boolean",
"default": false,
"description": "Boolean indicating whether this version is the default version for this prompt"
}
},
"additionalProperties": false,
"required": [
"version",
"prompt_id",
"variables",
"is_default"
],
"title": "Prompt",
"description": "A prompt resource representing a stored OpenAI Compatible prompt template in Llama Stack."
},
"ResponseGuardrailSpec": { "ResponseGuardrailSpec": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -9766,6 +9849,32 @@
"title": "OpenAIResponseInputToolMCP", "title": "OpenAIResponseInputToolMCP",
"description": "Model Context Protocol (MCP) tool configuration for OpenAI response inputs." "description": "Model Context Protocol (MCP) tool configuration for OpenAI response inputs."
}, },
"OpenAIResponsePromptParam": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier of the prompt template"
},
"variables": {
"type": "object",
"additionalProperties": {
"$ref": "#/components/schemas/OpenAIResponseInputMessageContent"
},
"description": "Dictionary of variable names to OpenAIResponseInputMessageContent structure for template substitution"
},
"version": {
"type": "string",
"description": "Version number of the prompt to use (defaults to latest if not specified)"
}
},
"additionalProperties": false,
"required": [
"id"
],
"title": "OpenAIResponsePromptParam",
"description": "Prompt object that is used for OpenAI responses."
},
"CreateOpenaiResponseRequest": { "CreateOpenaiResponseRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -9787,6 +9896,10 @@
"type": "string", "type": "string",
"description": "The underlying LLM used for completions." "description": "The underlying LLM used for completions."
}, },
"prompt": {
"$ref": "#/components/schemas/OpenAIResponsePromptParam",
"description": "Prompt object with ID, version, and variables."
},
"instructions": { "instructions": {
"type": "string" "type": "string"
}, },
@ -9875,6 +9988,10 @@
"type": "string", "type": "string",
"description": "(Optional) ID of the previous response in a conversation" "description": "(Optional) ID of the previous response in a conversation"
}, },
"prompt": {
"$ref": "#/components/schemas/Prompt",
"description": "(Optional) Prompt object with ID, version, and variables"
},
"status": { "status": {
"type": "string", "type": "string",
"description": "Current status of the response generation" "description": "Current status of the response generation"

View file

@ -6409,11 +6409,44 @@ components:
oneOf: oneOf:
- $ref: '#/components/schemas/OpenAIResponseInputMessageContentText' - $ref: '#/components/schemas/OpenAIResponseInputMessageContentText'
- $ref: '#/components/schemas/OpenAIResponseInputMessageContentImage' - $ref: '#/components/schemas/OpenAIResponseInputMessageContentImage'
- $ref: '#/components/schemas/OpenAIResponseInputMessageContentFile'
discriminator: discriminator:
propertyName: type propertyName: type
mapping: mapping:
input_text: '#/components/schemas/OpenAIResponseInputMessageContentText' input_text: '#/components/schemas/OpenAIResponseInputMessageContentText'
input_image: '#/components/schemas/OpenAIResponseInputMessageContentImage' input_image: '#/components/schemas/OpenAIResponseInputMessageContentImage'
input_file: '#/components/schemas/OpenAIResponseInputMessageContentFile'
OpenAIResponseInputMessageContentFile:
type: object
properties:
type:
type: string
const: input_file
default: input_file
description: >-
The type of the input item. Always `input_file`.
file_data:
type: string
description: >-
The data of the file to be sent to the model.
file_id:
type: string
description: >-
(Optional) The ID of the file to be sent to the model.
file_url:
type: string
description: >-
The URL of the file to be sent to the model.
filename:
type: string
description: >-
The name of the file to be sent to the model.
additionalProperties: false
required:
- type
title: OpenAIResponseInputMessageContentFile
description: >-
File content for input messages in OpenAI response format.
OpenAIResponseInputMessageContentImage: OpenAIResponseInputMessageContentImage:
type: object type: object
properties: properties:
@ -6434,6 +6467,10 @@ components:
default: input_image default: input_image
description: >- description: >-
Content type identifier, always "input_image" Content type identifier, always "input_image"
file_id:
type: string
description: >-
(Optional) The ID of the file to be sent to the model.
image_url: image_url:
type: string type: string
description: (Optional) URL of the image content description: (Optional) URL of the image content
@ -6704,6 +6741,10 @@ components:
type: string type: string
description: >- description: >-
(Optional) ID of the previous response in a conversation (Optional) ID of the previous response in a conversation
prompt:
$ref: '#/components/schemas/Prompt'
description: >-
(Optional) Prompt object with ID, version, and variables
status: status:
type: string type: string
description: >- description: >-
@ -7181,6 +7222,44 @@ components:
- total_tokens - total_tokens
title: OpenAIResponseUsage title: OpenAIResponseUsage
description: Usage information for OpenAI response. description: Usage information for OpenAI response.
Prompt:
type: object
properties:
prompt:
type: string
description: >-
The system prompt text with variable placeholders. Variables are only
supported when using the Responses API.
version:
type: integer
description: >-
Version (integer starting at 1, incremented on save)
prompt_id:
type: string
description: >-
Unique identifier formatted as 'pmpt_<48-digit-hash>'
variables:
type: array
items:
type: string
description: >-
List of prompt variable names that can be used in the prompt template
is_default:
type: boolean
default: false
description: >-
Boolean indicating whether this version is the default version for this
prompt
additionalProperties: false
required:
- version
- prompt_id
- variables
- is_default
title: Prompt
description: >-
A prompt resource representing a stored OpenAI Compatible prompt template
in Llama Stack.
ResponseGuardrailSpec: ResponseGuardrailSpec:
type: object type: object
properties: properties:
@ -7287,6 +7366,29 @@ components:
title: OpenAIResponseInputToolMCP title: OpenAIResponseInputToolMCP
description: >- description: >-
Model Context Protocol (MCP) tool configuration for OpenAI response inputs. Model Context Protocol (MCP) tool configuration for OpenAI response inputs.
OpenAIResponsePromptParam:
type: object
properties:
id:
type: string
description: Unique identifier of the prompt template
variables:
type: object
additionalProperties:
$ref: '#/components/schemas/OpenAIResponseInputMessageContent'
description: >-
Dictionary of variable names to OpenAIResponseInputMessageContent structure
for template substitution
version:
type: string
description: >-
Version number of the prompt to use (defaults to latest if not specified)
additionalProperties: false
required:
- id
title: OpenAIResponsePromptParam
description: >-
Prompt object that is used for OpenAI responses.
CreateOpenaiResponseRequest: CreateOpenaiResponseRequest:
type: object type: object
properties: properties:
@ -7300,6 +7402,10 @@ components:
model: model:
type: string type: string
description: The underlying LLM used for completions. description: The underlying LLM used for completions.
prompt:
$ref: '#/components/schemas/OpenAIResponsePromptParam'
description: >-
Prompt object with ID, version, and variables.
instructions: instructions:
type: string type: string
previous_response_id: previous_response_id:
@ -7377,6 +7483,10 @@ components:
type: string type: string
description: >- description: >-
(Optional) ID of the previous response in a conversation (Optional) ID of the previous response in a conversation
prompt:
$ref: '#/components/schemas/Prompt'
description: >-
(Optional) Prompt object with ID, version, and variables
status: status:
type: string type: string
description: >- description: >-

View file

@ -5729,16 +5729,53 @@
}, },
{ {
"$ref": "#/components/schemas/OpenAIResponseInputMessageContentImage" "$ref": "#/components/schemas/OpenAIResponseInputMessageContentImage"
},
{
"$ref": "#/components/schemas/OpenAIResponseInputMessageContentFile"
} }
], ],
"discriminator": { "discriminator": {
"propertyName": "type", "propertyName": "type",
"mapping": { "mapping": {
"input_text": "#/components/schemas/OpenAIResponseInputMessageContentText", "input_text": "#/components/schemas/OpenAIResponseInputMessageContentText",
"input_image": "#/components/schemas/OpenAIResponseInputMessageContentImage" "input_image": "#/components/schemas/OpenAIResponseInputMessageContentImage",
"input_file": "#/components/schemas/OpenAIResponseInputMessageContentFile"
} }
} }
}, },
"OpenAIResponseInputMessageContentFile": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "input_file",
"default": "input_file",
"description": "The type of the input item. Always `input_file`."
},
"file_data": {
"type": "string",
"description": "The data of the file to be sent to the model."
},
"file_id": {
"type": "string",
"description": "(Optional) The ID of the file to be sent to the model."
},
"file_url": {
"type": "string",
"description": "The URL of the file to be sent to the model."
},
"filename": {
"type": "string",
"description": "The name of the file to be sent to the model."
}
},
"additionalProperties": false,
"required": [
"type"
],
"title": "OpenAIResponseInputMessageContentFile",
"description": "File content for input messages in OpenAI response format."
},
"OpenAIResponseInputMessageContentImage": { "OpenAIResponseInputMessageContentImage": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -5766,6 +5803,10 @@
"default": "input_image", "default": "input_image",
"description": "Content type identifier, always \"input_image\"" "description": "Content type identifier, always \"input_image\""
}, },
"file_id": {
"type": "string",
"description": "(Optional) The ID of the file to be sent to the model."
},
"image_url": { "image_url": {
"type": "string", "type": "string",
"description": "(Optional) URL of the image content" "description": "(Optional) URL of the image content"
@ -7569,6 +7610,10 @@
"type": "string", "type": "string",
"description": "(Optional) ID of the previous response in a conversation" "description": "(Optional) ID of the previous response in a conversation"
}, },
"prompt": {
"$ref": "#/components/schemas/Prompt",
"description": "(Optional) Prompt object with ID, version, and variables"
},
"status": { "status": {
"type": "string", "type": "string",
"description": "Current status of the response generation" "description": "Current status of the response generation"
@ -8013,6 +8058,32 @@
"title": "OpenAIResponseInputToolMCP", "title": "OpenAIResponseInputToolMCP",
"description": "Model Context Protocol (MCP) tool configuration for OpenAI response inputs." "description": "Model Context Protocol (MCP) tool configuration for OpenAI response inputs."
}, },
"OpenAIResponsePromptParam": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier of the prompt template"
},
"variables": {
"type": "object",
"additionalProperties": {
"$ref": "#/components/schemas/OpenAIResponseInputMessageContent"
},
"description": "Dictionary of variable names to OpenAIResponseInputMessageContent structure for template substitution"
},
"version": {
"type": "string",
"description": "Version number of the prompt to use (defaults to latest if not specified)"
}
},
"additionalProperties": false,
"required": [
"id"
],
"title": "OpenAIResponsePromptParam",
"description": "Prompt object that is used for OpenAI responses."
},
"CreateOpenaiResponseRequest": { "CreateOpenaiResponseRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -8034,6 +8105,10 @@
"type": "string", "type": "string",
"description": "The underlying LLM used for completions." "description": "The underlying LLM used for completions."
}, },
"prompt": {
"$ref": "#/components/schemas/OpenAIResponsePromptParam",
"description": "Prompt object with ID, version, and variables."
},
"instructions": { "instructions": {
"type": "string" "type": "string"
}, },
@ -8122,6 +8197,10 @@
"type": "string", "type": "string",
"description": "(Optional) ID of the previous response in a conversation" "description": "(Optional) ID of the previous response in a conversation"
}, },
"prompt": {
"$ref": "#/components/schemas/Prompt",
"description": "(Optional) Prompt object with ID, version, and variables"
},
"status": { "status": {
"type": "string", "type": "string",
"description": "Current status of the response generation" "description": "Current status of the response generation"

View file

@ -4361,11 +4361,44 @@ components:
oneOf: oneOf:
- $ref: '#/components/schemas/OpenAIResponseInputMessageContentText' - $ref: '#/components/schemas/OpenAIResponseInputMessageContentText'
- $ref: '#/components/schemas/OpenAIResponseInputMessageContentImage' - $ref: '#/components/schemas/OpenAIResponseInputMessageContentImage'
- $ref: '#/components/schemas/OpenAIResponseInputMessageContentFile'
discriminator: discriminator:
propertyName: type propertyName: type
mapping: mapping:
input_text: '#/components/schemas/OpenAIResponseInputMessageContentText' input_text: '#/components/schemas/OpenAIResponseInputMessageContentText'
input_image: '#/components/schemas/OpenAIResponseInputMessageContentImage' input_image: '#/components/schemas/OpenAIResponseInputMessageContentImage'
input_file: '#/components/schemas/OpenAIResponseInputMessageContentFile'
OpenAIResponseInputMessageContentFile:
type: object
properties:
type:
type: string
const: input_file
default: input_file
description: >-
The type of the input item. Always `input_file`.
file_data:
type: string
description: >-
The data of the file to be sent to the model.
file_id:
type: string
description: >-
(Optional) The ID of the file to be sent to the model.
file_url:
type: string
description: >-
The URL of the file to be sent to the model.
filename:
type: string
description: >-
The name of the file to be sent to the model.
additionalProperties: false
required:
- type
title: OpenAIResponseInputMessageContentFile
description: >-
File content for input messages in OpenAI response format.
OpenAIResponseInputMessageContentImage: OpenAIResponseInputMessageContentImage:
type: object type: object
properties: properties:
@ -4386,6 +4419,10 @@ components:
default: input_image default: input_image
description: >- description: >-
Content type identifier, always "input_image" Content type identifier, always "input_image"
file_id:
type: string
description: >-
(Optional) The ID of the file to be sent to the model.
image_url: image_url:
type: string type: string
description: (Optional) URL of the image content description: (Optional) URL of the image content
@ -5785,6 +5822,10 @@ components:
type: string type: string
description: >- description: >-
(Optional) ID of the previous response in a conversation (Optional) ID of the previous response in a conversation
prompt:
$ref: '#/components/schemas/Prompt'
description: >-
(Optional) Prompt object with ID, version, and variables
status: status:
type: string type: string
description: >- description: >-
@ -6102,6 +6143,29 @@ components:
title: OpenAIResponseInputToolMCP title: OpenAIResponseInputToolMCP
description: >- description: >-
Model Context Protocol (MCP) tool configuration for OpenAI response inputs. Model Context Protocol (MCP) tool configuration for OpenAI response inputs.
OpenAIResponsePromptParam:
type: object
properties:
id:
type: string
description: Unique identifier of the prompt template
variables:
type: object
additionalProperties:
$ref: '#/components/schemas/OpenAIResponseInputMessageContent'
description: >-
Dictionary of variable names to OpenAIResponseInputMessageContent structure
for template substitution
version:
type: string
description: >-
Version number of the prompt to use (defaults to latest if not specified)
additionalProperties: false
required:
- id
title: OpenAIResponsePromptParam
description: >-
Prompt object that is used for OpenAI responses.
CreateOpenaiResponseRequest: CreateOpenaiResponseRequest:
type: object type: object
properties: properties:
@ -6115,6 +6179,10 @@ components:
model: model:
type: string type: string
description: The underlying LLM used for completions. description: The underlying LLM used for completions.
prompt:
$ref: '#/components/schemas/OpenAIResponsePromptParam'
description: >-
Prompt object with ID, version, and variables.
instructions: instructions:
type: string type: string
previous_response_id: previous_response_id:
@ -6192,6 +6260,10 @@ components:
type: string type: string
description: >- description: >-
(Optional) ID of the previous response in a conversation (Optional) ID of the previous response in a conversation
prompt:
$ref: '#/components/schemas/Prompt'
description: >-
(Optional) Prompt object with ID, version, and variables
status: status:
type: string type: string
description: >- description: >-

View file

@ -7401,16 +7401,53 @@
}, },
{ {
"$ref": "#/components/schemas/OpenAIResponseInputMessageContentImage" "$ref": "#/components/schemas/OpenAIResponseInputMessageContentImage"
},
{
"$ref": "#/components/schemas/OpenAIResponseInputMessageContentFile"
} }
], ],
"discriminator": { "discriminator": {
"propertyName": "type", "propertyName": "type",
"mapping": { "mapping": {
"input_text": "#/components/schemas/OpenAIResponseInputMessageContentText", "input_text": "#/components/schemas/OpenAIResponseInputMessageContentText",
"input_image": "#/components/schemas/OpenAIResponseInputMessageContentImage" "input_image": "#/components/schemas/OpenAIResponseInputMessageContentImage",
"input_file": "#/components/schemas/OpenAIResponseInputMessageContentFile"
} }
} }
}, },
"OpenAIResponseInputMessageContentFile": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "input_file",
"default": "input_file",
"description": "The type of the input item. Always `input_file`."
},
"file_data": {
"type": "string",
"description": "The data of the file to be sent to the model."
},
"file_id": {
"type": "string",
"description": "(Optional) The ID of the file to be sent to the model."
},
"file_url": {
"type": "string",
"description": "The URL of the file to be sent to the model."
},
"filename": {
"type": "string",
"description": "The name of the file to be sent to the model."
}
},
"additionalProperties": false,
"required": [
"type"
],
"title": "OpenAIResponseInputMessageContentFile",
"description": "File content for input messages in OpenAI response format."
},
"OpenAIResponseInputMessageContentImage": { "OpenAIResponseInputMessageContentImage": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -7438,6 +7475,10 @@
"default": "input_image", "default": "input_image",
"description": "Content type identifier, always \"input_image\"" "description": "Content type identifier, always \"input_image\""
}, },
"file_id": {
"type": "string",
"description": "(Optional) The ID of the file to be sent to the model."
},
"image_url": { "image_url": {
"type": "string", "type": "string",
"description": "(Optional) URL of the image content" "description": "(Optional) URL of the image content"
@ -9241,6 +9282,10 @@
"type": "string", "type": "string",
"description": "(Optional) ID of the previous response in a conversation" "description": "(Optional) ID of the previous response in a conversation"
}, },
"prompt": {
"$ref": "#/components/schemas/Prompt",
"description": "(Optional) Prompt object with ID, version, and variables"
},
"status": { "status": {
"type": "string", "type": "string",
"description": "Current status of the response generation" "description": "Current status of the response generation"
@ -9685,6 +9730,32 @@
"title": "OpenAIResponseInputToolMCP", "title": "OpenAIResponseInputToolMCP",
"description": "Model Context Protocol (MCP) tool configuration for OpenAI response inputs." "description": "Model Context Protocol (MCP) tool configuration for OpenAI response inputs."
}, },
"OpenAIResponsePromptParam": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier of the prompt template"
},
"variables": {
"type": "object",
"additionalProperties": {
"$ref": "#/components/schemas/OpenAIResponseInputMessageContent"
},
"description": "Dictionary of variable names to OpenAIResponseInputMessageContent structure for template substitution"
},
"version": {
"type": "string",
"description": "Version number of the prompt to use (defaults to latest if not specified)"
}
},
"additionalProperties": false,
"required": [
"id"
],
"title": "OpenAIResponsePromptParam",
"description": "Prompt object that is used for OpenAI responses."
},
"CreateOpenaiResponseRequest": { "CreateOpenaiResponseRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -9706,6 +9777,10 @@
"type": "string", "type": "string",
"description": "The underlying LLM used for completions." "description": "The underlying LLM used for completions."
}, },
"prompt": {
"$ref": "#/components/schemas/OpenAIResponsePromptParam",
"description": "Prompt object with ID, version, and variables."
},
"instructions": { "instructions": {
"type": "string" "type": "string"
}, },
@ -9794,6 +9869,10 @@
"type": "string", "type": "string",
"description": "(Optional) ID of the previous response in a conversation" "description": "(Optional) ID of the previous response in a conversation"
}, },
"prompt": {
"$ref": "#/components/schemas/Prompt",
"description": "(Optional) Prompt object with ID, version, and variables"
},
"status": { "status": {
"type": "string", "type": "string",
"description": "Current status of the response generation" "description": "Current status of the response generation"

View file

@ -5574,11 +5574,44 @@ components:
oneOf: oneOf:
- $ref: '#/components/schemas/OpenAIResponseInputMessageContentText' - $ref: '#/components/schemas/OpenAIResponseInputMessageContentText'
- $ref: '#/components/schemas/OpenAIResponseInputMessageContentImage' - $ref: '#/components/schemas/OpenAIResponseInputMessageContentImage'
- $ref: '#/components/schemas/OpenAIResponseInputMessageContentFile'
discriminator: discriminator:
propertyName: type propertyName: type
mapping: mapping:
input_text: '#/components/schemas/OpenAIResponseInputMessageContentText' input_text: '#/components/schemas/OpenAIResponseInputMessageContentText'
input_image: '#/components/schemas/OpenAIResponseInputMessageContentImage' input_image: '#/components/schemas/OpenAIResponseInputMessageContentImage'
input_file: '#/components/schemas/OpenAIResponseInputMessageContentFile'
OpenAIResponseInputMessageContentFile:
type: object
properties:
type:
type: string
const: input_file
default: input_file
description: >-
The type of the input item. Always `input_file`.
file_data:
type: string
description: >-
The data of the file to be sent to the model.
file_id:
type: string
description: >-
(Optional) The ID of the file to be sent to the model.
file_url:
type: string
description: >-
The URL of the file to be sent to the model.
filename:
type: string
description: >-
The name of the file to be sent to the model.
additionalProperties: false
required:
- type
title: OpenAIResponseInputMessageContentFile
description: >-
File content for input messages in OpenAI response format.
OpenAIResponseInputMessageContentImage: OpenAIResponseInputMessageContentImage:
type: object type: object
properties: properties:
@ -5599,6 +5632,10 @@ components:
default: input_image default: input_image
description: >- description: >-
Content type identifier, always "input_image" Content type identifier, always "input_image"
file_id:
type: string
description: >-
(Optional) The ID of the file to be sent to the model.
image_url: image_url:
type: string type: string
description: (Optional) URL of the image content description: (Optional) URL of the image content
@ -6998,6 +7035,10 @@ components:
type: string type: string
description: >- description: >-
(Optional) ID of the previous response in a conversation (Optional) ID of the previous response in a conversation
prompt:
$ref: '#/components/schemas/Prompt'
description: >-
(Optional) Prompt object with ID, version, and variables
status: status:
type: string type: string
description: >- description: >-
@ -7315,6 +7356,29 @@ components:
title: OpenAIResponseInputToolMCP title: OpenAIResponseInputToolMCP
description: >- description: >-
Model Context Protocol (MCP) tool configuration for OpenAI response inputs. Model Context Protocol (MCP) tool configuration for OpenAI response inputs.
OpenAIResponsePromptParam:
type: object
properties:
id:
type: string
description: Unique identifier of the prompt template
variables:
type: object
additionalProperties:
$ref: '#/components/schemas/OpenAIResponseInputMessageContent'
description: >-
Dictionary of variable names to OpenAIResponseInputMessageContent structure
for template substitution
version:
type: string
description: >-
Version number of the prompt to use (defaults to latest if not specified)
additionalProperties: false
required:
- id
title: OpenAIResponsePromptParam
description: >-
Prompt object that is used for OpenAI responses.
CreateOpenaiResponseRequest: CreateOpenaiResponseRequest:
type: object type: object
properties: properties:
@ -7328,6 +7392,10 @@ components:
model: model:
type: string type: string
description: The underlying LLM used for completions. description: The underlying LLM used for completions.
prompt:
$ref: '#/components/schemas/OpenAIResponsePromptParam'
description: >-
Prompt object with ID, version, and variables.
instructions: instructions:
type: string type: string
previous_response_id: previous_response_id:
@ -7405,6 +7473,10 @@ components:
type: string type: string
description: >- description: >-
(Optional) ID of the previous response in a conversation (Optional) ID of the previous response in a conversation
prompt:
$ref: '#/components/schemas/Prompt'
description: >-
(Optional) Prompt object with ID, version, and variables
status: status:
type: string type: string
description: >- description: >-

View file

@ -38,6 +38,7 @@ from .openai_responses import (
OpenAIResponseInputTool, OpenAIResponseInputTool,
OpenAIResponseObject, OpenAIResponseObject,
OpenAIResponseObjectStream, OpenAIResponseObjectStream,
OpenAIResponsePromptParam,
OpenAIResponseText, OpenAIResponseText,
) )
@ -810,6 +811,7 @@ class Agents(Protocol):
self, self,
input: str | list[OpenAIResponseInput], input: str | list[OpenAIResponseInput],
model: str, model: str,
prompt: OpenAIResponsePromptParam | None = None,
instructions: str | None = None, instructions: str | None = None,
previous_response_id: str | None = None, previous_response_id: str | None = None,
conversation: str | None = None, conversation: str | None = None,
@ -831,6 +833,7 @@ class Agents(Protocol):
:param input: Input message(s) to create the response. :param input: Input message(s) to create the response.
:param model: The underlying LLM used for completions. :param model: The underlying LLM used for completions.
:param prompt: Prompt object with ID, version, and variables.
:param previous_response_id: (Optional) if specified, the new response will be a continuation of the previous response. This can be used to easily fork-off new responses from existing responses. :param previous_response_id: (Optional) if specified, the new response will be a continuation of the previous response. This can be used to easily fork-off new responses from existing responses.
:param conversation: (Optional) The ID of a conversation to add the response to. Must begin with 'conv_'. Input and output messages will be automatically added to the conversation. :param conversation: (Optional) The ID of a conversation to add the response to. Must begin with 'conv_'. Input and output messages will be automatically added to the conversation.
:param include: (Optional) Additional fields to include in the response. :param include: (Optional) Additional fields to include in the response.

View file

@ -6,9 +6,10 @@
from typing import Annotated, Any, Literal from typing import Annotated, Any, Literal
from pydantic import BaseModel, Field from pydantic import BaseModel, Field, model_validator
from typing_extensions import TypedDict from typing_extensions import TypedDict
from llama_stack.apis.prompts.prompts import Prompt
from llama_stack.apis.vector_io import SearchRankingOptions as FileSearchRankingOptions from llama_stack.apis.vector_io import SearchRankingOptions as FileSearchRankingOptions
from llama_stack.schema_utils import json_schema_type, register_schema from llama_stack.schema_utils import json_schema_type, register_schema
@ -46,18 +47,44 @@ class OpenAIResponseInputMessageContentImage(BaseModel):
:param detail: Level of detail for image processing, can be "low", "high", or "auto" :param detail: Level of detail for image processing, can be "low", "high", or "auto"
:param type: Content type identifier, always "input_image" :param type: Content type identifier, always "input_image"
:param file_id: (Optional) The ID of the file to be sent to the model.
:param image_url: (Optional) URL of the image content :param image_url: (Optional) URL of the image content
""" """
detail: Literal["low"] | Literal["high"] | Literal["auto"] = "auto" detail: Literal["low"] | Literal["high"] | Literal["auto"] = "auto"
type: Literal["input_image"] = "input_image" type: Literal["input_image"] = "input_image"
# TODO: handle file_id file_id: str | None = None
image_url: str | None = None image_url: str | None = None
# TODO: handle file content types @json_schema_type
class OpenAIResponseInputMessageContentFile(BaseModel):
"""File content for input messages in OpenAI response format.
:param type: The type of the input item. Always `input_file`.
:param file_data: The data of the file to be sent to the model.
:param file_id: (Optional) The ID of the file to be sent to the model.
:param file_url: The URL of the file to be sent to the model.
:param filename: The name of the file to be sent to the model.
"""
type: Literal["input_file"] = "input_file"
file_data: str | None = None
file_id: str | None = None
file_url: str | None = None
filename: str | None = None
@model_validator(mode="after")
def validate_file_source(self) -> "OpenAIResponseInputMessageContentFile":
if not any([self.file_id, self.file_data, self.file_url]):
raise ValueError("At least one of 'file_id', 'file_data', or 'file_url' must be provided for file content")
return self
OpenAIResponseInputMessageContent = Annotated[ OpenAIResponseInputMessageContent = Annotated[
OpenAIResponseInputMessageContentText | OpenAIResponseInputMessageContentImage, OpenAIResponseInputMessageContentText
| OpenAIResponseInputMessageContentImage
| OpenAIResponseInputMessageContentFile,
Field(discriminator="type"), Field(discriminator="type"),
] ]
register_schema(OpenAIResponseInputMessageContent, name="OpenAIResponseInputMessageContent") register_schema(OpenAIResponseInputMessageContent, name="OpenAIResponseInputMessageContent")
@ -348,6 +375,20 @@ class OpenAIResponseTextFormat(TypedDict, total=False):
strict: bool | None strict: bool | None
@json_schema_type
class OpenAIResponsePromptParam(BaseModel):
"""Prompt object that is used for OpenAI responses.
:param id: Unique identifier of the prompt template
:param variables: Dictionary of variable names to OpenAIResponseInputMessageContent structure for template substitution
:param version: Version number of the prompt to use (defaults to latest if not specified)
"""
id: str
variables: dict[str, OpenAIResponseInputMessageContent] | None = None
version: str | None = None
@json_schema_type @json_schema_type
class OpenAIResponseText(BaseModel): class OpenAIResponseText(BaseModel):
"""Text response configuration for OpenAI responses. """Text response configuration for OpenAI responses.
@ -537,6 +578,7 @@ class OpenAIResponseObject(BaseModel):
:param object: Object type identifier, always "response" :param object: Object type identifier, always "response"
:param output: List of generated output items (messages, tool calls, etc.) :param output: List of generated output items (messages, tool calls, etc.)
:param parallel_tool_calls: Whether tool calls can be executed in parallel :param parallel_tool_calls: Whether tool calls can be executed in parallel
:param prompt: (Optional) Prompt object with ID, version, and variables
:param previous_response_id: (Optional) ID of the previous response in a conversation :param previous_response_id: (Optional) ID of the previous response in a conversation
:param status: Current status of the response generation :param status: Current status of the response generation
:param temperature: (Optional) Sampling temperature used for generation :param temperature: (Optional) Sampling temperature used for generation
@ -556,6 +598,7 @@ class OpenAIResponseObject(BaseModel):
output: list[OpenAIResponseOutput] output: list[OpenAIResponseOutput]
parallel_tool_calls: bool = False parallel_tool_calls: bool = False
previous_response_id: str | None = None previous_response_id: str | None = None
prompt: Prompt | None = None
status: str status: str
temperature: float | None = None temperature: float | None = None
# Default to text format to avoid breaking the loading of old responses # Default to text format to avoid breaking the loading of old responses

View file

@ -247,6 +247,9 @@ storage:
conversations: conversations:
table_name: openai_conversations table_name: openai_conversations
backend: sql_default backend: sql_default
prompts:
table_name: prompts
backend: sql_default
registered_resources: registered_resources:
models: [] models: []
shields: shields:

View file

@ -109,6 +109,9 @@ storage:
conversations: conversations:
table_name: openai_conversations table_name: openai_conversations
backend: sql_default backend: sql_default
prompts:
table_name: prompts
backend: sql_default
registered_resources: registered_resources:
models: models:
- metadata: {} - metadata: {}

View file

@ -105,6 +105,9 @@ storage:
conversations: conversations:
table_name: openai_conversations table_name: openai_conversations
backend: sql_default backend: sql_default
prompts:
table_name: prompts
backend: sql_default
registered_resources: registered_resources:
models: models:
- metadata: {} - metadata: {}

View file

@ -122,6 +122,9 @@ storage:
conversations: conversations:
table_name: openai_conversations table_name: openai_conversations
backend: sql_default backend: sql_default
prompts:
table_name: prompts
backend: sql_default
registered_resources: registered_resources:
models: models:
- metadata: {} - metadata: {}

View file

@ -112,6 +112,9 @@ storage:
conversations: conversations:
table_name: openai_conversations table_name: openai_conversations
backend: sql_default backend: sql_default
prompts:
table_name: prompts
backend: sql_default
registered_resources: registered_resources:
models: models:
- metadata: {} - metadata: {}

View file

@ -111,6 +111,9 @@ storage:
conversations: conversations:
table_name: openai_conversations table_name: openai_conversations
backend: sql_default backend: sql_default
prompts:
table_name: prompts
backend: sql_default
registered_resources: registered_resources:
models: models:
- metadata: {} - metadata: {}

View file

@ -100,6 +100,9 @@ storage:
conversations: conversations:
table_name: openai_conversations table_name: openai_conversations
backend: sql_default backend: sql_default
prompts:
table_name: prompts
backend: sql_default
registered_resources: registered_resources:
models: [] models: []
shields: [] shields: []

View file

@ -142,6 +142,9 @@ storage:
conversations: conversations:
table_name: openai_conversations table_name: openai_conversations
backend: sql_default backend: sql_default
prompts:
table_name: prompts
backend: sql_default
registered_resources: registered_resources:
models: models:
- metadata: {} - metadata: {}

View file

@ -87,6 +87,9 @@ storage:
conversations: conversations:
table_name: openai_conversations table_name: openai_conversations
backend: sql_default backend: sql_default
prompts:
table_name: prompts
backend: sql_default
registered_resources: registered_resources:
models: models:
- metadata: {} - metadata: {}

View file

@ -250,6 +250,9 @@ storage:
conversations: conversations:
table_name: openai_conversations table_name: openai_conversations
backend: sql_default backend: sql_default
prompts:
table_name: prompts
backend: sql_default
registered_resources: registered_resources:
models: [] models: []
shields: shields:

View file

@ -247,6 +247,9 @@ storage:
conversations: conversations:
table_name: openai_conversations table_name: openai_conversations
backend: sql_default backend: sql_default
prompts:
table_name: prompts
backend: sql_default
registered_resources: registered_resources:
models: [] models: []
shields: shields:

View file

@ -257,6 +257,10 @@ class RunConfigSettings(BaseModel):
backend="sql_default", backend="sql_default",
table_name="openai_conversations", table_name="openai_conversations",
).model_dump(exclude_none=True), ).model_dump(exclude_none=True),
"prompts": SqlStoreReference(
backend="sql_default",
table_name="prompts",
).model_dump(exclude_none=True),
} }
storage_config = dict( storage_config = dict(

View file

@ -115,6 +115,9 @@ storage:
conversations: conversations:
table_name: openai_conversations table_name: openai_conversations
backend: sql_default backend: sql_default
prompts:
table_name: prompts
backend: sql_default
registered_resources: registered_resources:
models: [] models: []
shields: [] shields: []

View file

@ -20,15 +20,17 @@ async def get_provider_impl(
from .agents import MetaReferenceAgentsImpl from .agents import MetaReferenceAgentsImpl
impl = MetaReferenceAgentsImpl( impl = MetaReferenceAgentsImpl(
config, config=config,
deps[Api.inference], inference_api=deps[Api.inference],
deps[Api.vector_io], vector_io_api=deps[Api.vector_io],
deps[Api.safety], safety_api=deps[Api.safety],
deps[Api.tool_runtime], tool_runtime_api=deps[Api.tool_runtime],
deps[Api.tool_groups], tool_groups_api=deps[Api.tool_groups],
deps[Api.conversations], conversations_api=deps[Api.conversations],
policy, prompts_api=deps[Api.prompts],
telemetry_enabled, files_api=deps[Api.files],
telemetry_enabled=Api.telemetry in deps,
policy=policy,
) )
await impl.initialize() await impl.initialize()
return impl return impl

View file

@ -29,9 +29,10 @@ from llama_stack.apis.agents import (
Turn, Turn,
) )
from llama_stack.apis.agents.agents import ResponseGuardrail from llama_stack.apis.agents.agents import ResponseGuardrail
from llama_stack.apis.agents.openai_responses import OpenAIResponseText from llama_stack.apis.agents.openai_responses import OpenAIResponsePromptParam, OpenAIResponseText
from llama_stack.apis.common.responses import PaginatedResponse from llama_stack.apis.common.responses import PaginatedResponse
from llama_stack.apis.conversations import Conversations from llama_stack.apis.conversations import Conversations
from llama_stack.apis.files import Files
from llama_stack.apis.inference import ( from llama_stack.apis.inference import (
Inference, Inference,
ToolConfig, ToolConfig,
@ -39,6 +40,7 @@ from llama_stack.apis.inference import (
ToolResponseMessage, ToolResponseMessage,
UserMessage, UserMessage,
) )
from llama_stack.apis.prompts import Prompts
from llama_stack.apis.safety import Safety from llama_stack.apis.safety import Safety
from llama_stack.apis.tools import ToolGroups, ToolRuntime from llama_stack.apis.tools import ToolGroups, ToolRuntime
from llama_stack.apis.vector_io import VectorIO from llama_stack.apis.vector_io import VectorIO
@ -66,6 +68,8 @@ class MetaReferenceAgentsImpl(Agents):
tool_runtime_api: ToolRuntime, tool_runtime_api: ToolRuntime,
tool_groups_api: ToolGroups, tool_groups_api: ToolGroups,
conversations_api: Conversations, conversations_api: Conversations,
prompts_api: Prompts,
files_api: Files,
policy: list[AccessRule], policy: list[AccessRule],
telemetry_enabled: bool = False, telemetry_enabled: bool = False,
): ):
@ -77,7 +81,8 @@ class MetaReferenceAgentsImpl(Agents):
self.tool_groups_api = tool_groups_api self.tool_groups_api = tool_groups_api
self.conversations_api = conversations_api self.conversations_api = conversations_api
self.telemetry_enabled = telemetry_enabled self.telemetry_enabled = telemetry_enabled
self.prompts_api = prompts_api
self.files_api = files_api
self.in_memory_store = InmemoryKVStoreImpl() self.in_memory_store = InmemoryKVStoreImpl()
self.openai_responses_impl: OpenAIResponsesImpl | None = None self.openai_responses_impl: OpenAIResponsesImpl | None = None
self.policy = policy self.policy = policy
@ -94,6 +99,8 @@ class MetaReferenceAgentsImpl(Agents):
vector_io_api=self.vector_io_api, vector_io_api=self.vector_io_api,
safety_api=self.safety_api, safety_api=self.safety_api,
conversations_api=self.conversations_api, conversations_api=self.conversations_api,
prompts_api=self.prompts_api,
files_api=self.files_api,
) )
async def create_agent( async def create_agent(
@ -329,6 +336,7 @@ class MetaReferenceAgentsImpl(Agents):
self, self,
input: str | list[OpenAIResponseInput], input: str | list[OpenAIResponseInput],
model: str, model: str,
prompt: OpenAIResponsePromptParam | None = None,
instructions: str | None = None, instructions: str | None = None,
previous_response_id: str | None = None, previous_response_id: str | None = None,
conversation: str | None = None, conversation: str | None = None,
@ -344,6 +352,7 @@ class MetaReferenceAgentsImpl(Agents):
return await self.openai_responses_impl.create_openai_response( return await self.openai_responses_impl.create_openai_response(
input, input,
model, model,
prompt,
instructions, instructions,
previous_response_id, previous_response_id,
conversation, conversation,

View file

@ -4,6 +4,7 @@
# This source code is licensed under the terms described in the LICENSE file in # This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree. # the root directory of this source tree.
import re
import time import time
import uuid import uuid
from collections.abc import AsyncIterator from collections.abc import AsyncIterator
@ -17,11 +18,14 @@ from llama_stack.apis.agents.openai_responses import (
ListOpenAIResponseObject, ListOpenAIResponseObject,
OpenAIDeleteResponseObject, OpenAIDeleteResponseObject,
OpenAIResponseInput, OpenAIResponseInput,
OpenAIResponseInputMessageContentFile,
OpenAIResponseInputMessageContentImage,
OpenAIResponseInputMessageContentText, OpenAIResponseInputMessageContentText,
OpenAIResponseInputTool, OpenAIResponseInputTool,
OpenAIResponseMessage, OpenAIResponseMessage,
OpenAIResponseObject, OpenAIResponseObject,
OpenAIResponseObjectStream, OpenAIResponseObjectStream,
OpenAIResponsePromptParam,
OpenAIResponseText, OpenAIResponseText,
OpenAIResponseTextFormat, OpenAIResponseTextFormat,
) )
@ -30,11 +34,17 @@ from llama_stack.apis.common.errors import (
) )
from llama_stack.apis.conversations import Conversations from llama_stack.apis.conversations import Conversations
from llama_stack.apis.conversations.conversations import ConversationItem from llama_stack.apis.conversations.conversations import ConversationItem
from llama_stack.apis.files import Files
from llama_stack.apis.inference import ( from llama_stack.apis.inference import (
Inference, Inference,
OpenAIChatCompletionContentPartParam,
OpenAIChatCompletionContentPartTextParam,
OpenAIMessageParam, OpenAIMessageParam,
OpenAISystemMessageParam, OpenAISystemMessageParam,
OpenAIUserMessageParam,
) )
from llama_stack.apis.prompts import Prompts
from llama_stack.apis.prompts.prompts import Prompt
from llama_stack.apis.safety import Safety from llama_stack.apis.safety import Safety
from llama_stack.apis.tools import ToolGroups, ToolRuntime from llama_stack.apis.tools import ToolGroups, ToolRuntime
from llama_stack.apis.vector_io import VectorIO from llama_stack.apis.vector_io import VectorIO
@ -71,6 +81,8 @@ class OpenAIResponsesImpl:
vector_io_api: VectorIO, # VectorIO vector_io_api: VectorIO, # VectorIO
safety_api: Safety, safety_api: Safety,
conversations_api: Conversations, conversations_api: Conversations,
prompts_api: Prompts,
files_api: Files,
): ):
self.inference_api = inference_api self.inference_api = inference_api
self.tool_groups_api = tool_groups_api self.tool_groups_api = tool_groups_api
@ -84,6 +96,8 @@ class OpenAIResponsesImpl:
tool_runtime_api=tool_runtime_api, tool_runtime_api=tool_runtime_api,
vector_io_api=vector_io_api, vector_io_api=vector_io_api,
) )
self.prompts_api = prompts_api
self.files_api = files_api
async def _prepend_previous_response( async def _prepend_previous_response(
self, self,
@ -123,11 +137,13 @@ class OpenAIResponsesImpl:
# Use stored messages directly and convert only new input # Use stored messages directly and convert only new input
message_adapter = TypeAdapter(list[OpenAIMessageParam]) message_adapter = TypeAdapter(list[OpenAIMessageParam])
messages = message_adapter.validate_python(previous_response.messages) messages = message_adapter.validate_python(previous_response.messages)
new_messages = await convert_response_input_to_chat_messages(input, previous_messages=messages) new_messages = await convert_response_input_to_chat_messages(
input, previous_messages=messages, files_api=self.files_api
)
messages.extend(new_messages) messages.extend(new_messages)
else: else:
# Backward compatibility: reconstruct from inputs # Backward compatibility: reconstruct from inputs
messages = await convert_response_input_to_chat_messages(all_input) messages = await convert_response_input_to_chat_messages(all_input, files_api=self.files_api)
tool_context.recover_tools_from_previous_response(previous_response) tool_context.recover_tools_from_previous_response(previous_response)
elif conversation is not None: elif conversation is not None:
@ -139,7 +155,7 @@ class OpenAIResponsesImpl:
all_input = input all_input = input
if not conversation_items.data: if not conversation_items.data:
# First turn - just convert the new input # First turn - just convert the new input
messages = await convert_response_input_to_chat_messages(input) messages = await convert_response_input_to_chat_messages(input, files_api=self.files_api)
else: else:
if not stored_messages: if not stored_messages:
all_input = conversation_items.data all_input = conversation_items.data
@ -155,14 +171,114 @@ class OpenAIResponsesImpl:
all_input = input all_input = input
messages = stored_messages or [] messages = stored_messages or []
new_messages = await convert_response_input_to_chat_messages(all_input, previous_messages=messages) new_messages = await convert_response_input_to_chat_messages(
all_input, previous_messages=messages, files_api=self.files_api
)
messages.extend(new_messages) messages.extend(new_messages)
else: else:
all_input = input all_input = input
messages = await convert_response_input_to_chat_messages(all_input) messages = await convert_response_input_to_chat_messages(all_input, files_api=self.files_api)
return all_input, messages, tool_context return all_input, messages, tool_context
async def _prepend_prompt(
self,
messages: list[OpenAIMessageParam],
prompt_params: OpenAIResponsePromptParam,
) -> Prompt:
"""Prepend prompt template to messages, resolving text/image/file variables.
For text-only prompts: Inserts as system message
For prompts with media: Inserts text as system message + media into first user message
"""
if not prompt_params or not prompt_params.id:
return None
prompt_version = int(prompt_params.version) if prompt_params.version else None
cur_prompt = await self.prompts_api.get_prompt(prompt_params.id, prompt_version)
if not cur_prompt:
return None
cur_prompt_text = cur_prompt.prompt
cur_prompt_variables = cur_prompt.variables
if not prompt_params.variables:
messages.insert(0, OpenAISystemMessageParam(content=cur_prompt_text))
return cur_prompt
# Validate that all provided variables exist in the prompt
for name in prompt_params.variables.keys():
if name not in cur_prompt_variables:
raise ValueError(f"Variable {name} not found in prompt {prompt_params.id}")
# Separate text and media variables
text_substitutions = {}
media_content_parts = []
for name, value in prompt_params.variables.items():
# Text variable found
if isinstance(value, OpenAIResponseInputMessageContentText):
text_substitutions[name] = value.text
# Media variable found
elif isinstance(value, OpenAIResponseInputMessageContentImage | OpenAIResponseInputMessageContentFile):
# use existing converter to achieve OpenAI Chat Completion format
from .utils import convert_response_content_to_chat_content
converted_parts = await convert_response_content_to_chat_content([value], files_api=self.files_api)
media_content_parts.extend(converted_parts)
# Eg: {{product_photo}} becomes "[Image: product_photo]"
# This gives the model textual context about what media exists in the prompt
var_type = value.type.replace("input_", "").replace("_", " ").title()
text_substitutions[name] = f"[{var_type}: {name}]"
def replace_variable(match: re.Match[str]) -> str:
var_name = match.group(1).strip()
return str(text_substitutions.get(var_name, match.group(0)))
pattern = r"\{\{\s*(\w+)\s*\}\}"
resolved_prompt_text = re.sub(pattern, replace_variable, cur_prompt_text)
# Insert system message with resolved text
messages.insert(0, OpenAISystemMessageParam(content=resolved_prompt_text))
# If we have media, prepend to first user message
if media_content_parts:
self._prepend_media_into_first_user_message(messages, media_content_parts)
return cur_prompt
def _prepend_media_into_first_user_message(
self, messages: list[OpenAIMessageParam], media_parts: list[OpenAIChatCompletionContentPartParam]
) -> None:
"""Prepend media content parts into the first user message."""
# Find first user message (skip the system message we just added)
first_user_msg_index = None
for i, message in enumerate(messages):
if isinstance(message, OpenAIUserMessageParam):
first_user_msg_index = i
break
if first_user_msg_index is not None:
user_msg = messages[first_user_msg_index]
# Convert string content to parts if needed, otherwise use existing parts directly
if isinstance(user_msg.content, str):
existing_parts = [OpenAIChatCompletionContentPartTextParam(text=user_msg.content)]
else:
existing_parts = user_msg.content
# Prepend media before user's content
combined_parts = media_parts + existing_parts
messages[first_user_msg_index] = OpenAIUserMessageParam(content=combined_parts, name=user_msg.name)
else:
# No user message exists - append one with just media
messages.append(OpenAIUserMessageParam(content=media_parts))
async def get_openai_response( async def get_openai_response(
self, self,
response_id: str, response_id: str,
@ -239,6 +355,7 @@ class OpenAIResponsesImpl:
self, self,
input: str | list[OpenAIResponseInput], input: str | list[OpenAIResponseInput],
model: str, model: str,
prompt: OpenAIResponsePromptParam | None = None,
instructions: str | None = None, instructions: str | None = None,
previous_response_id: str | None = None, previous_response_id: str | None = None,
conversation: str | None = None, conversation: str | None = None,
@ -269,6 +386,7 @@ class OpenAIResponsesImpl:
input=input, input=input,
conversation=conversation, conversation=conversation,
model=model, model=model,
prompt=prompt,
instructions=instructions, instructions=instructions,
previous_response_id=previous_response_id, previous_response_id=previous_response_id,
store=store, store=store,
@ -314,6 +432,7 @@ class OpenAIResponsesImpl:
self, self,
input: str | list[OpenAIResponseInput], input: str | list[OpenAIResponseInput],
model: str, model: str,
prompt: OpenAIResponsePromptParam | None = None,
instructions: str | None = None, instructions: str | None = None,
previous_response_id: str | None = None, previous_response_id: str | None = None,
conversation: str | None = None, conversation: str | None = None,
@ -332,6 +451,9 @@ class OpenAIResponsesImpl:
if instructions: if instructions:
messages.insert(0, OpenAISystemMessageParam(content=instructions)) messages.insert(0, OpenAISystemMessageParam(content=instructions))
# Prepend reusable prompt (if provided)
prompt_obj = await self._prepend_prompt(messages, prompt)
# Structured outputs # Structured outputs
response_format = await convert_response_text_to_chat_response_format(text) response_format = await convert_response_text_to_chat_response_format(text)
@ -354,6 +476,7 @@ class OpenAIResponsesImpl:
ctx=ctx, ctx=ctx,
response_id=response_id, response_id=response_id,
created_at=created_at, created_at=created_at,
prompt=prompt_obj,
text=text, text=text,
max_infer_iters=max_infer_iters, max_infer_iters=max_infer_iters,
tool_executor=self.tool_executor, tool_executor=self.tool_executor,

View file

@ -65,6 +65,7 @@ from llama_stack.apis.inference import (
OpenAIChoice, OpenAIChoice,
OpenAIMessageParam, OpenAIMessageParam,
) )
from llama_stack.apis.prompts.prompts import Prompt
from llama_stack.log import get_logger from llama_stack.log import get_logger
from llama_stack.providers.utils.inference.prompt_adapter import interleaved_content_as_str from llama_stack.providers.utils.inference.prompt_adapter import interleaved_content_as_str
from llama_stack.providers.utils.telemetry import tracing from llama_stack.providers.utils.telemetry import tracing
@ -107,6 +108,7 @@ class StreamingResponseOrchestrator:
ctx: ChatCompletionContext, ctx: ChatCompletionContext,
response_id: str, response_id: str,
created_at: int, created_at: int,
prompt: Prompt | None,
text: OpenAIResponseText, text: OpenAIResponseText,
max_infer_iters: int, max_infer_iters: int,
tool_executor, # Will be the tool execution logic from the main class tool_executor, # Will be the tool execution logic from the main class
@ -118,6 +120,7 @@ class StreamingResponseOrchestrator:
self.ctx = ctx self.ctx = ctx
self.response_id = response_id self.response_id = response_id
self.created_at = created_at self.created_at = created_at
self.prompt = prompt
self.text = text self.text = text
self.max_infer_iters = max_infer_iters self.max_infer_iters = max_infer_iters
self.tool_executor = tool_executor self.tool_executor = tool_executor
@ -175,6 +178,7 @@ class StreamingResponseOrchestrator:
object="response", object="response",
status=status, status=status,
output=self._clone_outputs(outputs), output=self._clone_outputs(outputs),
prompt=self.prompt,
text=self.text, text=self.text,
tools=self.ctx.available_tools(), tools=self.ctx.available_tools(),
error=error, error=error,

View file

@ -5,6 +5,7 @@
# the root directory of this source tree. # the root directory of this source tree.
import asyncio import asyncio
import base64
import re import re
import uuid import uuid
@ -14,6 +15,7 @@ from llama_stack.apis.agents.openai_responses import (
OpenAIResponseInput, OpenAIResponseInput,
OpenAIResponseInputFunctionToolCallOutput, OpenAIResponseInputFunctionToolCallOutput,
OpenAIResponseInputMessageContent, OpenAIResponseInputMessageContent,
OpenAIResponseInputMessageContentFile,
OpenAIResponseInputMessageContentImage, OpenAIResponseInputMessageContentImage,
OpenAIResponseInputMessageContentText, OpenAIResponseInputMessageContentText,
OpenAIResponseInputTool, OpenAIResponseInputTool,
@ -27,6 +29,7 @@ from llama_stack.apis.agents.openai_responses import (
OpenAIResponseOutputMessageMCPListTools, OpenAIResponseOutputMessageMCPListTools,
OpenAIResponseText, OpenAIResponseText,
) )
from llama_stack.apis.files import Files
from llama_stack.apis.inference import ( from llama_stack.apis.inference import (
OpenAIAssistantMessageParam, OpenAIAssistantMessageParam,
OpenAIChatCompletionContentPartImageParam, OpenAIChatCompletionContentPartImageParam,
@ -36,6 +39,8 @@ from llama_stack.apis.inference import (
OpenAIChatCompletionToolCallFunction, OpenAIChatCompletionToolCallFunction,
OpenAIChoice, OpenAIChoice,
OpenAIDeveloperMessageParam, OpenAIDeveloperMessageParam,
OpenAIFile,
OpenAIFileFile,
OpenAIImageURL, OpenAIImageURL,
OpenAIJSONSchema, OpenAIJSONSchema,
OpenAIMessageParam, OpenAIMessageParam,
@ -50,6 +55,49 @@ from llama_stack.apis.inference import (
from llama_stack.apis.safety import Safety from llama_stack.apis.safety import Safety
async def extract_file_content(file_id: str, files_api: Files) -> bytes:
"""
Retrieve file content directly using the Files API.
:param file_id: The file identifier (e.g., "file-abc123")
:param files_api: Files API instance
:returns: Raw file content as bytes
:raises: ValueError if file cannot be retrieved
"""
try:
response = await files_api.openai_retrieve_file_content(file_id)
if hasattr(response, "body"):
return response.body
elif hasattr(response, "content"):
return response.content
else:
raise AttributeError(f"Response object has no 'body' or 'content' attribute. Type: {type(response)}")
except Exception as e:
raise ValueError(f"Failed to retrieve file content for file_id '{file_id}': {str(e)}") from e
def get_mime_type_from_filename(filename: str | None) -> str:
"""
Determine MIME type from filename extension.
:param filename: The filename to analyze
:returns: MIME type string (defaults to "application/octet-stream" if unknown)
"""
if not filename:
return "application/octet-stream"
filename_lower = filename.lower()
if filename_lower.endswith(".pdf"):
return "application/pdf"
elif filename_lower.endswith((".png", ".jpg", ".jpeg")):
ext = filename_lower.split(".")[-1]
return f"image/{ext.replace('jpg', 'jpeg')}"
elif filename_lower.endswith(".txt"):
return "text/plain"
else:
return "application/octet-stream"
async def convert_chat_choice_to_response_message( async def convert_chat_choice_to_response_message(
choice: OpenAIChoice, choice: OpenAIChoice,
citation_files: dict[str, str] | None = None, citation_files: dict[str, str] | None = None,
@ -79,11 +127,15 @@ async def convert_chat_choice_to_response_message(
async def convert_response_content_to_chat_content( async def convert_response_content_to_chat_content(
content: (str | list[OpenAIResponseInputMessageContent] | list[OpenAIResponseOutputMessageContent]), content: (str | list[OpenAIResponseInputMessageContent] | list[OpenAIResponseOutputMessageContent]),
files_api: Files,
) -> str | list[OpenAIChatCompletionContentPartParam]: ) -> str | list[OpenAIChatCompletionContentPartParam]:
""" """
Convert the content parts from an OpenAI Response API request into OpenAI Chat Completion content parts. Convert the content parts from an OpenAI Response API request into OpenAI Chat Completion content parts.
The content schemas of each API look similar, but are not exactly the same. The content schemas of each API look similar, but are not exactly the same.
:param content: The content to convert
:param files_api: Files API for resolving file_id to raw file content (required)
""" """
if isinstance(content, str): if isinstance(content, str):
return content return content
@ -95,9 +147,69 @@ async def convert_response_content_to_chat_content(
elif isinstance(content_part, OpenAIResponseOutputMessageContentOutputText): elif isinstance(content_part, OpenAIResponseOutputMessageContentOutputText):
converted_parts.append(OpenAIChatCompletionContentPartTextParam(text=content_part.text)) converted_parts.append(OpenAIChatCompletionContentPartTextParam(text=content_part.text))
elif isinstance(content_part, OpenAIResponseInputMessageContentImage): elif isinstance(content_part, OpenAIResponseInputMessageContentImage):
detail = content_part.detail
if content_part.image_url: if content_part.image_url:
image_url = OpenAIImageURL(url=content_part.image_url, detail=content_part.detail) image_url = OpenAIImageURL(url=content_part.image_url, detail=detail)
converted_parts.append(OpenAIChatCompletionContentPartImageParam(image_url=image_url)) converted_parts.append(OpenAIChatCompletionContentPartImageParam(image_url=image_url))
elif content_part.file_id:
file_content = await extract_file_content(content_part.file_id, files_api)
encoded_content = base64.b64encode(file_content).decode("utf-8")
data_url = f"data:image/png;base64,{encoded_content}"
image_url = OpenAIImageURL(url=data_url, detail=detail)
converted_parts.append(OpenAIChatCompletionContentPartImageParam(image_url=image_url))
else:
raise ValueError(
f"Image content must have either 'image_url' or 'file_id'. "
f"Got image_url={content_part.image_url}, file_id={content_part.file_id}"
)
elif isinstance(content_part, OpenAIResponseInputMessageContentFile):
file_data = getattr(content_part, "file_data", None)
file_id = getattr(content_part, "file_id", None)
file_url = getattr(content_part, "file_url", None)
filename = getattr(content_part, "filename", None)
if not any([file_id, file_data, file_url]):
raise ValueError(
f"File content must have at least one of 'file_id', 'file_data', or 'file_url'. "
f"Got file_id={file_id}, file_data={'<data>' if file_data else None}, file_url={file_url}"
)
resolved_file_data = None
if file_id:
file_content = await extract_file_content(file_id, files_api)
# If filename is not provided, fetch it from the Files API
if not filename:
file_metadata = await files_api.openai_retrieve_file(file_id)
filename = file_metadata.filename
# Determine MIME type and encode as data URL
mime_type = get_mime_type_from_filename(filename)
base64_content = base64.b64encode(file_content).decode("utf-8")
resolved_file_data = f"data:{mime_type};base64,{base64_content}"
elif file_data:
# If file_data provided directly
if file_data.startswith("data:"):
resolved_file_data = file_data
else:
# Raw base64 data, wrap in data URL format
mime_type = get_mime_type_from_filename(filename)
resolved_file_data = f"data:{mime_type};base64,{file_data}"
elif file_url:
resolved_file_data = file_url
converted_parts.append(
OpenAIFile(
file=OpenAIFileFile(
file_data=resolved_file_data,
filename=filename,
)
)
)
elif isinstance(content_part, str): elif isinstance(content_part, str):
converted_parts.append(OpenAIChatCompletionContentPartTextParam(text=content_part)) converted_parts.append(OpenAIChatCompletionContentPartTextParam(text=content_part))
else: else:
@ -110,12 +222,14 @@ async def convert_response_content_to_chat_content(
async def convert_response_input_to_chat_messages( async def convert_response_input_to_chat_messages(
input: str | list[OpenAIResponseInput], input: str | list[OpenAIResponseInput],
previous_messages: list[OpenAIMessageParam] | None = None, previous_messages: list[OpenAIMessageParam] | None = None,
files_api: Files | None = None,
) -> list[OpenAIMessageParam]: ) -> list[OpenAIMessageParam]:
""" """
Convert the input from an OpenAI Response API request into OpenAI Chat Completion messages. Convert the input from an OpenAI Response API request into OpenAI Chat Completion messages.
:param input: The input to convert :param input: The input to convert
:param previous_messages: Optional previous messages to check for function_call references :param previous_messages: Optional previous messages to check for function_call references
:param files_api: Files API for resolving file_id to raw file content (optional, required for file/image content)
""" """
messages: list[OpenAIMessageParam] = [] messages: list[OpenAIMessageParam] = []
if isinstance(input, list): if isinstance(input, list):
@ -173,7 +287,7 @@ async def convert_response_input_to_chat_messages(
# these are handled by the responses impl itself and not pass through to chat completions # these are handled by the responses impl itself and not pass through to chat completions
pass pass
else: else:
content = await convert_response_content_to_chat_content(input_item.content) content = await convert_response_content_to_chat_content(input_item.content, files_api)
message_type = await get_message_type_by_role(input_item.role) message_type = await get_message_type_by_role(input_item.role)
if message_type is None: if message_type is None:
raise ValueError( raise ValueError(

View file

@ -35,6 +35,8 @@ def available_providers() -> list[ProviderSpec]:
Api.tool_runtime, Api.tool_runtime,
Api.tool_groups, Api.tool_groups,
Api.conversations, Api.conversations,
Api.prompts,
Api.files,
], ],
description="Meta's reference implementation of an agent system that can use tools, access vector databases, and perform complex reasoning tasks.", description="Meta's reference implementation of an agent system that can use tools, access vector databases, and perform complex reasoning tasks.",
), ),

View file

@ -16,7 +16,9 @@ from llama_stack.apis.agents import (
) )
from llama_stack.apis.common.responses import PaginatedResponse from llama_stack.apis.common.responses import PaginatedResponse
from llama_stack.apis.conversations import Conversations from llama_stack.apis.conversations import Conversations
from llama_stack.apis.files import Files
from llama_stack.apis.inference import Inference from llama_stack.apis.inference import Inference
from llama_stack.apis.prompts import Prompts
from llama_stack.apis.safety import Safety from llama_stack.apis.safety import Safety
from llama_stack.apis.tools import ListToolDefsResponse, ToolDef, ToolGroups, ToolRuntime from llama_stack.apis.tools import ListToolDefsResponse, ToolDef, ToolGroups, ToolRuntime
from llama_stack.apis.vector_io import VectorIO from llama_stack.apis.vector_io import VectorIO
@ -49,6 +51,8 @@ def mock_apis():
"tool_runtime_api": AsyncMock(spec=ToolRuntime), "tool_runtime_api": AsyncMock(spec=ToolRuntime),
"tool_groups_api": AsyncMock(spec=ToolGroups), "tool_groups_api": AsyncMock(spec=ToolGroups),
"conversations_api": AsyncMock(spec=Conversations), "conversations_api": AsyncMock(spec=Conversations),
"prompts_api": AsyncMock(spec=Prompts),
"files_api": AsyncMock(spec=Files),
} }
@ -81,7 +85,9 @@ async def agents_impl(config, mock_apis):
mock_apis["tool_runtime_api"], mock_apis["tool_runtime_api"],
mock_apis["tool_groups_api"], mock_apis["tool_groups_api"],
mock_apis["conversations_api"], mock_apis["conversations_api"],
[], mock_apis["prompts_api"],
mock_apis["files_api"],
[], # policy (empty list for tests)
) )
await impl.initialize() await impl.initialize()
yield impl yield impl

View file

@ -40,6 +40,7 @@ from llama_stack.apis.inference import (
OpenAIResponseFormatJSONSchema, OpenAIResponseFormatJSONSchema,
OpenAIUserMessageParam, OpenAIUserMessageParam,
) )
from llama_stack.apis.prompts import Prompt
from llama_stack.apis.tools.tools import ListToolDefsResponse, ToolDef, ToolGroups, ToolInvocationResult, ToolRuntime from llama_stack.apis.tools.tools import ListToolDefsResponse, ToolDef, ToolGroups, ToolInvocationResult, ToolRuntime
from llama_stack.core.access_control.access_control import default_policy from llama_stack.core.access_control.access_control import default_policy
from llama_stack.core.storage.datatypes import ResponsesStoreReference, SqliteSqlStoreConfig from llama_stack.core.storage.datatypes import ResponsesStoreReference, SqliteSqlStoreConfig
@ -97,6 +98,19 @@ def mock_safety_api():
return safety_api return safety_api
@pytest.fixture
def mock_prompts_api():
prompts_api = AsyncMock()
return prompts_api
@pytest.fixture
def mock_files_api():
"""Mock files API for testing."""
files_api = AsyncMock()
return files_api
@pytest.fixture @pytest.fixture
def openai_responses_impl( def openai_responses_impl(
mock_inference_api, mock_inference_api,
@ -106,6 +120,8 @@ def openai_responses_impl(
mock_vector_io_api, mock_vector_io_api,
mock_safety_api, mock_safety_api,
mock_conversations_api, mock_conversations_api,
mock_prompts_api,
mock_files_api,
): ):
return OpenAIResponsesImpl( return OpenAIResponsesImpl(
inference_api=mock_inference_api, inference_api=mock_inference_api,
@ -115,6 +131,8 @@ def openai_responses_impl(
vector_io_api=mock_vector_io_api, vector_io_api=mock_vector_io_api,
safety_api=mock_safety_api, safety_api=mock_safety_api,
conversations_api=mock_conversations_api, conversations_api=mock_conversations_api,
prompts_api=mock_prompts_api,
files_api=mock_files_api,
) )
@ -498,7 +516,7 @@ async def test_create_openai_response_with_tool_call_function_arguments_none(ope
mock_inference_api.openai_chat_completion.return_value = fake_stream_toolcall() mock_inference_api.openai_chat_completion.return_value = fake_stream_toolcall()
async def test_create_openai_response_with_multiple_messages(openai_responses_impl, mock_inference_api): async def test_create_openai_response_with_multiple_messages(openai_responses_impl, mock_inference_api, mock_files_api):
"""Test creating an OpenAI response with multiple messages.""" """Test creating an OpenAI response with multiple messages."""
# Setup # Setup
input_messages = [ input_messages = [
@ -709,7 +727,7 @@ async def test_create_openai_response_with_instructions(openai_responses_impl, m
async def test_create_openai_response_with_instructions_and_multiple_messages( async def test_create_openai_response_with_instructions_and_multiple_messages(
openai_responses_impl, mock_inference_api openai_responses_impl, mock_inference_api, mock_files_api
): ):
# Setup # Setup
input_messages = [ input_messages = [
@ -1169,3 +1187,657 @@ async def test_create_openai_response_with_invalid_text_format(openai_responses_
model=model, model=model,
text=OpenAIResponseText(format={"type": "invalid"}), text=OpenAIResponseText(format={"type": "invalid"}),
) )
async def test_create_openai_response_with_prompt(openai_responses_impl, mock_inference_api, mock_prompts_api):
"""Test creating an OpenAI response with a prompt."""
input_text = "What is the capital of Ireland?"
model = "meta-llama/Llama-3.1-8B-Instruct"
prompt_id = "pmpt_1234567890abcdef1234567890abcdef1234567890abcdef"
prompt = Prompt(
prompt="You are a helpful {{ area_name }} assistant at {{ company_name }}. Always provide accurate information.",
prompt_id=prompt_id,
version=1,
variables=["area_name", "company_name"],
is_default=True,
)
from llama_stack.apis.agents.openai_responses import (
OpenAIResponseInputMessageContentText,
OpenAIResponsePromptParam,
)
prompt_params_with_version_1 = OpenAIResponsePromptParam(
id=prompt_id,
version="1",
variables={
"area_name": OpenAIResponseInputMessageContentText(text="geography"),
"company_name": OpenAIResponseInputMessageContentText(text="Dummy Company"),
},
)
mock_prompts_api.get_prompt.return_value = prompt
mock_inference_api.openai_chat_completion.return_value = fake_stream()
result = await openai_responses_impl.create_openai_response(
input=input_text,
model=model,
prompt=prompt_params_with_version_1,
)
mock_prompts_api.get_prompt.assert_called_with(prompt_id, 1)
mock_inference_api.openai_chat_completion.assert_called()
call_args = mock_inference_api.openai_chat_completion.call_args
sent_messages = call_args.args[0].messages
assert len(sent_messages) == 2
system_messages = [msg for msg in sent_messages if msg.role == "system"]
assert len(system_messages) == 1
assert (
system_messages[0].content
== "You are a helpful geography assistant at Dummy Company. Always provide accurate information."
)
user_messages = [msg for msg in sent_messages if msg.role == "user"]
assert len(user_messages) == 1
assert user_messages[0].content == input_text
assert result.model == model
assert result.status == "completed"
assert result.prompt.prompt_id == prompt_id
assert result.prompt.variables == ["area_name", "company_name"]
assert result.prompt.version == 1
assert result.prompt.prompt == prompt.prompt
async def test_prepend_prompt_successful_without_variables(openai_responses_impl, mock_prompts_api):
"""Test prepend_prompt function without variables."""
# Setup
prompt_id = "pmpt_1234567890abcdef1234567890abcdef1234567890abcdef"
prompt = Prompt(
prompt="You are a helpful assistant. Always provide accurate information.",
prompt_id=prompt_id,
version=1,
variables=[],
is_default=True,
)
from llama_stack.apis.agents.openai_responses import OpenAIResponsePromptParam
from llama_stack.apis.inference import OpenAISystemMessageParam, OpenAIUserMessageParam
prompt_params = OpenAIResponsePromptParam(id=prompt_id, version="1")
mock_prompts_api.get_prompt.return_value = prompt
# Initial messages
messages = [OpenAIUserMessageParam(content="Hello")]
# Execute
result = await openai_responses_impl._prepend_prompt(messages, prompt_params)
# Verify
mock_prompts_api.get_prompt.assert_called_once_with(prompt_id, 1)
# Check that prompt was returned
assert result == prompt
# Check that system message was prepended
assert len(messages) == 2
assert isinstance(messages[0], OpenAISystemMessageParam)
assert messages[0].content == "You are a helpful assistant. Always provide accurate information."
async def test_prepend_prompt_no_version_specified(openai_responses_impl, mock_prompts_api):
"""Test prepend_prompt function when no version is specified (should use None)."""
# Setup
prompt_id = "pmpt_1234567890abcdef1234567890abcdef1234567890abcdef"
prompt = Prompt(
prompt="Default prompt text.",
prompt_id=prompt_id,
version=3,
variables=[],
is_default=True,
)
from llama_stack.apis.agents.openai_responses import OpenAIResponsePromptParam
from llama_stack.apis.inference import OpenAIUserMessageParam
prompt_params = OpenAIResponsePromptParam(id=prompt_id) # No version specified
mock_prompts_api.get_prompt.return_value = prompt
# Initial messages
messages = [OpenAIUserMessageParam(content="Test")]
# Execute
result = await openai_responses_impl._prepend_prompt(messages, prompt_params)
# Verify
mock_prompts_api.get_prompt.assert_called_once_with(prompt_id, None)
assert result == prompt
assert len(messages) == 2
async def test_prepend_prompt_invalid_variable(openai_responses_impl, mock_prompts_api):
"""Test error handling in prepend_prompt function when prompt parameters contain invalid variables."""
# Setup
prompt_id = "pmpt_1234567890abcdef1234567890abcdef1234567890abcdef"
prompt = Prompt(
prompt="You are a {{ role }} assistant.",
prompt_id=prompt_id,
version=1,
variables=["role"], # Only "role" is valid
is_default=True,
)
from llama_stack.apis.agents.openai_responses import (
OpenAIResponseInputMessageContentText,
OpenAIResponsePromptParam,
)
from llama_stack.apis.inference import OpenAIUserMessageParam
prompt_params = OpenAIResponsePromptParam(
id=prompt_id,
version="1",
variables={
"role": OpenAIResponseInputMessageContentText(text="helpful"),
"company": OpenAIResponseInputMessageContentText(
text="Dummy Company"
), # company is not in prompt.variables
},
)
mock_prompts_api.get_prompt.return_value = prompt
# Initial messages
messages = [OpenAIUserMessageParam(content="Test prompt")]
# Execute - should raise ValueError for invalid variable
with pytest.raises(ValueError, match="Variable company not found in prompt"):
await openai_responses_impl._prepend_prompt(messages, prompt_params)
# Verify
mock_prompts_api.get_prompt.assert_called_once_with(prompt_id, 1)
async def test_prepend_prompt_not_found(openai_responses_impl, mock_prompts_api):
"""Test prepend_prompt function when prompt is not found."""
# Setup
prompt_id = "pmpt_nonexistent"
from llama_stack.apis.agents.openai_responses import OpenAIResponsePromptParam
from llama_stack.apis.inference import OpenAIUserMessageParam
prompt_params = OpenAIResponsePromptParam(id=prompt_id, version="1")
mock_prompts_api.get_prompt.return_value = None # Prompt not found
# Initial messages
messages = [OpenAIUserMessageParam(content="Test prompt")]
initial_length = len(messages)
# Execute
result = await openai_responses_impl._prepend_prompt(messages, prompt_params)
# Verify
mock_prompts_api.get_prompt.assert_called_once_with(prompt_id, 1)
# Should return None when prompt not found
assert result is None
# Messages should not be modified
assert len(messages) == initial_length
assert messages[0].content == "Test prompt"
async def test_prepend_prompt_no_params(openai_responses_impl, mock_prompts_api):
"""Test handling in prepend_prompt function when prompt_params is None."""
# Setup
from llama_stack.apis.inference import OpenAIUserMessageParam
messages = [OpenAIUserMessageParam(content="Test")]
initial_length = len(messages)
# Execute
result = await openai_responses_impl._prepend_prompt(messages, None)
# Verify
mock_prompts_api.get_prompt.assert_not_called()
# Should return None when no prompt params
assert result is None
# Messages should not be modified
assert len(messages) == initial_length
async def test_prepend_prompt_variable_substitution(openai_responses_impl, mock_prompts_api):
"""Test complex variable substitution with multiple occurrences and special characters in prepend_prompt function."""
# Setup
prompt_id = "pmpt_1234567890abcdef1234567890abcdef1234567890abcdef"
# Support all whitespace variations: {{name}}, {{ name }}, {{ name}}, {{name }}, etc.
prompt = Prompt(
prompt="Hello {{name}}! You are working at {{ company}}. Your role is {{role}} at {{company}}. Remember, {{ name }}, to be {{ tone }}.",
prompt_id=prompt_id,
version=1,
variables=["name", "company", "role", "tone"],
is_default=True,
)
from llama_stack.apis.agents.openai_responses import (
OpenAIResponseInputMessageContentText,
OpenAIResponsePromptParam,
)
from llama_stack.apis.inference import OpenAISystemMessageParam, OpenAIUserMessageParam
prompt_params = OpenAIResponsePromptParam(
id=prompt_id,
version="1",
variables={
"name": OpenAIResponseInputMessageContentText(text="Alice"),
"company": OpenAIResponseInputMessageContentText(text="Dummy Company"),
"role": OpenAIResponseInputMessageContentText(text="AI Assistant"),
"tone": OpenAIResponseInputMessageContentText(text="professional"),
},
)
mock_prompts_api.get_prompt.return_value = prompt
# Initial messages
messages = [OpenAIUserMessageParam(content="Test")]
# Execute
result = await openai_responses_impl._prepend_prompt(messages, prompt_params)
# Verify
assert result == prompt
assert len(messages) == 2
assert isinstance(messages[0], OpenAISystemMessageParam)
expected_content = "Hello Alice! You are working at Dummy Company. Your role is AI Assistant at Dummy Company. Remember, Alice, to be professional."
assert messages[0].content == expected_content
async def test_prepend_prompt_with_image_variable(openai_responses_impl, mock_prompts_api, mock_files_api):
"""Test prepend_prompt with image variable - should create placeholder in system message and inject image into user message."""
# Setup
prompt_id = "pmpt_1234567890abcdef1234567890abcdef1234567890abcdef"
prompt = Prompt(
prompt="Analyze this {{product_image}} and describe what you see.",
prompt_id=prompt_id,
version=1,
variables=["product_image"],
is_default=True,
)
from llama_stack.apis.agents.openai_responses import (
OpenAIResponseInputMessageContentImage,
OpenAIResponsePromptParam,
)
from llama_stack.apis.inference import (
OpenAIChatCompletionContentPartImageParam,
OpenAISystemMessageParam,
OpenAIUserMessageParam,
)
# Mock file content
mock_file_content = b"fake_image_data"
mock_files_api.openai_retrieve_file_content.return_value = type("obj", (object,), {"body": mock_file_content})()
prompt_params = OpenAIResponsePromptParam(
id=prompt_id,
version="1",
variables={
"product_image": OpenAIResponseInputMessageContentImage(
file_id="file-abc123",
detail="high",
)
},
)
mock_prompts_api.get_prompt.return_value = prompt
# Initial messages
messages = [OpenAIUserMessageParam(content="What do you think?")]
# Execute
result = await openai_responses_impl._prepend_prompt(messages, prompt_params)
# Verify
assert result == prompt
assert len(messages) == 2
# Check system message has placeholder
assert isinstance(messages[0], OpenAISystemMessageParam)
assert messages[0].content == "Analyze this [Image: product_image] and describe what you see."
# Check user message has image prepended
assert isinstance(messages[1], OpenAIUserMessageParam)
assert isinstance(messages[1].content, list)
assert len(messages[1].content) == 2 # Image + original text
# First part should be image with data URL
assert isinstance(messages[1].content[0], OpenAIChatCompletionContentPartImageParam)
assert messages[1].content[0].image_url.url.startswith("data:image/")
assert messages[1].content[0].image_url.detail == "high"
# Second part should be original text
assert messages[1].content[1].text == "What do you think?"
async def test_prepend_prompt_with_file_variable(openai_responses_impl, mock_prompts_api, mock_files_api):
"""Test prepend_prompt with file variable - should create placeholder in system message and inject file into user message."""
# Setup
prompt_id = "pmpt_1234567890abcdef1234567890abcdef1234567890abcdef"
prompt = Prompt(
prompt="Review the document {{contract_file}} and summarize key points.",
prompt_id=prompt_id,
version=1,
variables=["contract_file"],
is_default=True,
)
from llama_stack.apis.agents.openai_responses import (
OpenAIResponseInputMessageContentFile,
OpenAIResponsePromptParam,
)
from llama_stack.apis.files import OpenAIFileObject
from llama_stack.apis.inference import (
OpenAIFile,
OpenAISystemMessageParam,
OpenAIUserMessageParam,
)
# Mock file retrieval
mock_file_content = b"fake_pdf_content"
mock_files_api.openai_retrieve_file_content.return_value = type("obj", (object,), {"body": mock_file_content})()
mock_files_api.openai_retrieve_file.return_value = OpenAIFileObject(
object="file",
id="file-contract-789",
bytes=len(mock_file_content),
created_at=1234567890,
expires_at=1234567890,
filename="contract.pdf",
purpose="assistants",
)
prompt_params = OpenAIResponsePromptParam(
id=prompt_id,
version="1",
variables={
"contract_file": OpenAIResponseInputMessageContentFile(
file_id="file-contract-789",
filename="contract.pdf",
)
},
)
mock_prompts_api.get_prompt.return_value = prompt
# Initial messages
messages = [OpenAIUserMessageParam(content="Please review this.")]
# Execute
result = await openai_responses_impl._prepend_prompt(messages, prompt_params)
# Verify
assert result == prompt
assert len(messages) == 2
# Check system message has placeholder
assert isinstance(messages[0], OpenAISystemMessageParam)
assert messages[0].content == "Review the document [File: contract_file] and summarize key points."
# Check user message has file prepended
assert isinstance(messages[1], OpenAIUserMessageParam)
assert isinstance(messages[1].content, list)
assert len(messages[1].content) == 2 # File + original text
# First part should be file with data URL (not file_id)
assert isinstance(messages[1].content[0], OpenAIFile)
assert messages[1].content[0].file.file_data.startswith("data:application/pdf;base64,")
assert messages[1].content[0].file.filename == "contract.pdf"
# file_id should NOT be set in the OpenAI request
assert messages[1].content[0].file.file_id is None
# Second part should be original text
assert messages[1].content[1].text == "Please review this."
async def test_prepend_prompt_with_mixed_variables(openai_responses_impl, mock_prompts_api, mock_files_api):
"""Test prepend_prompt with text, image, and file variables mixed together."""
# Setup
prompt_id = "pmpt_1234567890abcdef1234567890abcdef1234567890abcdef"
prompt = Prompt(
prompt="Hello {{name}}! Analyze {{photo}} and review {{document}}. Provide insights for {{company}}.",
prompt_id=prompt_id,
version=1,
variables=["name", "photo", "document", "company"],
is_default=True,
)
from llama_stack.apis.agents.openai_responses import (
OpenAIResponseInputMessageContentFile,
OpenAIResponseInputMessageContentImage,
OpenAIResponseInputMessageContentText,
OpenAIResponsePromptParam,
)
from llama_stack.apis.files import OpenAIFileObject
from llama_stack.apis.inference import (
OpenAIChatCompletionContentPartImageParam,
OpenAIFile,
OpenAISystemMessageParam,
OpenAIUserMessageParam,
)
# Mock file retrieval for document
mock_file_content = b"fake_doc_content"
mock_files_api.openai_retrieve_file_content.return_value = type("obj", (object,), {"body": mock_file_content})()
mock_files_api.openai_retrieve_file.return_value = OpenAIFileObject(
object="file",
id="file-doc-456",
bytes=len(mock_file_content),
created_at=1234567890,
expires_at=1234567890,
filename="doc.pdf",
purpose="assistants",
)
prompt_params = OpenAIResponsePromptParam(
id=prompt_id,
version="1",
variables={
"name": OpenAIResponseInputMessageContentText(text="Alice"),
"photo": OpenAIResponseInputMessageContentImage(file_id="file-photo-123", detail="auto"),
"document": OpenAIResponseInputMessageContentFile(file_id="file-doc-456", filename="doc.pdf"),
"company": OpenAIResponseInputMessageContentText(text="Acme Corp"),
},
)
mock_prompts_api.get_prompt.return_value = prompt
# Initial messages
messages = [OpenAIUserMessageParam(content="Here's my question.")]
# Execute
result = await openai_responses_impl._prepend_prompt(messages, prompt_params)
# Verify
assert result == prompt
assert len(messages) == 2
# Check system message has text and placeholders
assert isinstance(messages[0], OpenAISystemMessageParam)
expected_system = "Hello Alice! Analyze [Image: photo] and review [File: document]. Provide insights for Acme Corp."
assert messages[0].content == expected_system
# Check user message has media prepended (2 media items + original text)
assert isinstance(messages[1], OpenAIUserMessageParam)
assert isinstance(messages[1].content, list)
assert len(messages[1].content) == 3 # Image + File + original text
# First part should be image with data URL
assert isinstance(messages[1].content[0], OpenAIChatCompletionContentPartImageParam)
assert messages[1].content[0].image_url.url.startswith("data:image/")
# Second part should be file with data URL
assert isinstance(messages[1].content[1], OpenAIFile)
assert messages[1].content[1].file.file_data.startswith("data:application/pdf;base64,")
assert messages[1].content[1].file.filename == "doc.pdf"
assert messages[1].content[1].file.file_id is None # file_id should NOT be sent
# Third part should be original text
assert messages[1].content[2].text == "Here's my question."
async def test_prepend_prompt_with_image_using_image_url(openai_responses_impl, mock_prompts_api):
"""Test prepend_prompt with image variable using image_url instead of file_id."""
# Setup
prompt_id = "pmpt_1234567890abcdef1234567890abcdef1234567890abcdef"
prompt = Prompt(
prompt="Describe {{screenshot}}.",
prompt_id=prompt_id,
version=1,
variables=["screenshot"],
is_default=True,
)
from llama_stack.apis.agents.openai_responses import (
OpenAIResponseInputMessageContentImage,
OpenAIResponsePromptParam,
)
from llama_stack.apis.inference import (
OpenAIChatCompletionContentPartImageParam,
OpenAISystemMessageParam,
OpenAIUserMessageParam,
)
prompt_params = OpenAIResponsePromptParam(
id=prompt_id,
version="1",
variables={
"screenshot": OpenAIResponseInputMessageContentImage(
image_url="https://example.com/screenshot.png",
detail="low",
)
},
)
mock_prompts_api.get_prompt.return_value = prompt
# Initial messages
messages = [OpenAIUserMessageParam(content="What is this?")]
# Execute
result = await openai_responses_impl._prepend_prompt(messages, prompt_params)
# Verify
assert result == prompt
assert len(messages) == 2
# Check system message has placeholder
assert isinstance(messages[0], OpenAISystemMessageParam)
assert messages[0].content == "Describe [Image: screenshot]."
# Check user message has image with URL
assert isinstance(messages[1], OpenAIUserMessageParam)
assert isinstance(messages[1].content, list)
# Image should use the provided URL
assert isinstance(messages[1].content[0], OpenAIChatCompletionContentPartImageParam)
assert messages[1].content[0].image_url.url == "https://example.com/screenshot.png"
assert messages[1].content[0].image_url.detail == "low"
async def test_prepend_prompt_with_media_no_user_message(openai_responses_impl, mock_prompts_api, mock_files_api):
"""Test prepend_prompt with media when there's no existing user message - should create one."""
# Setup
prompt_id = "pmpt_1234567890abcdef1234567890abcdef1234567890abcdef"
prompt = Prompt(
prompt="Analyze {{image}}.",
prompt_id=prompt_id,
version=1,
variables=["image"],
is_default=True,
)
from llama_stack.apis.agents.openai_responses import (
OpenAIResponseInputMessageContentImage,
OpenAIResponsePromptParam,
)
from llama_stack.apis.inference import (
OpenAIAssistantMessageParam,
OpenAIChatCompletionContentPartImageParam,
OpenAISystemMessageParam,
OpenAIUserMessageParam,
)
# Mock file content
mock_file_content = b"fake_image_data"
mock_files_api.openai_retrieve_file_content.return_value = type("obj", (object,), {"body": mock_file_content})()
prompt_params = OpenAIResponsePromptParam(
id=prompt_id,
version="1",
variables={"image": OpenAIResponseInputMessageContentImage(file_id="file-img-999")},
)
mock_prompts_api.get_prompt.return_value = prompt
# Initial messages - only assistant message, no user message
messages = [OpenAIAssistantMessageParam(content="Previous response")]
# Execute
result = await openai_responses_impl._prepend_prompt(messages, prompt_params)
# Verify
assert result == prompt
assert len(messages) == 3 # System + Assistant + New User
# Check system message
assert isinstance(messages[0], OpenAISystemMessageParam)
assert messages[0].content == "Analyze [Image: image]."
# Original assistant message should still be there
assert isinstance(messages[1], OpenAIAssistantMessageParam)
assert messages[1].content == "Previous response"
# New user message with just the image should be appended
assert isinstance(messages[2], OpenAIUserMessageParam)
assert isinstance(messages[2].content, list)
assert len(messages[2].content) == 1
assert isinstance(messages[2].content[0], OpenAIChatCompletionContentPartImageParam)
assert messages[2].content[0].image_url.url.startswith("data:image/")
async def test_prepend_prompt_image_variable_missing_required_fields(openai_responses_impl, mock_prompts_api):
"""Test prepend_prompt with image variable that has neither file_id nor image_url - should raise error."""
# Setup
prompt_id = "pmpt_1234567890abcdef1234567890abcdef1234567890abcdef"
prompt = Prompt(
prompt="Analyze {{bad_image}}.",
prompt_id=prompt_id,
version=1,
variables=["bad_image"],
is_default=True,
)
from llama_stack.apis.agents.openai_responses import (
OpenAIResponseInputMessageContentImage,
OpenAIResponsePromptParam,
)
from llama_stack.apis.inference import OpenAIUserMessageParam
# Create image content with neither file_id nor image_url
prompt_params = OpenAIResponsePromptParam(
id=prompt_id,
version="1",
variables={"bad_image": OpenAIResponseInputMessageContentImage()}, # No file_id or image_url
)
mock_prompts_api.get_prompt.return_value = prompt
messages = [OpenAIUserMessageParam(content="Test")]
# Execute - should raise ValueError
with pytest.raises(ValueError, match="Image content must have either 'image_url' or 'file_id'"):
await openai_responses_impl._prepend_prompt(messages, prompt_params)

View file

@ -39,6 +39,8 @@ def responses_impl_with_conversations(
mock_vector_io_api, mock_vector_io_api,
mock_conversations_api, mock_conversations_api,
mock_safety_api, mock_safety_api,
mock_prompts_api,
mock_files_api,
): ):
"""Create OpenAIResponsesImpl instance with conversations API.""" """Create OpenAIResponsesImpl instance with conversations API."""
return OpenAIResponsesImpl( return OpenAIResponsesImpl(
@ -49,6 +51,8 @@ def responses_impl_with_conversations(
vector_io_api=mock_vector_io_api, vector_io_api=mock_vector_io_api,
conversations_api=mock_conversations_api, conversations_api=mock_conversations_api,
safety_api=mock_safety_api, safety_api=mock_safety_api,
prompts_api=mock_prompts_api,
files_api=mock_files_api,
) )

View file

@ -5,6 +5,8 @@
# the root directory of this source tree. # the root directory of this source tree.
from unittest.mock import AsyncMock
import pytest import pytest
from llama_stack.apis.agents.openai_responses import ( from llama_stack.apis.agents.openai_responses import (
@ -46,6 +48,12 @@ from llama_stack.providers.inline.agents.meta_reference.responses.utils import (
) )
@pytest.fixture
def mock_files_api():
"""Mock files API for testing."""
return AsyncMock()
class TestConvertChatChoiceToResponseMessage: class TestConvertChatChoiceToResponseMessage:
async def test_convert_string_content(self): async def test_convert_string_content(self):
choice = OpenAIChoice( choice = OpenAIChoice(
@ -78,17 +86,17 @@ class TestConvertChatChoiceToResponseMessage:
class TestConvertResponseContentToChatContent: class TestConvertResponseContentToChatContent:
async def test_convert_string_content(self): async def test_convert_string_content(self, mock_files_api):
result = await convert_response_content_to_chat_content("Simple string") result = await convert_response_content_to_chat_content("Simple string", mock_files_api)
assert result == "Simple string" assert result == "Simple string"
async def test_convert_text_content_parts(self): async def test_convert_text_content_parts(self, mock_files_api):
content = [ content = [
OpenAIResponseInputMessageContentText(text="First part"), OpenAIResponseInputMessageContentText(text="First part"),
OpenAIResponseOutputMessageContentOutputText(text="Second part"), OpenAIResponseOutputMessageContentOutputText(text="Second part"),
] ]
result = await convert_response_content_to_chat_content(content) result = await convert_response_content_to_chat_content(content, mock_files_api)
assert len(result) == 2 assert len(result) == 2
assert isinstance(result[0], OpenAIChatCompletionContentPartTextParam) assert isinstance(result[0], OpenAIChatCompletionContentPartTextParam)
@ -96,10 +104,10 @@ class TestConvertResponseContentToChatContent:
assert isinstance(result[1], OpenAIChatCompletionContentPartTextParam) assert isinstance(result[1], OpenAIChatCompletionContentPartTextParam)
assert result[1].text == "Second part" assert result[1].text == "Second part"
async def test_convert_image_content(self): async def test_convert_image_content(self, mock_files_api):
content = [OpenAIResponseInputMessageContentImage(image_url="https://example.com/image.jpg", detail="high")] content = [OpenAIResponseInputMessageContentImage(image_url="https://example.com/image.jpg", detail="high")]
result = await convert_response_content_to_chat_content(content) result = await convert_response_content_to_chat_content(content, mock_files_api)
assert len(result) == 1 assert len(result) == 1
assert isinstance(result[0], OpenAIChatCompletionContentPartImageParam) assert isinstance(result[0], OpenAIChatCompletionContentPartImageParam)

View file

@ -30,6 +30,8 @@ def mock_apis():
"vector_io_api": AsyncMock(), "vector_io_api": AsyncMock(),
"conversations_api": AsyncMock(), "conversations_api": AsyncMock(),
"safety_api": AsyncMock(), "safety_api": AsyncMock(),
"prompts_api": AsyncMock(),
"files_api": AsyncMock(),
} }