[BUG] hertzbeat-ai sends a hardcoded temperature to every provider and derives baseUrl/model from a fixed code list
Is there an existing issue for this?
- I have searched the existing issues
Current Behavior
Two related things in hertzbeat-ai that make non-OpenAI providers awkward to set up. Both are on
current master (2.0-SNAPSHOT). I ran the app against a local recording proxy to see what it
actually sends rather than guessing from the code.
1. temperature is fixed in code.
hertzbeat-ai/.../config/LlmConfig.java:104-109 builds the options with .temperature(0.3), and
ModelProviderConfig (hertzbeat-common-core/.../entity/dto/ModelProviderConfig.java:33-54) only
has type / code / baseUrl / model / apiKey / participationModel, so there is no way to change or
drop it. This is the body the proxy recorded, with message contents and tool schemas left out:
{
"model": "deepseek-reasoner",
"temperature": 0.3,
"stream": true,
"stream_options": { "include_usage": true },
"tools": [ "..." ],
"messages": [ "..." ]
}Those six keys are all that goes out. top_p, n, tool_choice, response_format and
max_tokens are not sent. So the one generation parameter that does get sent is the one a model is
most likely to disagree about, and an operator cannot touch it.
2. baseUrl and model are filled in from a hardcoded list of code values.
LlmConfig.java:79-101: when baseUrl is empty it is chosen by code, and the else branch sets
https://api.openai.com/v1. model works the same way and falls through to gpt-5 (I read that
off the source, I could not confirm it on the wire since the request is rejected before the model is
checked). Nothing is logged about the fallthrough, so all you get back is an error from a host you
never configured.
Expected Behavior
Generation parameters that are fixed in code today should be settable per provider, and left out of
the request when not set. I am not saying 0.3 is the wrong default, it should stay the default,
this is only about making it overridable.
A code the backend does not know should fail or at least warn, instead of quietly inheriting
OpenAI's endpoint.
Steps To Reproduce
POST /api/config/providerwith{"type":"provider","code":"deepseek","apiKey":"sk-invalid","participationModel":"PROTECTED"}, leavebaseUrlemptyPOST /api/chat/streamwith{"message":"hi"}- the error comes back fromapi.openai.com, see logs below- For the temperature part, point any provider
baseUrlat a local listener and send one chat message; the outbound body carriestemperatureregardless of provider or model
Environment
- Version: 2.0-SNAPSHOT (master)
- Module: hertzbeat-ai
- OS: macOS, JDK 25, embedded H2
Debug logs
The key was a throwaway value, so the reply below is the vendor's own masked text. Note the host in
the OkHttp thread name, and that LlmConfig only logs bean lifecycle at that moment:
2026-09-21 09:43:59 [http-nio-1157-exec-1] INFO org.apache.hertzbeat.ai.config.LlmConfig - Provider configuration change event received, refreshing ChatClient bean
2026-09-21 09:44:17 [OkHttp https://api.openai.com/...] ERROR org.springframework.ai.chat.model.MessageAggregator - Aggregation Error
java.util.concurrent.CompletionException: com.openai.errors.UnauthorizedException: 401: Incorrect API key provided: sk-fake-***********nope. You can find your API key at https://platform.openai.com/account/api-keys.
at com.openai.core.http.RetryingHttpClient.executeAsync$executeWithRetries$lambda$1(RetryingHttpClient.kt:127)
at org.springframework.ai.openai.http.okhttp.SpringAiOpenAiHttpClient$1.onResponse(SpringAiOpenAiHttpClient.java:137)It also reaches the chat client as-is:
event:error
data:{"conversationId":4,"response":"An error occurred: com.openai.errors.UnauthorizedException: 401: Incorrect API key provided: sk-fake-***********nope. You can find your API key at https://platform.openai.com/account/api-keys.","userMessageId":5,"assistantMessageId":null}Anything else?
On how far this reaches: the UI cannot produce case 2, since
web-app/src/app/pojo/ModelProviderConfig.ts:37-56 only offers openai / zai / zhipu in the
dropdown and fills in a baseUrl for them. Case 2 needs POST /api/config/provider, which accepts
any code string without checking it. Case 1 does affect anyone using the UI.
This is not the cause of #3971 and I am not proposing a fix for it here. That reporter did set
https://api.deepseek.com/v1, so the empty-baseUrl branch is never entered, and I have not
reproduced their 400 myself.
I'd like to work on this. What I have in mind is an optional parameter field on ModelProviderConfig
plus the matching change in LlmConfig, a warn when code is unknown, and unit tests for
LlmConfig, which has none today. No new provider names hardcoded. Say the word if you would rather
see it done differently and I'll adjust before opening the PR.
Source: apache/hertzbeat