spring-projects / spring-projects/spring-ai

Since 2.0.1, OpenAI per-request timeout silently drops to 60s for any Prompt carrying options (DEFAULT_TIMEOUT overrides the configured client timeout)

Open
#6,915 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

status: waiting-for-triage
Dominant language
Java
Stars
9.5k
Forks
2.9k
Avg merge
1d 7h
Merged PRs (30d)
6

Description

Since 2.0.1, OpenAI per-request timeout silently drops to 60s for any Prompt carrying options (DEFAULT_TIMEOUT overrides the configured client timeout)

Bug description

Spring AI 2.0.1 introduced per-request timeout support in the OpenAI module: OpenAiChatModel.buildRequestOptions(Prompt) reads ((OpenAiChatOptions) prompt.getOptions()).getTimeout() and passes it as com.openai.core.RequestOptions, which SpringAiOpenAiHttpClient.newCall applies as OkHttp read/write/call timeouts, overriding the client-level timeouts for that call.

Combined with two pre-existing behaviors, this silently caps every request at 60 seconds for a very common usage pattern:

  1. OpenAiChatOptions' constructor replaces an unset timeout with a non-null default: this.timeout = (timeout != null ? timeout : AbstractOpenAiOptions.DEFAULT_TIMEOUT) where DEFAULT_TIMEOUT = Duration.ofSeconds(60).
  2. OpenAiChatModel.buildRequestPrompt(Prompt) does not merge default options into prompt options — if the prompt carries any options object, the model's default options (including a deliberately configured timeout) are ignored entirely.

So any application that attaches per-prompt OpenAiChatOptions — the standard way to pass tools, temperature, or model per request — gets timeout = 60s on those options implicitly, and that non-null value wins over whatever timeout was configured on the model's default options / HTTP client. The user-configured timeout is applied to the underlying OkHttp client via OpenAiSetup, but the per-request value overrides it on every call, so it is unreachable in practice.

In 2.0.0 this could not happen: OpenAiChatModel called openAiClient.chat().completions().create(request) without RequestOptions, so the client-level timeout always governed. This is therefore a behavioral regression in 2.0.1 for existing applications.

The same constructor-defaulting applies to maxRetries (DEFAULT_MAX_RETRIES = 3), which is harder to notice but has the same "explicit-looking value from an implicit default" problem when options objects are inspected or copied.

Note: AnthropicChatOptions does not have this problem — it leaves an unset timeout null, and AnthropicChatModel.requestOptionsFor falls back to RequestOptions.none(), so the client-level timeout applies. The OpenAI module's non-null default is the outlier.

How we hit it (real-world impact)

Azure OpenAI GPT-5.1 non-streaming chat completions routinely take longer than 60s before the first response byte (we measured ~120s server-side for a ~4k-token completion). After upgrading 2.0.0 → 2.0.1:

  • every such call was killed client-side at exactly 60s (InterruptedIOException: timeout from OkHttp's call-timeout watchdog while waiting for response headers),
  • the openai-java RetryingHttpClient then retried it 3 more times (the DEFAULT_MAX_RETRIES = 3 from the same options object reaches the client builder), each attempt capped at the same 60s,
  • net effect: 4 failed attempts ≈ 4 minutes wall time, 4 full generations billed by the provider per user request, then failure — while our explicitly configured 5-minute timeout appeared to be set correctly in code.

Diagnosing this was expensive precisely because the misbehavior looks like a provider/network problem: the configured timeout is visibly 5 minutes, and nothing logs that a 60s default replaced it.

Environment

  • Spring AI 2.0.1 (spring-ai-openai), openai-java 4.49.0
  • Spring Boot 4.1, Java 21
  • Azure OpenAI via OpenAiChatOptions.azure(true) (plain OpenAI is equally affected)

Steps to reproduce

OpenAiChatOptions defaults = OpenAiChatOptions.builder()
        .model("gpt-5.1")
        .timeout(Duration.ofMinutes(5))      // user intent: allow slow generations
        .build();

OpenAiChatModel model = OpenAiChatModel.builder().options(defaults).build();

// Standard per-request options (tools/temperature/model) — timeout not set:
OpenAiChatOptions promptOptions = OpenAiChatOptions.builder()
        .model("gpt-5.1")
        .temperature(0.2)
        .build();

// promptOptions.getTimeout() is already Duration.ofSeconds(60), not null.
model.call(new Prompt(messages, promptOptions));
// -> RequestOptions.timeout = 60s overrides the client-level 5m;
//    any completion slower than 60s fails with a client-side timeout
//    and is retried up to 3 times (DEFAULT_MAX_RETRIES).

Expected: the request uses the configured 5-minute timeout (as in 2.0.0), or at minimum the per-request override only applies when the user explicitly set a timeout on the prompt options.

Actual: the request is capped at 60 seconds.

Suggested fix

Any of these would resolve it (first one seems cleanest):

  1. Stop defaulting timeout/maxRetries in the OpenAiChatOptions constructor — keep them null when unset (as AnthropicChatOptions does), and let buildRequestOptions skip null so the client-level configuration applies. OpenAiSetup already handles the client-side defaulting.
  2. Alternatively, have buildRequestPrompt merge default options into prompt options so a configured default timeout survives when the prompt options don't set one.
  3. At minimum: document prominently that since 2.0.1 a prompt-level options object implies a 60s request timeout unless timeout is set explicitly.

Workaround (for anyone else hitting this)

Set .timeout(...) (and .maxRetries(...)) explicitly on every per-prompt OpenAiChatOptions you create — setting them on the model's default options alone has no effect on requests that carry prompt-level options.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with OpenAiChatOptions constructor defaults and OpenAiChatModel.buildRequestOptions(Prompt) and buildRequestPrompt(Prompt); then inspect SpringAiOpenAiHttpClient.newCall and OpenAiSetup for timeout precedence. Reproduce the provided model-default and prompt-options scenario, and verify that an unset prompt timeout no longer overrides the configured client or model timeout while an explicit override still works.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, spring
Domain
api, backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.