Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/content/docs/development/chat_models.md
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ Azure OpenAI provides access to OpenAI models (GPT-4, GPT-4o, etc.) through Azur
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `api_key` | str | Required | Azure OpenAI API key for authentication |
| `api_version` | str | Required | Azure OpenAI REST API version (e.g., "2024-02-15-preview"). See [API versions](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#rest-api-versioning) |
| `api_version` | str | Required | Azure OpenAI REST API version (e.g., "2024-10-21"). See [API versions](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#rest-api-versioning) |
| `azure_endpoint` | str | Required | Azure OpenAI endpoint URL (e.g., `https://{resource-name}.openai.azure.com`) |
| `timeout` | float | `60.0` | API request timeout in seconds |
| `max_retries` | int | `3` | Maximum number of API retry attempts |
Expand All @@ -518,7 +518,7 @@ Azure OpenAI provides access to OpenAI models (GPT-4, GPT-4o, etc.) through Azur
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `api_key` | String | Required | Azure OpenAI API key for authentication |
| `api_version` | String | Required | Azure OpenAI REST API version (e.g., "2024-02-01"). See [API versions](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#rest-api-versioning) |
| `api_version` | String | Required | Azure OpenAI REST API version (e.g., "2024-10-21"). See [API versions](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#rest-api-versioning) |
| `azure_endpoint` | String | Required | Azure OpenAI endpoint URL (e.g., `https://{resource-name}.openai.azure.com`) — either a direct Azure resource or a proxy/gateway URL that fronts an Azure OpenAI service |
| `timeout` | int | None | Timeout in seconds for API requests; must be greater than 0, otherwise ignored (SDK default applies) |
| `max_retries` | int | None | Maximum number of API retry attempts; must be non-negative, otherwise ignored (SDK default applies) |
Expand Down Expand Up @@ -580,7 +580,7 @@ class MyAgent(Agent):
return ResourceDescriptor(
clazz=ResourceName.ChatModel.AZURE_OPENAI_CONNECTION,
api_key="<your-api-key>",
api_version="2024-02-15-preview",
api_version="2024-10-21",
azure_endpoint="https://your-resource.openai.azure.com"
)

Expand All @@ -606,7 +606,7 @@ public class MyAgent extends Agent {
public static ResourceDescriptor azureOpenAIConnection() {
return ResourceDescriptor.Builder.newBuilder(ResourceName.ChatModel.AZURE_OPENAI_CONNECTION)
.addInitialArgument("api_key", "<your-api-key>")
.addInitialArgument("api_version", "2024-02-01")
.addInitialArgument("api_version", "2024-10-21")
.addInitialArgument("azure_endpoint", "https://your-resource.openai.azure.com")
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.core.JsonSchemaLocalValidation;
import com.openai.core.JsonValue;
import com.openai.models.ChatModel;
import com.openai.models.FunctionDefinition;
import com.openai.models.FunctionParameters;
import com.openai.models.ReasoningEffort;
import com.openai.models.ResponseFormatJsonSchema;
import com.openai.models.chat.completions.ChatCompletion;
import com.openai.models.chat.completions.ChatCompletionCreateParams;
import com.openai.models.chat.completions.ChatCompletionFunctionTool;
Expand All @@ -43,6 +45,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* A chat model integration for the OpenAI Chat Completions service using the official Java SDK.
Expand Down Expand Up @@ -119,11 +122,70 @@ public OpenAICompletionsConnection(
this.client = builder.build();
}

// Models for which OpenAI documents json_schema strict Structured Outputs support.
// Source of truth: https://platform.openai.com/docs/guides/structured-outputs
//
// A name carrying a non-text modality marker is rejected before anything else. Audio, realtime,
// speech and transcription variants expose no json_schema response format even though they
// share the name prefix of a capable text family, so the marker check has to win over a prefix
// match rather than merely coexist with it.
//
// A text family whose entire lifetime post-dates the Structured Outputs cutoff is matched by
// name prefix, so its dated snapshots and size variants resolve without enumerating each one.
//
// Two cases cannot use a prefix and are matched exactly instead. The gpt-4o family straddles
// the cutoff — gpt-4o-2024-05-13 predates it and is not capable — so the boundary there is
// temporal rather than nominal. The o1 family is not uniform: o1 is capable while o1-mini is
// not, so an "o1" prefix would admit an incapable sibling.
//
// A name outside every listed family reports not-capable and degrades to the prompt fallback
// rather than failing at the provider. Within a listed family the prefix assumes capability,
// so a family variant that ships without json_schema support has to be excluded explicitly,
// either by a marker that appears in no capable name or by replacing the family prefix with
// exact names.
private static final Set<String> NON_TEXT_MODALITY_MARKERS =
Set.of("-audio", "-realtime", "-tts", "-transcribe");
private static final Set<String> NATIVE_STRUCTURED_OUTPUT_FAMILY_PREFIXES =
Set.of("gpt-4o-mini", "gpt-4o-search-preview", "gpt-4.1", "gpt-5", "o3", "o4-mini");
private static final Set<String> NATIVE_STRUCTURED_OUTPUT_MODELS =
Set.of("gpt-4o", "gpt-4o-2024-08-06", "gpt-4o-2024-11-20", "o1", "o1-2024-12-17");

@Override
protected boolean supportsNativeStructuredOutput(String effectiveModel) {
if (effectiveModel == null) {
return false;
}
if (NON_TEXT_MODALITY_MARKERS.stream().anyMatch(effectiveModel::contains)) {
return false;
}
return NATIVE_STRUCTURED_OUTPUT_FAMILY_PREFIXES.stream()
.anyMatch(effectiveModel::startsWith)
|| NATIVE_STRUCTURED_OUTPUT_MODELS.contains(effectiveModel);
}

@Override
public ChatMessage chat(
List<ChatMessage> messages, List<Tool> tools, Map<String, Object> modelParams) {
return doChat(messages, tools, modelParams, null);
}

@Override
public ChatMessage chat(
List<ChatMessage> messages,
List<Tool> tools,
Map<String, Object> modelParams,
Object outputSchema) {
return doChat(messages, tools, modelParams, outputSchema);
}

private ChatMessage doChat(
List<ChatMessage> messages,
List<Tool> tools,
Map<String, Object> modelParams,
Object outputSchema) {
try {
ChatCompletionCreateParams params = buildRequest(messages, tools, modelParams);
ChatCompletionCreateParams params =
buildRequest(messages, tools, modelParams, outputSchema);
ChatCompletion completion = client.chat().completions().create(params);
ChatMessage response =
OpenAIChatCompletionsUtils.convertFromOpenAIMessage(
Expand All @@ -150,8 +212,13 @@ public ChatMessage chat(
}
}

private ChatCompletionCreateParams buildRequest(
List<ChatMessage> messages, List<Tool> tools, Map<String, Object> rawModelParams) {
// Package-private so the request body (including the native response_format) can be asserted
// without issuing a live API call through the final OpenAI client.
ChatCompletionCreateParams buildRequest(
List<ChatMessage> messages,
List<Tool> tools,
Map<String, Object> rawModelParams,
Object outputSchema) {
Map<String, Object> modelParams =
rawModelParams != null ? new HashMap<>(rawModelParams) : new HashMap<>();

Expand All @@ -170,6 +237,19 @@ private ChatCompletionCreateParams buildRequest(
builder.tools(convertTools(tools, strictMode));
}

// Native structured output applies only for a POJO Class schema on a model the provider
// documents as capable; a RowTypeInfo (wrapped in OutputSchema) or an incapable model keeps
// the prompt-engineering fallback.
//
// TODO(#912): the requested strategy is not visible here, so this re-check cannot tell an
// explicit NATIVE request apart from one that merely resolved to native. A caller asking
// for NATIVE on a model this predicate rejects therefore gets an unconstrained response
// instead of an error. Once strategy resolution is wired up, NATIVE must either bypass
// this capability re-check or fail explicitly.
if (outputSchema instanceof Class && supportsNativeStructuredOutput(modelName)) {
builder.responseFormat(toNativeResponseFormat((Class<?>) outputSchema));
}

Object temperature = modelParams.remove("temperature");
if (temperature instanceof Number) {
builder.temperature(((Number) temperature).doubleValue());
Expand Down Expand Up @@ -208,6 +288,26 @@ private ChatCompletionCreateParams buildRequest(
return builder.build();
}

// Derives the strict json_schema response format from a POJO class via the SDK's typed
// structured-output builder. The Kotlin-facade StructuredOutputsKt.responseFormatFromClass is
// not callable from Java, so the response format is extracted through the typed builder, which
// generates the same strict draft-2020-12 schema, and then reattached to the standard builder.
private static <T> ResponseFormatJsonSchema toNativeResponseFormat(Class<T> schemaClass) {
return ChatCompletionCreateParams.builder()
.model(ChatModel.of(""))
.addUserMessage("")
.responseFormat(schemaClass, JsonSchemaLocalValidation.NO)
.build()
.rawParams()
.responseFormat()
.orElseThrow(
() ->
new IllegalStateException(
"OpenAI SDK did not produce a response_format for schema "
+ schemaClass.getName()))
.asJsonSchema();
}

private List<ChatCompletionTool> convertTools(List<Tool> tools, boolean strictMode) {
List<ChatCompletionTool> openaiTools = new ArrayList<>(tools.size());
for (Tool tool : tools) {
Expand Down
Loading
Loading