Skip to content

Commit 296ccca

Browse files
Merge pull request #37428 from vespa-engine/add-reasoning-effort-option
Support reasoning_effort in the OpenAI client
2 parents 174296f + 91b699c commit 296ccca

6 files changed

Lines changed: 50 additions & 5 deletions

File tree

model-integration/abi-spec.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"public ai.vespa.llm.clients.LlmClientConfig$Builder model(java.lang.String)",
3737
"public ai.vespa.llm.clients.LlmClientConfig$Builder temperature(double)",
3838
"public ai.vespa.llm.clients.LlmClientConfig$Builder maxTokens(int)",
39+
"public ai.vespa.llm.clients.LlmClientConfig$Builder reasoningEffort(java.lang.String)",
3940
"public final boolean dispatchGetConfig(com.yahoo.config.ConfigInstance$Producer)",
4041
"public final java.lang.String getDefMd5()",
4142
"public final java.lang.String getDefName()",
@@ -78,7 +79,8 @@
7879
"public java.lang.String endpoint()",
7980
"public java.lang.String model()",
8081
"public double temperature()",
81-
"public int maxTokens()"
82+
"public int maxTokens()",
83+
"public java.lang.String reasoningEffort()"
8284
],
8385
"fields" : [
8486
"public static final java.lang.String CONFIG_DEF_MD5",

model-integration/src/main/java/ai/vespa/llm/clients/OpenAI.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.openai.client.OpenAIClientAsync;
1919
import com.openai.core.JsonValue;
2020
import com.openai.models.ResponseFormatJsonSchema;
21+
import com.openai.models.ReasoningEffort;
2122
import com.fasterxml.jackson.core.type.TypeReference;
2223

2324
import java.util.HashMap;
@@ -70,9 +71,14 @@ public OpenAI(LlmClientConfig config, Secrets secretStore) {
7071
if (config.maxTokens() >= 0) {
7172
configOptions.put(InferenceParameters.OPTION_MAX_TOKENS, String.valueOf(config.maxTokens()));
7273
}
74+
75+
if (!config.reasoningEffort().isBlank()) {
76+
configOptions.put(InferenceParameters.OPTION_REASONING_EFFORT, config.reasoningEffort());
77+
}
7378
}
7479

75-
private InferenceParameters prepareParameters(InferenceParameters parameters) {
80+
// Package-private for testing
81+
InferenceParameters prepareParameters(InferenceParameters parameters) {
7682
setApiKey(parameters);
7783
setEndpoint(parameters);
7884
return parameters.withDefaultOptions(configOptions::get);
@@ -183,7 +189,8 @@ public CompletableFuture<Completion.FinishReason> completeAsync(
183189
return future;
184190
}
185191

186-
private ChatCompletionCreateParams getChatCompletionCreateParams(InferenceParameters parameters, Prompt prompt) {
192+
// Package-private for testing
193+
ChatCompletionCreateParams getChatCompletionCreateParams(InferenceParameters parameters, Prompt prompt) {
187194
ChatCompletionCreateParams.Builder builder = ChatCompletionCreateParams.builder()
188195
.model(ChatModel.of(parameters.get(InferenceParameters.OPTION_MODEL).map(Object::toString).orElse(DEFAULT_MODEL)))
189196
.addUserMessage(prompt.toString());
@@ -194,6 +201,8 @@ private ChatCompletionCreateParams getChatCompletionCreateParams(InferenceParame
194201
parameters.getInt(InferenceParameters.OPTION_N_PREDICT).ifPresent(builder::n);
195202
parameters.getDouble(InferenceParameters.OPTION_FREQUENCY_PENALTY).ifPresent(builder::frequencyPenalty);
196203
parameters.getDouble(InferenceParameters.OPTION_PRESENCE_PENALTY).ifPresent(builder::presencePenalty);
204+
parameters.get(InferenceParameters.OPTION_REASONING_EFFORT)
205+
.ifPresent(effort -> builder.reasoningEffort(ReasoningEffort.of(effort)));
197206
// Add JSON schema if specified
198207
addResponseFormat(parameters, builder);
199208

model-integration/src/main/resources/configdefinitions/llm-client.def

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,9 @@ temperature double default=-1
2020

2121
# The maximum number of tokens that can be generated in the completion.
2222
# -1 means use default for the API.
23-
maxTokens int default=-1
23+
maxTokens int default=-1
24+
# Constrains effort on reasoning for reasoning models, e.g. "low", "medium", "high".
25+
# Accepted values depend on the provider (OpenAI also accepts "minimal"; Google's
26+
# OpenAI-compatible API accepts "none"). Passed through to the API as-is.
27+
# Empty means use default for the API.
28+
reasoningEffort string default=""

model-integration/src/test/java/ai/vespa/llm/clients/OpenAITest.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.junit.jupiter.api.Test;
1212
import com.openai.errors.UnauthorizedException;
1313
import com.openai.errors.OpenAIIoException;
14+
import com.openai.models.ReasoningEffort;
1415

1516
import static org.junit.jupiter.api.Assertions.assertTrue;
1617

@@ -69,6 +70,32 @@ public void testCompleteAsync() {
6970
assertNumTokens(text.toString(), 3, 10);
7071
}
7172

73+
@Test
74+
public void testReasoningEffortOption() {
75+
var prompt = StringPrompt.from("hello");
76+
77+
// Not set: not sent to the API
78+
var openai = new OpenAI(new LlmClientConfig.Builder().apiKeySecretName("openai").build(), new MockSecrets());
79+
var params = openai.getChatCompletionCreateParams(new InferenceParameters(Map.<String, String>of()::get), prompt);
80+
assertTrue(params.reasoningEffort().isEmpty());
81+
82+
// Set in component config
83+
var openaiWithConfig = new OpenAI(
84+
new LlmClientConfig.Builder().apiKeySecretName("openai").reasoningEffort("low").build(),
85+
new MockSecrets());
86+
params = openaiWithConfig.getChatCompletionCreateParams(
87+
openaiWithConfig.prepareParameters(new InferenceParameters(Map.<String, String>of()::get)), prompt);
88+
assertEquals(ReasoningEffort.LOW, params.reasoningEffort().orElseThrow());
89+
90+
// Per-request option overrides config; non-enum values pass through
91+
// as-is (providers accept different sets, e.g. "minimal" and "none").
92+
params = openaiWithConfig.getChatCompletionCreateParams(
93+
openaiWithConfig.prepareParameters(
94+
new InferenceParameters(Map.of(InferenceParameters.OPTION_REASONING_EFFORT, "none")::get)),
95+
prompt);
96+
assertEquals(ReasoningEffort.of("none"), params.reasoningEffort().orElseThrow());
97+
}
98+
7299
@Test
73100
public void testClientCaching() {
74101
// Create OpenAI instance

vespajlib/abi-spec.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4324,7 +4324,8 @@
43244324
"public static final java.lang.String OPTION_FREQUENCY_PENALTY",
43254325
"public static final java.lang.String OPTION_PRESENCE_PENALTY",
43264326
"public static final java.lang.String OPTION_SEED",
4327-
"public static final java.lang.String OPTION_JSON_SCHEMA"
4327+
"public static final java.lang.String OPTION_JSON_SCHEMA",
4328+
"public static final java.lang.String OPTION_REASONING_EFFORT"
43284329
]
43294330
},
43304331
"ai.vespa.llm.LanguageModel" : {

vespajlib/src/main/java/ai/vespa/llm/InferenceParameters.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public class InferenceParameters {
3131
public static final String OPTION_PRESENCE_PENALTY = "presencepenalty";
3232
public static final String OPTION_SEED = "seed";
3333
public static final String OPTION_JSON_SCHEMA = "json_schema";
34+
public static final String OPTION_REASONING_EFFORT = "reasoningEffort";
3435

3536
private String apiKey;
3637
private String endpoint;

0 commit comments

Comments
 (0)