feat: add function tools to openai responses

This commit is contained in:
Ashwin Bharambe 2025-04-30 13:06:33 -07:00
parent 5a2bfd6ad5
commit 248a4a3f72
5 changed files with 220 additions and 3 deletions

View file

@ -6405,6 +6405,113 @@
"title": "OpenAIResponseInputMessageContentText" "title": "OpenAIResponseInputMessageContentText"
}, },
"OpenAIResponseInputTool": { "OpenAIResponseInputTool": {
"oneOf": [
{
"$ref": "#/components/schemas/OpenAIResponseInputToolWebSearch"
},
{
"$ref": "#/components/schemas/OpenAIResponseInputToolFileSearch"
},
{
"$ref": "#/components/schemas/OpenAIResponseInputToolFunction"
}
],
"discriminator": {
"propertyName": "type",
"mapping": {
"web_search": "#/components/schemas/OpenAIResponseInputToolWebSearch",
"file_search": "#/components/schemas/OpenAIResponseInputToolFileSearch",
"function": "#/components/schemas/OpenAIResponseInputToolFunction"
}
}
},
"OpenAIResponseInputToolFileSearch": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "file_search",
"default": "file_search"
},
"vector_store_id": {
"type": "array",
"items": {
"type": "string"
}
},
"ranking_options": {
"type": "object",
"properties": {
"ranker": {
"type": "string"
},
"score_threshold": {
"type": "number",
"default": 0.0
}
},
"additionalProperties": false,
"title": "FileSearchRankingOptions"
}
},
"additionalProperties": false,
"required": [
"type",
"vector_store_id"
],
"title": "OpenAIResponseInputToolFileSearch"
},
"OpenAIResponseInputToolFunction": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "function",
"default": "function"
},
"name": {
"type": "string"
},
"description": {
"type": "string"
},
"parameters": {
"type": "object",
"additionalProperties": {
"oneOf": [
{
"type": "null"
},
{
"type": "boolean"
},
{
"type": "number"
},
{
"type": "string"
},
{
"type": "array"
},
{
"type": "object"
}
]
}
},
"strict": {
"type": "boolean"
}
},
"additionalProperties": false,
"required": [
"type",
"name"
],
"title": "OpenAIResponseInputToolFunction"
},
"OpenAIResponseInputToolWebSearch": {
"type": "object", "type": "object",
"properties": { "properties": {
"type": { "type": {

View file

@ -4467,6 +4467,71 @@ components:
- type - type
title: OpenAIResponseInputMessageContentText title: OpenAIResponseInputMessageContentText
OpenAIResponseInputTool: OpenAIResponseInputTool:
oneOf:
- $ref: '#/components/schemas/OpenAIResponseInputToolWebSearch'
- $ref: '#/components/schemas/OpenAIResponseInputToolFileSearch'
- $ref: '#/components/schemas/OpenAIResponseInputToolFunction'
discriminator:
propertyName: type
mapping:
web_search: '#/components/schemas/OpenAIResponseInputToolWebSearch'
file_search: '#/components/schemas/OpenAIResponseInputToolFileSearch'
function: '#/components/schemas/OpenAIResponseInputToolFunction'
OpenAIResponseInputToolFileSearch:
type: object
properties:
type:
type: string
const: file_search
default: file_search
vector_store_id:
type: array
items:
type: string
ranking_options:
type: object
properties:
ranker:
type: string
score_threshold:
type: number
default: 0.0
additionalProperties: false
title: FileSearchRankingOptions
additionalProperties: false
required:
- type
- vector_store_id
title: OpenAIResponseInputToolFileSearch
OpenAIResponseInputToolFunction:
type: object
properties:
type:
type: string
const: function
default: function
name:
type: string
description:
type: string
parameters:
type: object
additionalProperties:
oneOf:
- type: 'null'
- type: boolean
- type: number
- type: string
- type: array
- type: object
strict:
type: boolean
additionalProperties: false
required:
- type
- name
title: OpenAIResponseInputToolFunction
OpenAIResponseInputToolWebSearch:
type: object type: object
properties: properties:
type: type:

View file

@ -4,7 +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.
from typing import List, Literal, Optional, Union from typing import Any, Dict, List, Literal, Optional, Union
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from typing_extensions import Annotated from typing_extensions import Annotated
@ -133,8 +133,30 @@ class OpenAIResponseInputToolWebSearch(BaseModel):
# TODO: add user_location # TODO: add user_location
@json_schema_type
class OpenAIResponseInputToolFunction(BaseModel):
type: Literal["function"] = "function"
name: str
description: Optional[str] = None
parameters: Optional[Dict[str, Any]]
strict: Optional[bool]
class FileSearchRankingOptions(BaseModel):
ranker: Optional[str] = None
score_threshold: Optional[float] = Field(default=0.0, ge=0.0, le=1.0)
@json_schema_type
class OpenAIResponseInputToolFileSearch(BaseModel):
type: Literal["file_search"] = "file_search"
vector_store_id: List[str]
ranking_options: Optional[FileSearchRankingOptions] = None
# TODO: add filters
OpenAIResponseInputTool = Annotated[ OpenAIResponseInputTool = Annotated[
Union[OpenAIResponseInputToolWebSearch,], Union[OpenAIResponseInputToolWebSearch, OpenAIResponseInputToolFileSearch, OpenAIResponseInputToolFunction],
Field(discriminator="type"), Field(discriminator="type"),
] ]
register_schema(OpenAIResponseInputTool, name="OpenAIResponseInputTool") register_schema(OpenAIResponseInputTool, name="OpenAIResponseInputTool")

View file

@ -17,6 +17,7 @@ from importlib.metadata import version as parse_version
from pathlib import Path from pathlib import Path
from typing import Any, List, Optional, Union from typing import Any, List, Optional, Union
import rich.pretty
import yaml import yaml
from fastapi import Body, FastAPI, HTTPException, Request from fastapi import Body, FastAPI, HTTPException, Request
from fastapi import Path as FastapiPath from fastapi import Path as FastapiPath
@ -187,11 +188,30 @@ async def sse_generator(event_gen_coroutine):
) )
async def log_request_pre_validation(request: Request):
if request.method in ("POST", "PUT", "PATCH"):
try:
body_bytes = await request.body()
if body_bytes:
try:
parsed_body = json.loads(body_bytes.decode())
log_output = rich.pretty.pretty_repr(parsed_body)
except (json.JSONDecodeError, UnicodeDecodeError):
log_output = repr(body_bytes)
logger.debug(f"Incoming raw request body for {request.method} {request.url.path}:\n{log_output}")
else:
logger.debug(f"Incoming {request.method} {request.url.path} request with empty body.")
except Exception as e:
logger.warning(f"Could not read or log request body for {request.method} {request.url.path}: {e}")
def create_dynamic_typed_route(func: Any, method: str, route: str): def create_dynamic_typed_route(func: Any, method: str, route: str):
async def endpoint(request: Request, **kwargs): async def endpoint(request: Request, **kwargs):
# Get auth attributes from the request scope # Get auth attributes from the request scope
user_attributes = request.scope.get("user_attributes", {}) user_attributes = request.scope.get("user_attributes", {})
await log_request_pre_validation(request)
# Use context manager with both provider data and auth attributes # Use context manager with both provider data and auth attributes
with request_provider_data_context(request.headers, user_attributes): with request_provider_data_context(request.headers, user_attributes):
is_streaming = is_streaming_request(func.__name__, request, **kwargs) is_streaming = is_streaming_request(func.__name__, request, **kwargs)

View file

@ -192,6 +192,7 @@ class OpenAIResponsesImpl:
status="completed", status="completed",
output=output_messages, output=output_messages,
) )
logger.debug(f"OpenAI Responses response: {response}")
if store: if store:
# Store in kvstore # Store in kvstore
@ -218,7 +219,9 @@ class OpenAIResponsesImpl:
chat_tools: List[ChatCompletionToolParam] = [] chat_tools: List[ChatCompletionToolParam] = []
for input_tool in tools: for input_tool in tools:
# TODO: Handle other tool types # TODO: Handle other tool types
if input_tool.type == "web_search": if input_tool.type == "function":
chat_tools.append(ChatCompletionToolParam(type="function", function=input_tool.model_dump()))
elif input_tool.type == "web_search":
tool_name = "web_search" tool_name = "web_search"
tool = await self.tool_groups_api.get_tool(tool_name) tool = await self.tool_groups_api.get_tool(tool_name)
tool_def = ToolDefinition( tool_def = ToolDefinition(