Repository: openai/openai-agents-python

Issue:
example @function_tool with optional parameters
I'm struggling to find a way to declare a function which has an optional parameter. Ideally, I want something like this. Let me know what's the way, if you don't mind!

```python

@function_tool
def get_latest_elasticsearch_version(major_version: int | None = None) -> str:
    """Returns the latest GA version of Elasticsearch in "X.Y.Z" format.

    Args:
        major_version: Major version to filter by (e.g. 7, 8). Defaults to latest
    """

--snip--
```

Discussion:
@rm-openai: When you use @function_tool, it enforces "strict mode" (https://platform.openai.com/docs/guides/function-calling?api-mode=chat#strict-mode), which means params are required.

<details>

<summary>See code/output</summary>

```
@function_tool
def get_latest_elasticsearch_version(major_version: int | None = None) -> str:
    """Returns the latest GA version of Elasticsearch in "X.Y.Z" format.

    Args:
        major_version: Major version to filter by (e.g. 7, 8). Defaults to latest
    """
    return "test"

print(json.dumps(get_latest_elasticsearch_version.params_json_schema, indent=2))
```

prints:

```
{
  "properties": {
    "major_version": {
      "anyOf": [
        {
          "type": "integer"
        },
        {
          "type": "null"
        }
      ],
      "description": "Major version to filter by (e.g. 7, 8). Defaults to latest",
      "title": "Major Version"
    }
  },
  "title": "get_latest_elasticsearch_version_args",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "major_version"
  ]
}
```

</details>



If you _specifically_ don't want strict mode, you can manually create a `FunctionTool`. Note that this is not recommended - strict mode makes JSON much more reliable. But here's an example of how you might do this, still leveraging the `function_schema` helper:

```
import json
from typing import Any

from agents import FunctionTool, RunContextWrapper
from agents.function_schema import function_schema


def get_latest_elasticsearch_version(major_version: int | None = None) -> str:
    """Returns the latest GA version of Elasticsearch in "X.Y.Z" format.

    Args:
        major_version: Major version to filter by (e.g. 7, 8). Defaults to latest
    """
    return "test"


schema = function_schema(get_latest_elasticsearch_version, strict_json_schema=False)


async def on_invoke_tool(ctx: RunContextWrapper[Any], input_json: str) -> str:
    parsed_json = json.loads(input_json)
    data = schema.params_pydantic_model(**parsed_json)
    args, kwargs = schema.to_call_args(data)
    return get_latest_elasticsearch_version(*args, **kwargs)


tool = FunctionTool(
    name=schema.name,
    description=schema.description or "",
    params_json_schema=schema.params_json_schema,
    on_invoke_tool=on_invoke_tool,
)

print(json.dumps(tool.params_json_schema, indent=2))
```

output:

```
{
  "properties": {
    "major_version": {
      "anyOf": [
        {
          "type": "integer"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Major version to filter by (e.g. 7, 8). Defaults to latest",
      "title": "Major Version"
    }
  },
  "title": "get_latest_elasticsearch_version_args",
  "type": "object"
}
```@codefromthecrypt: @rm-openai thanks for the workaround!

Since other genai frameworks support optional parameters (via supplying defaults), is it possible to convert this to a feature request? I think that even if at the moment we can work around this, it would be more elegant to be able to express a function naturally. This will make it easier to port code elegantly. WDYT?@rm-openai: Yes, makes sense. I think the most sensible way to do this is by adding a `strict_mode: bool = True` to `function_tool()`. Will try to get it soon, and leave it open in case someone else is interested.@Jai0401: Hey @rm-openai, I’d love to work on implementing this feature. Would you be open to a PR for adding `strict_mode: bool = True` to `@function_tool()`? If so, I can start working on it and align with any specific guidelines you have in mind.@rm-openai: Sure thing. Should be straightforward - add `strict_mode` as a param, document why its not a good idea to set to False, thread it through to function_schema/FunctionTool, and add tests. Thanks for taking this on!@codefromthecrypt: heh cool, yeah I had WIP but go for it @Jai0401 I'll help you review.

edge cases are multiple optional parameters of different types. e.g. `x: int = 42`, `x: string = "hello"`, and of course the motivating optional one `x: int | None = None`.

also independently test the things you need to change to support this (e.g. in function_schema.py)

Have fun!@rm-openai: @codefromthecrypt - I realized I misread the original schema. This should actually work out of the box with the current SDK. Did you actually run into an issue? @codefromthecrypt: @rm-openai yep. my original example in the desc creates the following API call. @Jai0401 maybe you can add a unit test about the schema created in your PR, though the tests you have prove the same point I think.

So in the current code, a follow-up to this fails as it sees `major_version` as a required field. I guess it is expecting the LLM to pass literally null instead of leave it out.
```json
{
  "messages": [
    {
      "role": "user",
      "content": "What is the latest version of Elasticsearch?"
    }
  ],
  "model": "qwen2.5:0.5b",
  "stream": false,
  "temperature": 0,
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_latest_elasticsearch_version",
        "description": "Returns the latest GA version of Elasticsearch in \"X.Y.Z\" format.",
        "parameters": {
          "properties": {
            "major_version": {
              "anyOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Major version to filter by (e.g. 7, 8). Defaults to latest",
              "title": "Major Version"
            }
          },
          "required": [
            "major_version"
          ],
          "title": "get_latest_elasticsearch_version_args",
          "type": "object",
          "additionalProperties": false
        }
      }
    }
  ]
}
```

I'm expecting more like some other frameworks, where when it is optional, the wrapped type is used without being required (ideal IMHO), or in worst case keep it defined as an optional, but don't mark it required (e.g. pydantic AI interpretation of the same signature and docstring)
```json
{
  "messages": [
    {
      "role": "user",
      "content": "What is the latest version of Elasticsearch 8?"
    }
  ],
  "model": "qwen2.5:3b",
  "n": 1,
  "stream": false,
  "temperature": 0,
  "tool_choice": "auto",
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_latest_elasticsearch_version",
        "description": "Returns the latest GA version of Elasticsearch in \"X.Y.Z\" format.",
        "parameters": {
          "properties": {
            "major_version": {
              "anyOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Major version to filter by (e.g. 7, 8). Defaults to latest",
              "title": "Major Version"
            }
          },
          "type": "object",
          "additionalProperties": false
        }
      }
    }
  ]
}
```

Make sense?

@rm-openai: Is there any reason you'd prefer to not use strict mode? The LLM can indeed pass null as you noted. And strict mode basically guarantees valid JSON, which is a big deal. @codefromthecrypt: The problem is that the LLM isn't passing null. Maybe I worded incorrectly. This only works if I pass a version in my question. If I don't, it breaks out asking me to supply one.@rm-openai: @codefromthecrypt i think documentation would help there. i.e. one of:
1. In the docstring, something like `Pass null if you don't know the version number`
2. In your tool description, "When using the `get_latest_elasticsearch_version`, pass null if that information isn't provided"
Thoughts?@codefromthecrypt: Personally, I don't think most functions will describe themselves like this. I don't have to do this in other frameworks, either.

Optional, especially when you provide a default, should semantically be optional from the perspective of the LLM. I think you've come around to disagree with this conceptually and I've not been able to convince you to accept non required parameters based on signature. If that's the case, close this out won't fix, and meanwhile thanks for listening.@codefromthecrypt: FYI while I haven't gotten this to work, the suggestion is to not represent optional parameters in the json schema of the tool call. Rather, use for lack of better terms prompt engineering, to describe how to pass the zero value for the appropriate type.

What this implies is a hybrid similar to how certain frameworks support LLMs who don't support tools, so they have to do templating in the prompt to describe the functions etc. What we have to do here is mechanically for each optional parameter, determine and evaluate a means to simulate the parameter being not marked required. If someone gets a general purpose template for this, it is possible we can post-process. Also, if we can hook into the schema generation or replace it,  a 3rd party lib could do the same.

@Jai0401 if you end up with ideas on this let me know, meanwhile if I do I will. OTOH, this is unexpected heavy lifting for me, so not sure I will.@codefromthecrypt: Thanks for progressing this @rm-openai  It now works! I also verified that if you take off `= None` it becomes required even with `strict_mode=False`

All good

---

follow-up interest, but nothing required cc @Jai0401

Here's my function
```python
@function_tool(strict_mode=False)
def get_latest_elasticsearch_version(major_version: int | None = None) -> str:
    """Returns the latest GA version of Elasticsearch in "X.Y.Z" format.

    Args:
        major_version: Major version to filter by (e.g. 7, 8). Defaults to latest
    """
    return "8.17.3"

Now, when I use it mitmproxy shows this json, which is not required, but defined literally as an optional:
```json
{
    "messages": [
        {
            "role": "user",
            "content": "What is the latest version of Elasticsearch?"
        }
    ],
    "model": "qwen2.5:3b",
    "stream": false,
    "temperature": 0,
    "tools": [
        {
            "type": "function",
            "function": {
                "name": "get_latest_elasticsearch_version",
                "description": "Returns the latest GA version of Elasticsearch in \"X.Y.Z\" format.",
                "parameters": {
                    "properties": {
                        "major_version": {
                            "anyOf": [
                                {
                                    "type": "integer"
                                },
                                {
                                    "type": "null"
                                }
                            ],
                            "default": null,
                            "description": "Major version to filter by (e.g. 7, 8). Defaults to latest",
                            "title": "Major Version"
                        }
                    },
                    "title": "get_latest_elasticsearch_version_args",
                    "type": "object"
                }
            }
        }
    ]
}
```

Now, I have to use qwen2.5:3b, not qwen2.5:0.5b because the smaller doesn't interpret this as a `tool_calls` precisely, rather as XML 
```diff
2c2
<     "id": "chatcmpl-823",
---
>     "id": "chatcmpl-443",
4,5c4,5
<     "created": 1742206965,
<     "model": "qwen2.5:0.5b",
---
>     "created": 1742207060,
>     "model": "qwen2.5:3b",
12c12,23
<                 "content": "<tool_call>\n{\"name\": \"get_latest_elasticsearch_version\", \"arguments\": null}\n</tool_call>"
---
>                 "content": "",
>                 "tool_calls": [
>                     {
>                         "id": "call_iu2d5rg9",
>                         "index": 0,
>                         "type": "function",
>                         "function": {
>                             "name": "get_latest_elasticsearch_version",
>                             "arguments": "{}"
>                         }
>                     }
>                 ]
14c25
<             "finish_reason": "stop"
---
>             "finish_reason": "tool_calls"
```

Aside, but what I was originally hoping for was that if the parameter is an optional of an int, it could be translated to a non-required int. The same way as if it were defined like this:

```python
@function_tool(strict_mode=False)
def get_latest_elasticsearch_version(major_version: int=0) -> str:
```

`major_version: int=0` currently renders like this, which is almost exactly how semantic-kernel renders `major_version: int | None = None` (just that semantic-kernel also adds a redundant `"required": []`
```json
{
    "messages": [
        {
            "role": "user",
            "content": "What is the latest version of Elasticsearch?"
        }
    ],
    "model": "qwen2.5:3b",
    "stream": false,
    "temperature": 0,
    "tools": [
        {
            "type": "function",
            "function": {
                "name": "get_latest_elasticsearch_version",
                "description": "Returns the latest GA version of Elasticsearch in \"X.Y.Z\" format.",
                "parameters": {
                    "properties": {
                        "major_version": {
                            "description": "Major version to filter by (e.g. 7, 8). Defaults to latest",
                            "title": "Major Version",
                            "type": "integer"
                        }
                    },
                    "title": "get_latest_elasticsearch_version_args",
                    "type": "object"
                }
            }
        }
    ]
}
```

There is an improvement here, as if you use too small a model, e.g.  `qwen2.5:0.5b`, it asks the user to pass the version instead of breaking with a malformed tool call. OTOH, both ways don't work with `qwen2.5:0.5b`, so up to you if you feel we should do anything more.@rm-openai: @codefromthecrypt - Sorry would you mind explaining a bit more what's missing? As far as I can tell, if you mark it as `strict_mode=False`, the parameter becomes optional for the LLM.@codefromthecrypt: Tldr if it makes sense to treat int optional = none the same as int =0, let's do it. Regardless, all good.@github-actions: This issue is stale because it has been open for 7 days with no activity.@github-actions: This issue was closed because it has been inactive for 3 days since being marked as stale.@HG2407: @rm-openai What if I want some parameters to be required and others to be optional, I can easily do this via pydantic, where any field that I have not wrapped in an optional object will be required. Is there a way to replicate this here ? Because as far as I understand, if I do strict_mode = False, while low, there's a chance that llm might not pass anything, I want to avoid that.

Secondly, suppose my tool calls an api, which is linked to the backend, now I want to pass an auth token to the api, which I currently do by adding it as a parameter in each tool and asking agent to provide it through its system prompt, is there a better way to do it ?

Edit: I just checked there's something called `function_schema` that you use to extract the parameters and description from the docstring and then convert them to pydantic model. is there a way to directly provide a pydantic model to this ?

Solution patch:
diff --git a/.github/ISSUE_TEMPLATE/model_provider.md b/.github/ISSUE_TEMPLATE/model_provider.md
new file mode 100644
index 00000000..b56cb24e
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/model_provider.md
@@ -0,0 +1,26 @@
+---
+name: Custom model providers
+about: Questions or bugs about using non-OpenAI models
+title: ''
+labels: bug
+assignees: ''
+
+---
+
+### Please read this first
+
+- **Have you read the custom model provider docs, including the 'Common issues' section?** [Model provider docs](https://openai.github.io/openai-agents-python/models/#using-other-llm-providers)
+- **Have you searched for related issues?** Others may have faced similar issues.
+
+### Describe the question
+A clear and concise description of what the question or bug is.
+
+### Debug information
+- Agents SDK version: (e.g. `v0.0.3`)
+- Python version (e.g. Python 3.10)
+
+### Repro steps
+Ideally provide a minimal python script that can be run to reproduce the issue.
+
+### Expected behavior
+A clear and concise description of what you expected to happen.
diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
new file mode 100644
index 00000000..0fdeab1e
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
@@ -0,0 +1,18 @@
+### Summary
+
+<!-- Please give a short summary of the change and the problem this solves. -->
+
+### Test plan
+
+<!-- Please explain how this was tested -->
+
+### Issue number
+
+<!-- For example: "Closes #1234" -->
+
+### Checks
+
+- [ ] I've added new tests (if relevant)
+- [ ] I've added/updated the relevant documentation
+- [ ] I've run `make lint` and `make format`
+- [ ] I've made sure tests pass
diff --git a/README.md b/README.md
index 90fea502..210f6f4b 100644
--- a/README.md
+++ b/README.md
@@ -47,9 +47,11 @@ print(result.final_output)
 
 (_If running this, ensure you set the `OPENAI_API_KEY` environment variable_)
 
+(_For Jupyter notebook users, see [hello_world_jupyter.py](examples/basic/hello_world_jupyter.py)_)
+
 ## Handoffs example
 
-```py
+```python
 from agents import Agent, Runner
 import asyncio
 
@@ -140,7 +142,7 @@ The Agents SDK is designed to be highly flexible, allowing you to model a wide r
 
 ## Tracing
 
-The Agents SDK automatically traces your agent runs, making it easy to track and debug the behavior of your agents. Tracing is extensible by design, supporting custom spans and a wide variety of external destinations, including [Logfire](https://logfire.pydantic.dev/docs/integrations/llms/openai/#openai-agents), [AgentOps](https://docs.agentops.ai/v1/integrations/agentssdk), and [Braintrust](https://braintrust.dev/docs/guides/traces/integrations#openai-agents-sdk). For more details about how to customize or disable tracing, see [Tracing](http://openai.github.io/openai-agents-python/tracing).
+The Agents SDK automatically traces your agent runs, making it easy to track and debug the behavior of your agents. Tracing is extensible by design, supporting custom spans and a wide variety of external destinations, including [Logfire](https://logfire.pydantic.dev/docs/integrations/llms/openai/#openai-agents), [AgentOps](https://docs.agentops.ai/v1/integrations/agentssdk), [Braintrust](https://braintrust.dev/docs/guides/traces/integrations#openai-agents-sdk), [Scorecard](https://docs.scorecard.io/docs/documentation/features/tracing#openai-agents-sdk-integration), and [Keywords AI](https://docs.keywordsai.co/integration/development-frameworks/openai-agent). For more details about how to customize or disable tracing, see [Tracing](http://openai.github.io/openai-agents-python/tracing).
 
 ## Development (only needed if you need to edit the SDK/examples)
 
diff --git a/docs/agents.md b/docs/agents.md
index 9b6264b5..17589b3d 100644
--- a/docs/agents.md
+++ b/docs/agents.md
@@ -13,6 +13,7 @@ The most common properties of an agent you'll configure are:
 ```python
 from agents import Agent, ModelSettings, function_tool
 
+@function_tool
 def get_weather(city: str) -> str:
     return f"The weather in {city} is sunny"
 
@@ -20,7 +21,7 @@ agent = Agent(
     name="Haiku agent",
     instructions="Always respond in haiku form",
     model="o3-mini",
-    tools=[function_tool(get_weather)],
+    tools=[get_weather],
 )
 ```
 
diff --git a/docs/context.md b/docs/context.md
index 5dcacebe..69c43fbb 100644
--- a/docs/context.md
+++ b/docs/context.md
@@ -36,6 +36,7 @@ class UserInfo:  # (1)!
     name: str
     uid: int
 
+@function_tool
 async def fetch_user_age(wrapper: RunContextWrapper[UserInfo]) -> str:  # (2)!
     return f"User {wrapper.context.name} is 47 years old"
 
@@ -44,7 +45,7 @@ async def main():
 
     agent = Agent[UserInfo](  # (4)!
         name="Assistant",
-        tools=[function_tool(fetch_user_age)],
+        tools=[fetch_user_age],
     )
 
     result = await Runner.run(
diff --git a/docs/models.md b/docs/models.md
index 7ad515bc..ab4cefb8 100644
--- a/docs/models.md
+++ b/docs/models.md
@@ -53,21 +53,41 @@ async def main():
 
 ## Using other LLM providers
 
-Many providers also support the OpenAI API format, which means you can pass a `base_url` to the existing OpenAI model implementations and use them easily. `ModelSettings` is used to configure tuning parameters (e.g., temperature, top_p) for the model you select.
+You can use other LLM providers in 3 ways (examples [here](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/)):
 
-```python
-external_client = AsyncOpenAI(
-    api_key="EXTERNAL_API_KEY",
-    base_url="https://api.external.com/v1/",
-)
+1. [`set_default_openai_client`][agents.set_default_openai_client] is useful in cases where you want to globally use an instance of `AsyncOpenAI` as the LLM client. This is for cases where the LLM provider has an OpenAI compatible API endpoint, and you can set the `base_url` and `api_key`. See a configurable example in [examples/model_providers/custom_example_global.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_global.py).
+2. [`ModelProvider`][agents.models.interface.ModelProvider] is at the `Runner.run` level. This lets you say "use a custom model provider for all agents in this run". See a configurable example in [examples/model_providers/custom_example_provider.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_provider.py).
+3. [`Agent.model`][agents.agent.Agent.model] lets you specify the model on a specific Agent instance. This enables you to mix and match different providers for different agents. See a configurable example in [examples/model_providers/custom_example_agent.py](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/custom_example_agent.py).
+
+In cases where you do not have an API key from `platform.openai.com`, we recommend disabling tracing via `set_tracing_disabled()`, or setting up a [different tracing processor](tracing.md).
+
+!!! note
+
+    In these examples, we use the Chat Completions API/model, because most LLM providers don't yet support the Responses API. If your LLM provider does support it, we recommend using Responses.
+
+## Common issues with using other LLM providers
+
+### Tracing client error 401
+
+If you get errors related to tracing, this is because traces are uploaded to OpenAI servers, and you don't have an OpenAI API key. You have three options to resolve this:
+
+1. Disable tracing entirely: [`set_tracing_disabled(True)`][agents.set_tracing_disabled].
+2. Set an OpenAI key for tracing: [`set_tracing_export_api_key(...)`][agents.set_tracing_export_api_key]. This API key will only be used for uploading traces, and must be from [platform.openai.com](https://platform.openai.com/).
+3. Use a non-OpenAI trace processor. See the [tracing docs](tracing.md#custom-tracing-processors).
+
+### Responses API support
+
+The SDK uses the Responses API by default, but most other LLM providers don't yet support it. You may see 404s or similar issues as a result. To resolve, you have two options:
+
+1. Call [`set_default_openai_api("chat_completions")`][agents.set_default_openai_api]. This works if you are setting `OPENAI_API_KEY` and `OPENAI_BASE_URL` via environment vars.
+2. Use [`OpenAIChatCompletionsModel`][agents.models.openai_chatcompletions.OpenAIChatCompletionsModel]. There are examples [here](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers/).
+
+### Structured outputs support
+
+Some model providers don't have support for [structured outputs](https://platform.openai.com/docs/guides/structured-outputs). This sometimes results in an error that looks something like this:
 
-spanish_agent = Agent(
-    name="Spanish agent",
-    instructions="You only speak Spanish.",
-    model=OpenAIChatCompletionsModel(
-        model="EXTERNAL_MODEL_NAME",
-        openai_client=external_client,
-    ),
-    model_settings=ModelSettings(temperature=0.5),
-)
 ```
+BadRequestError: Error code: 400 - {'error': {'message': "'response_format.type' : value is not one of the allowed values ['text','json_object']", 'type': 'invalid_request_error'}}
+```
+
+This is a shortcoming of some model providers - they support JSON outputs, but don't allow you to specify the `json_schema` to use for the output. We are working on a fix for this, but we suggest relying on providers that do have support for JSON schema output, because otherwise your app will often break because of malformed JSON.
diff --git a/docs/running_agents.md b/docs/running_agents.md
index a2f137cf..32abf9d5 100644
--- a/docs/running_agents.md
+++ b/docs/running_agents.md
@@ -78,7 +78,7 @@ async def main():
         # San Francisco
 
         # Second turn
-        new_input = output.to_input_list() + [{"role": "user", "content": "What state is it in?"}]
+        new_input = result.to_input_list() + [{"role": "user", "content": "What state is it in?"}]
         result = await Runner.run(agent, new_input)
         print(result.final_output)
         # California
diff --git a/docs/tracing.md b/docs/tracing.md
index da0d536f..d7d0a651 100644
--- a/docs/tracing.md
+++ b/docs/tracing.md
@@ -50,7 +50,7 @@ async def main():
 
     with trace("Joke workflow"): # (1)!
         first_result = await Runner.run(agent, "Tell me a joke")
-        second_result = await Runner.run(agent, f"Rate this joke: {first_output.final_output}")
+        second_result = await Runner.run(agent, f"Rate this joke: {first_result.final_output}")
         print(f"Joke: {first_result.final_output}")
         print(f"Rating: {second_result.final_output}")
 ```
@@ -93,3 +93,5 @@ External trace processors include:
 -   [Braintrust](https://braintrust.dev/docs/guides/traces/integrations#openai-agents-sdk)
 -   [Pydantic Logfire](https://logfire.pydantic.dev/docs/integrations/llms/openai/#openai-agents)
 -   [AgentOps](https://docs.agentops.ai/v1/integrations/agentssdk)
+-   [Scorecard](https://docs.scorecard.io/docs/documentation/features/tracing#openai-agents-sdk-integration))
+-   [Keywords AI](https://docs.keywordsai.co/integration/development-frameworks/openai-agent)
diff --git a/examples/agent_patterns/input_guardrails.py b/examples/agent_patterns/input_guardrails.py
index 62591886..8c8e1826 100644
--- a/examples/agent_patterns/input_guardrails.py
+++ b/examples/agent_patterns/input_guardrails.py
@@ -53,7 +53,7 @@ async def math_guardrail(
 
     return GuardrailFunctionOutput(
         output_info=final_output,
-        tripwire_triggered=not final_output.is_math_homework,
+        tripwire_triggered=final_output.is_math_homework,
     )
 
 
diff --git a/examples/basic/hello_world_jupyter.py b/examples/basic/hello_world_jupyter.py
new file mode 100644
index 00000000..bb8f14c0
--- /dev/null
+++ b/examples/basic/hello_world_jupyter.py
@@ -0,0 +1,11 @@
+from agents import Agent, Runner
+
+agent = Agent(name="Assistant", instructions="You are a helpful assistant")
+
+# Intended for Jupyter notebooks where there's an existing event loop
+result = await Runner.run(agent, "Write a haiku about recursion in programming.") # type: ignore[top-level-await]  # noqa: F704
+print(result.final_output)
+
+# Code within code loops,
+# Infinite mirrors reflect—
+# Logic folds on self.
diff --git a/examples/model_providers/README.md b/examples/model_providers/README.md
new file mode 100644
index 00000000..f9330c24
--- /dev/null
+++ b/examples/model_providers/README.md
@@ -0,0 +1,19 @@
+# Custom LLM providers
+
+The examples in this directory demonstrate how you might use a non-OpenAI LLM provider. To run them, first set a base URL, API key and model.
+
+```bash
+export EXAMPLE_BASE_URL="..."
+export EXAMPLE_API_KEY="..."
+export EXAMPLE_MODEL_NAME"..."
+```
+
+Then run the examples, e.g.:
+
+```
+python examples/model_providers/custom_example_provider.py
+
+Loops within themselves,
+Function calls its own being,
+Depth without ending.
+```
diff --git a/examples/model_providers/custom_example_agent.py b/examples/model_providers/custom_example_agent.py
new file mode 100644
index 00000000..f10865c4
--- /dev/null
+++ b/examples/model_providers/custom_example_agent.py
@@ -0,0 +1,55 @@
+import asyncio
+import os
+
+from openai import AsyncOpenAI
+
+from agents import Agent, OpenAIChatCompletionsModel, Runner, function_tool, set_tracing_disabled
+
+BASE_URL = os.getenv("EXAMPLE_BASE_URL") or ""
+API_KEY = os.getenv("EXAMPLE_API_KEY") or ""
+MODEL_NAME = os.getenv("EXAMPLE_MODEL_NAME") or ""
+
+if not BASE_URL or not API_KEY or not MODEL_NAME:
+    raise ValueError(
+        "Please set EXAMPLE_BASE_URL, EXAMPLE_API_KEY, EXAMPLE_MODEL_NAME via env var or code."
+    )
+
+"""This example uses a custom provider for a specific agent. Steps:
+1. Create a custom OpenAI client.
+2. Create a `Model` that uses the custom client.
+3. Set the `model` on the Agent.
+
+Note that in this example, we disable tracing under the assumption that you don't have an API key
+from platform.openai.com. If you do have one, you can either set the `OPENAI_API_KEY` env var
+or call set_tracing_export_api_key() to set a tracing specific key.
+"""
+client = AsyncOpenAI(base_url=BASE_URL, api_key=API_KEY)
+set_tracing_disabled(disabled=True)
+
+# An alternate approach that would also work:
+# PROVIDER = OpenAIProvider(openai_client=client)
+# agent = Agent(..., model="some-custom-model")
+# Runner.run(agent, ..., run_config=RunConfig(model_provider=PROVIDER))
+
+
+@function_tool
+def get_weather(city: str):
+    print(f"[debug] getting weather for {city}")
+    return f"The weather in {city} is sunny."
+
+
+async def main():
+    # This agent will use the custom LLM provider
+    agent = Agent(
+        name="Assistant",
+        instructions="You only respond in haikus.",
+        model=OpenAIChatCompletionsModel(model=MODEL_NAME, openai_client=client),
+        tools=[get_weather],
+    )
+
+    result = await Runner.run(agent, "What's the weather in Tokyo?")
+    print(result.final_output)
+
+
+if __name__ == "__main__":
+    asyncio.run(main())
diff --git a/examples/model_providers/custom_example_global.py b/examples/model_providers/custom_example_global.py
new file mode 100644
index 00000000..ae9756d3
--- /dev/null
+++ b/examples/model_providers/custom_example_global.py
@@ -0,0 +1,63 @@
+import asyncio
+import os
+
+from openai import AsyncOpenAI
+
+from agents import (
+    Agent,
+    Runner,
+    function_tool,
+    set_default_openai_api,
+    set_default_openai_client,
+    set_tracing_disabled,
+)
+
+BASE_URL = os.getenv("EXAMPLE_BASE_URL") or ""
+API_KEY = os.getenv("EXAMPLE_API_KEY") or ""
+MODEL_NAME = os.getenv("EXAMPLE_MODEL_NAME") or ""
+
+if not BASE_URL or not API_KEY or not MODEL_NAME:
+    raise ValueError(
+        "Please set EXAMPLE_BASE_URL, EXAMPLE_API_KEY, EXAMPLE_MODEL_NAME via env var or code."
+    )
+
+
+"""This example uses a custom provider for all requests by default. We do three things:
+1. Create a custom client.
+2. Set it as the default OpenAI client, and don't use it for tracing.
+3. Set the default API as Chat Completions, as most LLM providers don't yet support Responses API.
+
+Note that in this example, we disable tracing under the assumption that you don't have an API key
+from platform.openai.com. If you do have one, you can either set the `OPENAI_API_KEY` env var
+or call set_tracing_export_api_key() to set a tracing specific key.
+"""
+
+client = AsyncOpenAI(
+    base_url=BASE_URL,
+    api_key=API_KEY,
+)
+set_default_openai_client(client=client, use_for_tracing=False)
+set_default_openai_api("chat_completions")
+set_tracing_disabled(disabled=True)
+
+
+@function_tool
+def get_weather(city: str):
+    print(f"[debug] getting weather for {city}")
+    return f"The weather in {city} is sunny."
+
+
+async def main():
+    agent = Agent(
+        name="Assistant",
+        instructions="You only respond in haikus.",
+        model=MODEL_NAME,
+        tools=[get_weather],
+    )
+
+    result = await Runner.run(agent, "What's the weather in Tokyo?")
+    print(result.final_output)
+
+
+if __name__ == "__main__":
+    asyncio.run(main())
diff --git a/examples/model_providers/custom_example_provider.py b/examples/model_providers/custom_example_provider.py
new file mode 100644
index 00000000..4e590198
--- /dev/null
+++ b/examples/model_providers/custom_example_provider.py
@@ -0,0 +1,77 @@
+from __future__ import annotations
+
+import asyncio
+import os
+
+from openai import AsyncOpenAI
+
+from agents import (
+    Agent,
+    Model,
+    ModelProvider,
+    OpenAIChatCompletionsModel,
+    RunConfig,
+    Runner,
+    function_tool,
+    set_tracing_disabled,
+)
+
+BASE_URL = os.getenv("EXAMPLE_BASE_URL") or ""
+API_KEY = os.getenv("EXAMPLE_API_KEY") or ""
+MODEL_NAME = os.getenv("EXAMPLE_MODEL_NAME") or ""
+
+if not BASE_URL or not API_KEY or not MODEL_NAME:
+    raise ValueError(
+        "Please set EXAMPLE_BASE_URL, EXAMPLE_API_KEY, EXAMPLE_MODEL_NAME via env var or code."
+    )
+
+
+"""This example uses a custom provider for some calls to Runner.run(), and direct calls to OpenAI for
+others. Steps:
+1. Create a custom OpenAI client.
+2. Create a ModelProvider that uses the custom client.
+3. Use the ModelProvider in calls to Runner.run(), only when we want to use the custom LLM provider.
+
+Note that in this example, we disable tracing under the assumption that you don't have an API key
+from platform.openai.com. If you do have one, you can either set the `OPENAI_API_KEY` env var
+or call set_tracing_export_api_key() to set a tracing specific key.
+"""
+client = AsyncOpenAI(base_url=BASE_URL, api_key=API_KEY)
+set_tracing_disabled(disabled=True)
+
+
+class CustomModelProvider(ModelProvider):
+    def get_model(self, model_name: str | None) -> Model:
+        return OpenAIChatCompletionsModel(model=model_name or MODEL_NAME, openai_client=client)
+
+
+CUSTOM_MODEL_PROVIDER = CustomModelProvider()
+
+
+@function_tool
+def get_weather(city: str):
+    print(f"[debug] getting weather for {city}")
+    return f"The weather in {city} is sunny."
+
+
+async def main():
+    agent = Agent(name="Assistant", instructions="You only respond in haikus.", tools=[get_weather])
+
+    # This will use the custom model provider
+    result = await Runner.run(
+        agent,
+        "What's the weather in Tokyo?",
+        run_config=RunConfig(model_provider=CUSTOM_MODEL_PROVIDER),
+    )
+    print(result.final_output)
+
+    # If you uncomment this, it will use OpenAI directly, not the custom provider
+    # result = await Runner.run(
+    #     agent,
+    #     "What's the weather in Tokyo?",
+    # )
+    # print(result.final_output)
+
+
+if __name__ == "__main__":
+    asyncio.run(main())
diff --git a/pyproject.toml b/pyproject.toml
index 262ce17c..8184a670 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [project]
 name = "openai-agents"
-version = "0.0.3"
+version = "0.0.4"
 description = "OpenAI Agents SDK"
 readme = "README.md"
 requires-python = ">=3.9"
diff --git a/src/agents/__init__.py b/src/agents/__init__.py
index 69c500ab..a2d7f247 100644
--- a/src/agents/__init__.py
+++ b/src/agents/__init__.py
@@ -92,13 +92,19 @@
 from .usage import Usage
 
 
-def set_default_openai_key(key: str) -> None:
-    """Set the default OpenAI API key to use for LLM requests and tracing. This is only necessary if
-    the OPENAI_API_KEY environment variable is not already set.
+def set_default_openai_key(key: str, use_for_tracing: bool = True) -> None:
+    """Set the default OpenAI API key to use for LLM requests (and optionally tracing(). This is
+    only necessary if the OPENAI_API_KEY environment variable is not already set.
 
     If provided, this key will be used instead of the OPENAI_API_KEY environment variable.
+
+    Args:
+        key: The OpenAI key to use.
+        use_for_tracing: Whether to also use this key to send traces to OpenAI. Defaults to True
+            If False, you'll either need to set the OPENAI_API_KEY environment variable or call
+            set_tracing_export_api_key() with the API key you want to use for tracing.
     """
-    _config.set_default_openai_key(key)
+    _config.set_default_openai_key(key, use_for_tracing)
 
 
 def set_default_openai_client(client: AsyncOpenAI, use_for_tracing: bool = True) -> None:
@@ -123,10 +129,9 @@ def set_default_openai_api(api: Literal["chat_completions", "responses"]) -> Non
 
 def enable_verbose_stdout_logging():
     """Enables verbose logging to stdout. This is useful for debugging."""
-    for name in ["openai.agents", "openai.agents.tracing"]:
-        logger = logging.getLogger(name)
-        logger.setLevel(logging.DEBUG)
-        logger.addHandler(logging.StreamHandler(sys.stdout))
+    logger = logging.getLogger("openai.agents")
+    logger.setLevel(logging.DEBUG)
+    logger.addHandler(logging.StreamHandler(sys.stdout))
 
 
 __all__ = [
diff --git a/src/agents/_config.py b/src/agents/_config.py
index 55ded64d..304cfb83 100644
--- a/src/agents/_config.py
+++ b/src/agents/_config.py
@@ -5,15 +5,18 @@
 from .tracing import set_tracing_export_api_key
 
 
-def set_default_openai_key(key: str) -> None:
-    set_tracing_export_api_key(key)
+def set_default_openai_key(key: str, use_for_tracing: bool) -> None:
     _openai_shared.set_default_openai_key(key)
 
+    if use_for_tracing:
+        set_tracing_export_api_key(key)
+
 
 def set_default_openai_client(client: AsyncOpenAI, use_for_tracing: bool) -> None:
+    _openai_shared.set_default_openai_client(client)
+
     if use_for_tracing:
         set_tracing_export_api_key(client.api_key)
-    _openai_shared.set_default_openai_client(client)
 
 
 def set_default_openai_api(api: Literal["chat_completions", "responses"]) -> None:
diff --git a/src/agents/function_schema.py b/src/agents/function_schema.py
index a4b57672..681affce 100644
--- a/src/agents/function_schema.py
+++ b/src/agents/function_schema.py
@@ -33,6 +33,9 @@ class FuncSchema:
     """The signature of the function."""
     takes_context: bool = False
     """Whether the function takes a RunContextWrapper argument (must be the first argument)."""
+    strict_json_schema: bool = True
+    """Whether the JSON schema is in strict mode. We **strongly** recommend setting this to True,
+    as it increases the likelihood of correct JSON input."""
 
     def to_call_args(self, data: BaseModel) -> tuple[list[Any], dict[str, Any]]:
         """
@@ -337,4 +340,5 @@ def function_schema(
         params_json_schema=json_schema,
         signature=sig,
         takes_context=takes_context,
+        strict_json_schema=strict_json_schema,
     )
diff --git a/src/agents/guardrail.py b/src/agents/guardrail.py
index fcae0b8a..5bebcd66 100644
--- a/src/agents/guardrail.py
+++ b/src/agents/guardrail.py
@@ -86,7 +86,7 @@ class InputGuardrail(Generic[TContext]):
         [RunContextWrapper[TContext], Agent[Any], str | list[TResponseInputItem]],
         MaybeAwaitable[GuardrailFunctionOutput],
     ]
-    """A function that receives the the agent input and the context, and returns a
+    """A function that receives the agent input and the context, and returns a
      `GuardrailResult`. The result marks whether the tripwire was triggered, and can optionally
      include information about the guardrail's output.
     """
diff --git a/src/agents/model_settings.py b/src/agents/model_settings.py
index d8178ae3..cc4b6cb6 100644
--- a/src/agents/model_settings.py
+++ b/src/agents/model_settings.py
@@ -10,15 +10,34 @@ class ModelSettings:
 
     This class holds optional model configuration parameters (e.g. temperature,
     top_p, penalties, truncation, etc.).
+
+    Not all models/providers support all of these parameters, so please check the API documentation
+    for the specific model and provider you are using.
     """
 
     temperature: float | None = None
+    """The temperature to use when calling the model."""
+
     top_p: float | None = None
+    """The top_p to use when calling the model."""
+
     frequency_penalty: float | None = None
+    """The frequency penalty to use when calling the model."""
+
     presence_penalty: float | None = None
+    """The presence penalty to use when calling the model."""
+
     tool_choice: Literal["auto", "required", "none"] | str | None = None
+    """The tool choice to use when calling the model."""
+
     parallel_tool_calls: bool | None = False
+    """Whether to use parallel tool calls when calling the model."""
+
     truncation: Literal["auto", "disabled"] | None = None
+    """The truncation strategy to use when calling the model."""
+
+    max_tokens: int | None = None
+    """The maximum number of output tokens to generate."""
 
     def resolve(self, override: ModelSettings | None) -> ModelSettings:
         """Produce a new ModelSettings by overlaying any non-None values from the
@@ -33,4 +52,5 @@ def resolve(self, override: ModelSettings | None) -> ModelSettings:
             tool_choice=override.tool_choice or self.tool_choice,
             parallel_tool_calls=override.parallel_tool_calls or self.parallel_tool_calls,
             truncation=override.truncation or self.truncation,
+            max_tokens=override.max_tokens or self.max_tokens,
         )
diff --git a/src/agents/models/openai_chatcompletions.py b/src/agents/models/openai_chatcompletions.py
index a7340d05..3543225d 100644
--- a/src/agents/models/openai_chatcompletions.py
+++ b/src/agents/models/openai_chatcompletions.py
@@ -51,8 +51,10 @@
     ResponseOutputText,
     ResponseRefusalDeltaEvent,
     ResponseTextDeltaEvent,
+    ResponseUsage,
 )
 from openai.types.responses.response_input_param import FunctionCallOutput, ItemReference, Message
+from openai.types.responses.response_usage import OutputTokensDetails
 
 from .. import _debug
 from ..agent_output import AgentOutputSchema
@@ -405,7 +407,23 @@ async def stream_response(
             for function_call in state.function_calls.values():
                 outputs.append(function_call)
 
-            final_response = response.model_copy(update={"output": outputs, "usage": usage})
+            final_response = response.model_copy()
+            final_response.output = outputs
+            final_response.usage = (
+                ResponseUsage(
+                    input_tokens=usage.prompt_tokens,
+                    output_tokens=usage.completion_tokens,
+                    total_tokens=usage.total_tokens,
+                    output_tokens_details=OutputTokensDetails(
+                        reasoning_tokens=usage.completion_tokens_details.reasoning_tokens
+                        if usage.completion_tokens_details
+                        and usage.completion_tokens_details.reasoning_tokens
+                        else 0
+                    ),
+                )
+                if usage
+                else None
+            )
 
             yield ResponseCompletedEvent(
                 response=final_response,
@@ -503,6 +521,7 @@ async def _fetch_response(
             top_p=self._non_null_or_not_given(model_settings.top_p),
             frequency_penalty=self._non_null_or_not_given(model_settings.frequency_penalty),
             presence_penalty=self._non_null_or_not_given(model_settings.presence_penalty),
+            max_tokens=self._non_null_or_not_given(model_settings.max_tokens),
             tool_choice=tool_choice,
             response_format=response_format,
             parallel_tool_calls=parallel_tool_calls,
@@ -808,6 +827,13 @@ def ensure_assistant_message() -> ChatCompletionAssistantMessageParam:
                         "content": cls.extract_text_content(content),
                     }
                     result.append(msg_developer)
+                elif role == "assistant":
+                    flush_assistant_message()
+                    msg_assistant: ChatCompletionAssistantMessageParam = {
+                        "role": "assistant",
+                        "content": cls.extract_text_content(content),
+                    }
+                    result.append(msg_assistant)
                 else:
                     raise UserError(f"Unexpected role in easy_input_message: {role}")
 
diff --git a/src/agents/models/openai_provider.py b/src/agents/models/openai_provider.py
index 51946638..e6a859fb 100644
--- a/src/agents/models/openai_provider.py
+++ b/src/agents/models/openai_provider.py
@@ -38,28 +38,41 @@ def __init__(
             assert api_key is None and base_url is None, (
                 "Don't provide api_key or base_url if you provide openai_client"
             )
-            self._client = openai_client
+            self._client: AsyncOpenAI | None = openai_client
         else:
-            self._client = _openai_shared.get_default_openai_client() or AsyncOpenAI(
-                api_key=api_key or _openai_shared.get_default_openai_key(),
-                base_url=base_url,
-                organization=organization,
-                project=project,
-                http_client=shared_http_client(),
-            )
+            self._client = None
+            self._stored_api_key = api_key
+            self._stored_base_url = base_url
+            self._stored_organization = organization
+            self._stored_project = project
 
-        self._is_openai_model = self._client.base_url.host.startswith("api.openai.com")
         if use_responses is not None:
             self._use_responses = use_responses
         else:
             self._use_responses = _openai_shared.get_use_responses_by_default()
 
+    # We lazy load the client in case you never actually use OpenAIProvider(). Otherwise
+    # AsyncOpenAI() raises an error if you don't have an API key set.
+    def _get_client(self) -> AsyncOpenAI:
+        if self._client is None:
+            self._client = _openai_shared.get_default_openai_client() or AsyncOpenAI(
+                api_key=self._stored_api_key or _openai_shared.get_default_openai_key(),
+                base_url=self._stored_base_url,
+                organization=self._stored_organization,
+                project=self._stored_project,
+                http_client=shared_http_client(),
+            )
+
+        return self._client
+
     def get_model(self, model_name: str | None) -> Model:
         if model_name is None:
             model_name = DEFAULT_MODEL
 
+        client = self._get_client()
+
         return (
-            OpenAIResponsesModel(model=model_name, openai_client=self._client)
+            OpenAIResponsesModel(model=model_name, openai_client=client)
             if self._use_responses
-            else OpenAIChatCompletionsModel(model=model_name, openai_client=self._client)
+            else OpenAIChatCompletionsModel(model=model_name, openai_client=client)
         )
diff --git a/src/agents/models/openai_responses.py b/src/agents/models/openai_responses.py
index e060fb8e..78765ecb 100644
--- a/src/agents/models/openai_responses.py
+++ b/src/agents/models/openai_responses.py
@@ -5,7 +5,7 @@
 from dataclasses import dataclass
 from typing import TYPE_CHECKING, Any, Literal, overload
 
-from openai import NOT_GIVEN, AsyncOpenAI, AsyncStream, NotGiven
+from openai import NOT_GIVEN, APIStatusError, AsyncOpenAI, AsyncStream, NotGiven
 from openai.types import ChatModel
 from openai.types.responses import (
     Response,
@@ -113,7 +113,8 @@ async def get_response(
                         },
                     )
                 )
-                logger.error(f"Error getting response: {e}")
+                request_id = e.request_id if isinstance(e, APIStatusError) else None
+                logger.error(f"Error getting response: {e}. (request_id: {request_id})")
                 raise
 
         return ModelResponse(
@@ -235,6 +236,7 @@ async def _fetch_response(
             temperature=self._non_null_or_not_given(model_settings.temperature),
             top_p=self._non_null_or_not_given(model_settings.top_p),
             truncation=self._non_null_or_not_given(model_settings.truncation),
+            max_output_tokens=self._non_null_or_not_given(model_settings.max_tokens),
             tool_choice=tool_choice,
             parallel_tool_calls=parallel_tool_calls,
             stream=stream,
diff --git a/src/agents/result.py b/src/agents/result.py
index 56838273..6e806b72 100644
--- a/src/agents/result.py
+++ b/src/agents/result.py
@@ -216,5 +216,3 @@ def _cleanup_tasks(self):
 
         if self._output_guardrails_task and not self._output_guardrails_task.done():
             self._output_guardrails_task.cancel()
-            self._output_guardrails_task.cancel()
-            self._output_guardrails_task.cancel()
diff --git a/src/agents/tool.py b/src/agents/tool.py
index 75872680..cbe87944 100644
--- a/src/agents/tool.py
+++ b/src/agents/tool.py
@@ -137,6 +137,7 @@ def function_tool(
     docstring_style: DocstringStyle | None = None,
     use_docstring_info: bool = True,
     failure_error_function: ToolErrorFunction | None = None,
+    strict_mode: bool = True,
 ) -> FunctionTool:
     """Overload for usage as @function_tool (no parentheses)."""
     ...
@@ -150,6 +151,7 @@ def function_tool(
     docstring_style: DocstringStyle | None = None,
     use_docstring_info: bool = True,
     failure_error_function: ToolErrorFunction | None = None,
+    strict_mode: bool = True,
 ) -> Callable[[ToolFunction[...]], FunctionTool]:
     """Overload for usage as @function_tool(...)."""
     ...
@@ -163,6 +165,7 @@ def function_tool(
     docstring_style: DocstringStyle | None = None,
     use_docstring_info: bool = True,
     failure_error_function: ToolErrorFunction | None = default_tool_error_function,
+    strict_mode: bool = True,
 ) -> FunctionTool | Callable[[ToolFunction[...]], FunctionTool]:
     """
     Decorator to create a FunctionTool from a function. By default, we will:
@@ -186,6 +189,8 @@ def function_tool(
         failure_error_function: If provided, use this function to generate an error message when
             the tool call fails. The error message is sent to the LLM. If you pass None, then no
             error message will be sent and instead an Exception will be raised.
+        strict_mode: If False, parameters with default values become optional in the
+            function schema.
     """
 
     def _create_function_tool(the_func: ToolFunction[...]) -> FunctionTool:
@@ -195,6 +200,7 @@ def _create_function_tool(the_func: ToolFunction[...]) -> FunctionTool:
             description_override=description_override,
             docstring_style=docstring_style,
             use_docstring_info=use_docstring_info,
+            strict_json_schema=strict_mode,
         )
 
         async def _on_invoke_tool_impl(ctx: RunContextWrapper[Any], input: str) -> str:
@@ -273,6 +279,7 @@ async def _on_invoke_tool(ctx: RunContextWrapper[Any], input: str) -> str:
             description=schema.description or "",
             params_json_schema=schema.params_json_schema,
             on_invoke_tool=_on_invoke_tool,
+            strict_json_schema=strict_mode,
         )
 
     # If func is actually a callable, we were used as @function_tool with no parentheses
diff --git a/src/agents/tracing/create.py b/src/agents/tracing/create.py
index 8d7fc493..78a064bc 100644
--- a/src/agents/tracing/create.py
+++ b/src/agents/tracing/create.py
@@ -3,7 +3,7 @@
 from collections.abc import Mapping, Sequence
 from typing import TYPE_CHECKING, Any
 
-from .logger import logger
+from ..logger import logger
 from .setup import GLOBAL_TRACE_PROVIDER
 from .span_data import (
     AgentSpanData,
diff --git a/src/agents/tracing/processors.py b/src/agents/tracing/processors.py
index 308adf2a..1b39deda 100644
--- a/src/agents/tracing/processors.py
+++ b/src/agents/tracing/processors.py
@@ -9,7 +9,7 @@
 
 import httpx
 
-from .logger import logger
+from ..logger import logger
 from .processor_interface import TracingExporter, TracingProcessor
 from .spans import Span
 from .traces import Trace
@@ -40,7 +40,7 @@ def __init__(
         """
         Args:
             api_key: The API key for the "Authorization" header. Defaults to
-                `os.environ["OPENAI_TRACE_API_KEY"]` if not provided.
+                `os.environ["OPENAI_API_KEY"]` if not provided.
             organization: The OpenAI organization to use. Defaults to
                 `os.environ["OPENAI_ORG_ID"]` if not provided.
             project: The OpenAI project to use. Defaults to
diff --git a/src/agents/tracing/scope.py b/src/agents/tracing/scope.py
index 9ccd9f87..513ca8c0 100644
--- a/src/agents/tracing/scope.py
+++ b/src/agents/tracing/scope.py
@@ -2,7 +2,7 @@
 import contextvars
 from typing import TYPE_CHECKING, Any
 
-from .logger import logger
+from ..logger import logger
 
 if TYPE_CHECKING:
     from .spans import Span
diff --git a/src/agents/tracing/setup.py b/src/agents/tracing/setup.py
index bc340c9f..3a7c6ade 100644
--- a/src/agents/tracing/setup.py
+++ b/src/agents/tracing/setup.py
@@ -4,8 +4,8 @@
 import threading
 from typing import Any
 
+from ..logger import logger
 from . import util
-from .logger import logger
 from .processor_interface import TracingProcessor
 from .scope import Scope
 from .spans import NoOpSpan, Span, SpanImpl, TSpanData
diff --git a/src/agents/tracing/spans.py b/src/agents/tracing/spans.py
index d682a9a0..ee933e73 100644
--- a/src/agents/tracing/spans.py
+++ b/src/agents/tracing/spans.py
@@ -6,8 +6,8 @@
 
 from typing_extensions import TypedDict
 
+from ..logger import logger
 from . import util
-from .logger import logger
 from .processor_interface import TracingProcessor
 from .scope import Scope
 from .span_data import SpanData
diff --git a/src/agents/tracing/traces.py b/src/agents/tracing/traces.py
index bf3b43df..53d06284 100644
--- a/src/agents/tracing/traces.py
+++ b/src/agents/tracing/traces.py
@@ -4,8 +4,8 @@
 import contextvars
 from typing import Any
 
+from ..logger import logger
 from . import util
-from .logger import logger
 from .processor_interface import TracingProcessor
 from .scope import Scope
 
diff --git a/uv.lock b/uv.lock
index 9179bd4f..c3af99bd 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,5 +1,4 @@
 version = 1
-revision = 1
 requires-python = ">=3.9"
 
 [[package]]
@@ -783,7 +782,7 @@ wheels = [
 
 [[package]]
 name = "openai-agents"
-version = "0.0.3"
+version = "0.0.4"
 source = { editable = "." }
 dependencies = [
     { name = "griffe" },


Test patch:
diff --git a/tests/test_function_tool_decorator.py b/tests/test_function_tool_decorator.py
index 3a47deb4..b5816606 100644
--- a/tests/test_function_tool_decorator.py
+++ b/tests/test_function_tool_decorator.py
@@ -1,6 +1,6 @@
 import asyncio
 import json
-from typing import Any
+from typing import Any, Optional
 
 import pytest
 
@@ -142,3 +142,52 @@ async def test_no_error_on_invalid_json_async():
     tool = will_not_fail_on_bad_json_async
     result = await tool.on_invoke_tool(ctx_wrapper(), "{not valid json}")
     assert result == "error_ModelBehaviorError"
+
+
+@function_tool(strict_mode=False)
+def optional_param_function(a: int, b: Optional[int] = None) -> str:
+    if b is None:
+        return f"{a}_no_b"
+    return f"{a}_{b}"
+
+
+@pytest.mark.asyncio
+async def test_optional_param_function():
+    tool = optional_param_function
+
+    input_data = {"a": 5}
+    output = await tool.on_invoke_tool(ctx_wrapper(), json.dumps(input_data))
+    assert output == "5_no_b"
+
+    input_data = {"a": 5, "b": 10}
+    output = await tool.on_invoke_tool(ctx_wrapper(), json.dumps(input_data))
+    assert output == "5_10"
+
+
+@function_tool(strict_mode=False)
+def multiple_optional_params_function(
+    x: int = 42,
+    y: str = "hello",
+    z: Optional[int] = None,
+) -> str:
+    if z is None:
+        return f"{x}_{y}_no_z"
+    return f"{x}_{y}_{z}"
+
+
+
+@pytest.mark.asyncio
+async def test_multiple_optional_params_function():
+    tool = multiple_optional_params_function
+
+    input_data: dict[str,Any] = {}
+    output = await tool.on_invoke_tool(ctx_wrapper(), json.dumps(input_data))
+    assert output == "42_hello_no_z"
+
+    input_data = {"x": 10, "y": "world"}
+    output = await tool.on_invoke_tool(ctx_wrapper(), json.dumps(input_data))
+    assert output == "10_world_no_z"
+
+    input_data = {"x": 10, "y": "world", "z": 99}
+    output = await tool.on_invoke_tool(ctx_wrapper(), json.dumps(input_data))
+    assert output == "10_world_99"
diff --git a/tests/test_openai_chatcompletions_converter.py b/tests/test_openai_chatcompletions_converter.py
index 8cf07d7c..73acb8ab 100644
--- a/tests/test_openai_chatcompletions_converter.py
+++ b/tests/test_openai_chatcompletions_converter.py
@@ -393,3 +393,38 @@ def test_unknown_object_errors():
     with pytest.raises(UserError, match="Unhandled item type or structure"):
         # Purposely ignore the type error
         _Converter.items_to_messages([TestObject()])  # type: ignore
+
+
+def test_assistant_messages_in_history():
+    """
+    Test that assistant messages are added to the history.
+    """
+    messages = _Converter.items_to_messages(
+        [
+            {
+                "role": "user",
+                "content": "Hello",
+            },
+            {
+                "role": "assistant",
+                "content": "Hello?",
+            },
+            {
+                "role": "user",
+                "content": "What was my Name?",
+            },
+        ]
+    )
+
+    assert messages == [
+        {"role": "user", "content": "Hello"},
+        {"role": "assistant", "content": "Hello?"},
+        {"role": "user", "content": "What was my Name?"},
+    ]
+    assert len(messages) == 3
+    assert messages[0]["role"] == "user"
+    assert messages[0]["content"] == "Hello"
+    assert messages[1]["role"] == "assistant"
+    assert messages[1]["content"] == "Hello?"
+    assert messages[2]["role"] == "user"
+    assert messages[2]["content"] == "What was my Name?"
diff --git a/tests/test_openai_chatcompletions_stream.py b/tests/test_openai_chatcompletions_stream.py
index 2a15f7f0..7add92a6 100644
--- a/tests/test_openai_chatcompletions_stream.py
+++ b/tests/test_openai_chatcompletions_stream.py
@@ -107,6 +107,11 @@ async def patched_fetch_response(self, *args, **kwargs):
     assert isinstance(completed_resp.output[0].content[0], ResponseOutputText)
     assert completed_resp.output[0].content[0].text == "Hello"
 
+    assert completed_resp.usage, "usage should not be None"
+    assert completed_resp.usage.input_tokens == 7
+    assert completed_resp.usage.output_tokens == 5
+    assert completed_resp.usage.total_tokens == 12
+
 
 @pytest.mark.allow_call_model_methods
 @pytest.mark.asyncio