Skip to content

Commit c0885ca

Browse files
release: 2.5.0 (#503)
* feat(client): automatic schema generation and arg parsing for function calling (#497) * tool-calls: structured function calls without docs. * fn-calling: backfill missing functions and tests. * fn-calling: draft documentation and some review updates. * fn-calling: raw example for Responses API. * fn-calling: minor changes from PR review. * release: 2.5.0 --------- Co-authored-by: D Gardner <damien@gardnerhrm.com> Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
1 parent b8a7201 commit c0885ca

22 files changed

+1380
-135
lines changed

.release-please-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "2.4.0"
2+
".": "2.5.0"
33
}

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## 2.5.0 (2025-06-06)
4+
5+
Full Changelog: [v2.4.0...v2.5.0](https://github.com/openai/openai-java/compare/v2.4.0...v2.5.0)
6+
7+
### Features
8+
9+
* **client:** automatic schema generation and arg parsing for function calling ([#497](https://github.com/openai/openai-java/issues/497)) ([800dc8f](https://github.com/openai/openai-java/commit/800dc8fcf1e7109c419fb6125f6ad2822b6d6748))
10+
311
## 2.4.0 (2025-06-03)
412

513
Full Changelog: [v2.3.2...v2.4.0](https://github.com/openai/openai-java/compare/v2.3.2...v2.4.0)

README.md

Lines changed: 199 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22

33
<!-- x-release-please-start-version -->
44

5-
[![Maven Central](https://img.shields.io/maven-central/v/com.openai/openai-java)](https://central.sonatype.com/artifact/com.openai/openai-java/2.4.0)
6-
[![javadoc](https://javadoc.io/badge2/com.openai/openai-java/2.4.0/javadoc.svg)](https://javadoc.io/doc/com.openai/openai-java/2.4.0)
5+
[![Maven Central](https://img.shields.io/maven-central/v/com.openai/openai-java)](https://central.sonatype.com/artifact/com.openai/openai-java/2.5.0)
6+
[![javadoc](https://javadoc.io/badge2/com.openai/openai-java/2.5.0/javadoc.svg)](https://javadoc.io/doc/com.openai/openai-java/2.5.0)
77

88
<!-- x-release-please-end -->
99

1010
The OpenAI Java SDK provides convenient access to the [OpenAI REST API](https://platform.openai.com/docs) from applications written in Java.
1111

1212
<!-- x-release-please-start-version -->
1313

14-
The REST API documentation can be found on [platform.openai.com](https://platform.openai.com/docs). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.openai/openai-java/2.4.0).
14+
The REST API documentation can be found on [platform.openai.com](https://platform.openai.com/docs). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.openai/openai-java/2.5.0).
1515

1616
<!-- x-release-please-end -->
1717

@@ -22,7 +22,7 @@ The REST API documentation can be found on [platform.openai.com](https://platfor
2222
### Gradle
2323

2424
```kotlin
25-
implementation("com.openai:openai-java:2.4.0")
25+
implementation("com.openai:openai-java:2.5.0")
2626
```
2727

2828
### Maven
@@ -31,7 +31,7 @@ implementation("com.openai:openai-java:2.4.0")
3131
<dependency>
3232
<groupId>com.openai</groupId>
3333
<artifactId>openai-java</artifactId>
34-
<version>2.4.0</version>
34+
<version>2.5.0</version>
3535
</dependency>
3636
```
3737

@@ -533,6 +533,200 @@ If you use `@JsonProperty(required = false)`, the `false` value will be ignored.
533533
must mark all properties as _required_, so the schema generated from your Java classes will respect
534534
that restriction and ignore any annotation that would violate it.
535535

536+
## Function calling with JSON schemas
537+
538+
OpenAI [Function Calling](https://platform.openai.com/docs/guides/function-calling?api-mode=chat)
539+
lets you integrate external functions directly into the language model's responses. Instead of
540+
producing plain text, the model can output instructions (with parameters) for calling a function
541+
when appropriate. You define a [JSON schema](https://json-schema.org/overview/what-is-jsonschema)
542+
for functions, and the model uses it to decide when and how to trigger these calls, enabling more
543+
interactive, data-driven applications.
544+
545+
A JSON schema describing a function's parameters can be defined via the API by building a
546+
[`ChatCompletionTool`](openai-java-core/src/main/kotlin/com/openai/models/chat/completions/ChatCompletionTool.kt)
547+
containing a
548+
[`FunctionDefinition`](openai-java-core/src/main/kotlin/com/openai/models/FunctionDefinition.kt)
549+
and then using `addTool` to set it on the input parameters. The response from the AI model may then
550+
contain requests to call your functions, detailing the functions' names and their parameter values
551+
as JSON data that conforms to the JSON schema from the function definition. You can then parse the
552+
parameter values from this JSON, invoke your functions, and pass your functions' results back to the
553+
AI model. A full, working example of _Function Calling_ using the low-level API can be seen in
554+
[`FunctionCallingRawExample`](openai-java-example/src/main/java/com/openai/example/FunctionCallingRawExample.java).
555+
556+
However, for greater convenience, the SDK can derive a function and its parameters automatically
557+
from the structure of an arbitrary Java class: the class's name provides the function name, and the
558+
class's fields define the function's parameters. When the AI model responds with the parameter
559+
values in JSON form, you can then easily convert that JSON to an instance of your Java class and
560+
use the parameter values to invoke your custom function. A full, working example of the use of
561+
_Function Calling_ with Java classes to define function parameters can be seen in
562+
[`FunctionCallingExample`](openai-java-example/src/main/java/com/openai/example/FunctionCallingExample.java).
563+
564+
Like for [Structured Outputs](#structured-outputs-with-json-schemas), Java classes can contain
565+
fields declared to be instances of other classes and can use collections. Optionally, annotations
566+
can be used to set the descriptions of the function (class) and its parameters (fields) to assist
567+
the AI model in understanding the purpose of the function and the possible values of its parameters.
568+
569+
```java
570+
import com.fasterxml.jackson.annotation.JsonClassDescription;
571+
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
572+
573+
@JsonClassDescription("Gets the quality of the given SDK.")
574+
static class GetSdkQuality {
575+
@JsonPropertyDescription("The name of the SDK.")
576+
public String name;
577+
578+
public SdkQuality execute() {
579+
return new SdkQuality(
580+
name, name.contains("OpenAI") ? "It's robust and polished!" : "*shrug*");
581+
}
582+
}
583+
584+
static class SdkQuality {
585+
public String quality;
586+
587+
public SdkQuality(String name, String evaluation) {
588+
quality = name + ": " + evaluation;
589+
}
590+
}
591+
592+
@JsonClassDescription("Gets the review score (out of 10) for the named SDK.")
593+
static class GetSdkScore {
594+
public String name;
595+
596+
public int execute() {
597+
return name.contains("OpenAI") ? 10 : 3;
598+
}
599+
}
600+
```
601+
602+
When your functions are defined, add them to the input parameters using `addTool(Class<T>)` and then
603+
call them if requested to do so in the AI model's response. `Function.argments(Class<T>)` can be
604+
used to parse a function's parameters in JSON form to an instance of your function-defining class.
605+
The fields of that instance will be set to the values of the parameters to the function call.
606+
607+
After calling the function, use `ChatCompletionToolMessageParam.Builder.contentAsJson(Object)` to
608+
pass the function's result back to the AI model. The method will convert the result to JSON form
609+
for consumption by the model. The `Object` can be any object, including simple `String` instances
610+
and boxed primitive types.
611+
612+
```java
613+
import com.openai.client.OpenAIClient;
614+
import com.openai.client.okhttp.OpenAIOkHttpClient;
615+
import com.openai.models.ChatModel;
616+
import com.openai.models.chat.completions.*;
617+
import java.util.Collection;
618+
619+
OpenAIClient client = OpenAIOkHttpClient.fromEnv();
620+
621+
ChatCompletionCreateParams.Builder createParamsBuilder = ChatCompletionCreateParams.builder()
622+
.model(ChatModel.GPT_3_5_TURBO)
623+
.maxCompletionTokens(2048)
624+
.addTool(GetSdkQuality.class)
625+
.addTool(GetSdkScore.class)
626+
.addUserMessage("How good are the following SDKs and what do reviewers say: "
627+
+ "OpenAI Java SDK, Unknown Company SDK.");
628+
629+
client.chat().completions().create(createParamsBuilder.build()).choices().stream()
630+
.map(ChatCompletion.Choice::message)
631+
// Add each assistant message onto the builder so that we keep track of the
632+
// conversation for asking a follow-up question later.
633+
.peek(createParamsBuilder::addMessage)
634+
.flatMap(message -> {
635+
message.content().ifPresent(System.out::println);
636+
return message.toolCalls().stream().flatMap(Collection::stream);
637+
})
638+
.forEach(toolCall -> {
639+
Object result = callFunction(toolCall.function());
640+
// Add the tool call result to the conversation.
641+
createParamsBuilder.addMessage(ChatCompletionToolMessageParam.builder()
642+
.toolCallId(toolCall.id())
643+
.contentAsJson(result)
644+
.build());
645+
});
646+
647+
// Ask a follow-up question about the function call result.
648+
createParamsBuilder.addUserMessage("Why do you say that?");
649+
client.chat().completions().create(createParamsBuilder.build()).choices().stream()
650+
.flatMap(choice -> choice.message().content().stream())
651+
.forEach(System.out::println);
652+
653+
static Object callFunction(ChatCompletionMessageToolCall.Function function) {
654+
switch (function.name()) {
655+
case "GetSdkQuality":
656+
return function.arguments(GetSdkQuality.class).execute();
657+
case "GetSdkScore":
658+
return function.arguments(GetSdkScore.class).execute();
659+
default:
660+
throw new IllegalArgumentException("Unknown function: " + function.name());
661+
}
662+
}
663+
```
664+
665+
In the code above, an `execute()` method encapsulates each function's logic. However, there is no
666+
requirement to follow that pattern. You are free to implement your function's logic in any way that
667+
best suits your use case. The pattern above is only intended to _suggest_ that a suitable pattern
668+
may make the process of function calling simpler to understand and implement.
669+
670+
### Usage with the Responses API
671+
672+
_Function Calling_ is also supported for the Responses API. The usage is the same as described
673+
except where the Responses API differs slightly from the Chat Completions API. Pass the top-level
674+
class to `addTool(Class<T>)` when building the parameters. In the response, look for
675+
[`RepoonseOutputItem`](openai-java-core/src/main/kotlin/com/openai/models/responses/ResponseOutputItem.kt)
676+
instances that are function calls. Parse the parameters to each function call to an instance of the
677+
class using
678+
[`ResponseFunctionToolCall.arguments(Class<T>)`](openai-java-core/src/main/kotlin/com/openai/models/responses/ResponseFunctionToolCall.kt).
679+
Finally, pass the result of each call back to the model.
680+
681+
For a full example of the usage of _Function Calling_ with the Responses API using the low-level
682+
API to define and parse function parameters, see
683+
[`ResponsesFunctionCallingRawExample`](openai-java-example/src/main/java/com/openai/example/ResponsesFunctionCallingRawExample.java).
684+
685+
For a full example of the usage of _Function Calling_ with the Responses API using Java classes to
686+
define and parse function parameters, see
687+
[`ResponsesFunctionCallingExample`](openai-java-example/src/main/java/com/openai/example/ResponsesFunctionCallingExample.java).
688+
689+
### Local function JSON schema validation
690+
691+
Like for _Structured Outputs_, you can perform local validation to check that the JSON schema
692+
derived from your function class respects the restrictions imposed by OpenAI on such schemas. Local
693+
validation is enabled by default, but it can be disabled by adding `JsonSchemaLocalValidation.NO` to
694+
the call to `addTool`.
695+
696+
```java
697+
ChatCompletionCreateParams.Builder createParamsBuilder = ChatCompletionCreateParams.builder()
698+
.model(ChatModel.GPT_3_5_TURBO)
699+
.maxCompletionTokens(2048)
700+
.addTool(GetSdkQuality.class, JsonSchemaLocalValidation.NO)
701+
.addTool(GetSdkScore.class, JsonSchemaLocalValidation.NO)
702+
.addUserMessage("How good are the following SDKs and what do reviewers say: "
703+
+ "OpenAI Java SDK, Unknown Company SDK.");
704+
```
705+
706+
See [Local JSON schema validation](#local-json-schema-validation) for more details on local schema
707+
validation and under what circumstances you might want to disable it.
708+
709+
### Annotating function classes
710+
711+
You can use annotations to add further information about functions to the JSON schemas that are
712+
derived from your function classes, or to exclude individual fields from the parameters to the
713+
function. Details from annotations captured in the JSON schema may be used by the AI model to
714+
improve its response. The SDK supports the use of
715+
[Jackson Databind](https://github.com/FasterXML/jackson-databind) annotations.
716+
717+
- Use `@JsonClassDescription` to add a description to a function class detailing when and how to use
718+
that function.
719+
- Use `@JsonTypeName` to set the function name to something other than the simple name of the class,
720+
which is used by default.
721+
- Use `@JsonPropertyDescription` to add a detailed description to function parameter (a field of
722+
a function class).
723+
- Use `@JsonIgnore` to omit a field of a class from the generated JSON schema for a function's
724+
parameters.
725+
726+
OpenAI provides some
727+
[Best practices for defining functions](https://platform.openai.com/docs/guides/function-calling#best-practices-for-defining-functions)
728+
that may help you to understand how to use the above annotations effectively for your functions.
729+
536730
## File uploads
537731

538732
The SDK defines methods that accept files.

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ repositories {
88

99
allprojects {
1010
group = "com.openai"
11-
version = "2.4.0" // x-release-please-version
11+
version = "2.5.0" // x-release-please-version
1212
}
1313

1414
subprojects {

openai-java-core/src/main/kotlin/com/openai/core/JsonSchemaValidator.kt

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -392,36 +392,45 @@ internal class JsonSchemaValidator private constructor() {
392392
// The schema must declare that additional properties are not allowed. For this check, it
393393
// does not matter if there are no "properties" in the schema.
394394
verify(
395-
schema.get(ADDITIONAL_PROPS) != null &&
396-
schema.get(ADDITIONAL_PROPS).asBoolean() == false,
395+
schema.get(ADDITIONAL_PROPS) != null && !schema.get(ADDITIONAL_PROPS).asBoolean(),
397396
path,
398397
) {
399398
"'$ADDITIONAL_PROPS' field is missing or is not set to 'false'."
400399
}
401400

402401
val properties = schema.get(PROPS)
403402

404-
// The "properties" field may be missing (there may be no properties to declare), but if it
405-
// is present, it must be a non-empty object, or validation cannot continue.
406-
// TODO: Decide if a missing or empty "properties" field is OK or not.
403+
// An object schema _must_ have a `"properties"` field, and it must contain at least one
404+
// property. The AI model will report an error relating to a missing or empty `"required"`
405+
// array if the "properties" field is missing or empty (and therefore the `"required"` array
406+
// will also be missing or empty). This condition can arise if a `Map` is used as the field
407+
// type: it will cause the generation of an object schema with no defined properties. If not
408+
// present or empty, validation cannot continue.
407409
verify(
408-
properties == null || (properties.isObject && !properties.isEmpty),
410+
properties != null && properties.isObject && !properties.isEmpty,
409411
path,
410-
{ "'$PROPS' field is not a non-empty object." },
412+
{ "'$PROPS' field is missing, empty or not an object." },
411413
) {
412414
return
413415
}
414416

415-
if (properties != null) { // Must be an object.
416-
// If a "properties" field is present, there must also be a "required" field. All
417-
// properties must be named in the list of required properties.
418-
validatePropertiesRequired(
419-
properties.fieldNames().asSequence().toSet(),
420-
schema.get(REQUIRED),
421-
"$path/$REQUIRED",
422-
)
423-
validateProperties(properties, "$path/$PROPS", depth)
417+
// Similarly, insist that the `"required"` array is present or stop validation.
418+
val required = schema.get(REQUIRED)
419+
420+
verify(
421+
required != null && required.isArray && !required.isEmpty,
422+
path,
423+
{ "'$REQUIRED' field is missing, empty or not an array." },
424+
) {
425+
return
424426
}
427+
428+
validatePropertiesRequired(
429+
properties.fieldNames().asSequence().toSet(),
430+
required,
431+
"$path/$REQUIRED",
432+
)
433+
validateProperties(properties, "$path/$PROPS", depth)
425434
}
426435

427436
/**
@@ -554,10 +563,10 @@ internal class JsonSchemaValidator private constructor() {
554563
*/
555564
private fun validatePropertiesRequired(
556565
propertyNames: Collection<String>,
557-
required: JsonNode?,
566+
required: JsonNode,
558567
path: String,
559568
) {
560-
val requiredNames = required?.map { it.asText() }?.toSet() ?: emptySet()
569+
val requiredNames = required.map { it.asText() }.toSet()
561570

562571
propertyNames.forEach { propertyName ->
563572
verify(propertyName in requiredNames, path) {

0 commit comments

Comments
 (0)