001/*
002 *  Copyright (c) 2023-2026, Agents-Flex (fuhai999@gmail.com).
003 *  <p>
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *  <p>
008 *  http://www.apache.org/licenses/LICENSE-2.0
009 *  <p>
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package com.agentsflex.core.model.chat;
017
018import com.agentsflex.core.util.Maps;
019import com.agentsflex.core.util.Metadata;
020import com.agentsflex.core.util.StringUtil;
021
022import java.util.List;
023import java.util.Map;
024
025/**
026 * 聊天选项配置类,用于控制大语言模型(LLM)的生成行为。
027 * 支持 Builder 模式,便于链式调用。
028 * 注意:不同模型厂商对参数的支持和默认值可能不同。
029 */
030public class ChatOptions extends Metadata {
031
032
033    /**
034     * 指定使用的大模型名称。
035     * 例如:"gpt-4", "qwen-max", "claude-3-sonnet" 等。
036     * 如果未设置,将使用客户端默认模型。
037     */
038    private String model;
039
040    /**
041     * 随机种子(Seed),用于控制生成结果的可重复性。
042     * 当 seed 相同时,相同输入将产生相同输出(前提是其他参数也一致)。
043     * 注意:并非所有模型都支持 seed 参数。
044     */
045    private String seed;
046
047    /**
048     * 温度(Temperature)控制输出的随机性。
049     * <ul>
050     *   <li>值越低(如 0.1~0.3):输出更确定、稳定、可重复,适合事实性任务(如 RAG、结构化输出)</li>
051     *   <li>值越高(如 0.7~1.0):输出更多样、有创意,但可能不稳定或偏离事实</li>
052     * </ul>
053     * 推荐值:
054     * <ul>
055     *   <li>文档处理、路由、工具调用:0.1 ~ 0.3</li>
056     *   <li>问答、摘要:0.2 ~ 0.5</li>
057     *   <li>创意写作:0.7 ~ 1.0</li>
058     * </ul>
059     * 默认值:0.5f
060     */
061    private Float temperature = 0.5f;
062
063    /**
064     * Top-p(也称 nucleus sampling)控制生成时考虑的概率质量。
065     * 模型从累积概率不超过 p 的最小词集中采样。
066     * - 值为 1.0 表示考虑所有词(等同于无 top-p 限制)
067     * - 值为 0.9 表示只考虑累积概率达 90% 的词
068     * 注意:temperature 和 top_p 不应同时调整,通常只用其一。
069     */
070    private Float topP;
071
072    /**
073     * Top-k 控制生成时考虑的最高概率词的数量。
074     * 模型仅从 top-k 个最可能的词中采样。
075     * - 值为 50 表示只考虑概率最高的 50 个词
076     * - 值越小,输出越确定;值越大,输出越多样
077     * 注意:与 top_p 类似,通常不与 temperature 同时使用。
078     */
079    private Integer topK;
080
081    /**
082     * 生成内容的最大 token 数量(不包括输入 prompt)。
083     * 用于限制响应长度,防止生成过长内容。
084     * 注意:不同模型有不同上限,超过将被截断或报错。
085     */
086    private Integer maxTokens;
087
088    /**
089     * 停止序列(Stop Sequences),当生成内容包含这些字符串时立即停止。
090     * 例如:设置为 ["\n", "。"] 可在句末或换行时停止。
091     * 适用于需要精确控制输出长度的场景。
092     */
093    private List<String> stop;
094
095    /**
096     * 是否启用“思考模式”(Thinking Mode)。
097     * 适用于支持该特性的模型(如 Qwen3),开启后模型会显式输出推理过程。
098     * 默认为 null(由模型决定)。
099     */
100    private Boolean thinkingEnabled;
101
102    /**
103     * 是否返回 Usage 信息, 仅在 stream 模式下有效。
104     * 适用于支持该特性的模型(如 Qwen3),开启后模型会返回 Usage 信息。
105     * 默认为 true。
106     */
107    private Boolean includeUsage;
108
109    /**
110     * 额外的模型参数,用于传递模型特有或未明确暴露的配置。
111     * 例如:{"response_format": "json", "presence_penalty": 0.5}
112     * 使用 addExtraBody() 方法可方便地添加单个参数。
113     */
114    private Map<String, Object> extraBody;
115
116
117    protected Boolean retryEnabled; // 默认开启错误重试
118    protected Integer retryCount;
119    protected Integer retryInitialDelayMs;
120
121
122    private Map<String, Object> responseFormat;
123
124    /**
125     * 是否为流式请求。
126     * 这个不允许用户设置,由 Framework 自动设置(用户设置也可能被修改)。
127     * 用户调用 chat 或者 chatStream 方法时,会自动设置这个字段。
128     */
129    private boolean streaming;
130
131    // ===== 构造函数 =====
132    public ChatOptions() {
133    }
134
135    private ChatOptions(Builder builder) {
136        this.model = builder.model;
137        this.seed = builder.seed;
138        this.temperature = builder.temperature;
139        this.topP = builder.topP;
140        this.topK = builder.topK;
141        this.maxTokens = builder.maxTokens;
142        this.stop = builder.stop;
143        this.thinkingEnabled = builder.thinkingEnabled;
144        this.includeUsage = builder.includeUsage;
145        this.extraBody = builder.extraBody;
146        this.retryEnabled = builder.retryEnabled;
147        this.retryCount = builder.retryCount;
148        this.retryInitialDelayMs = builder.retryInitialDelayMs;
149        this.responseFormat = builder.responseFormat;
150
151        if (builder.metadata != null && !builder.metadata.isEmpty()){
152            this.putMetadata(builder.metadata);
153        }
154    }
155
156    // ===== Getter / Setter =====
157
158    public String getModel() {
159        return model;
160    }
161
162    public String getModelOrDefault(String defaultModel) {
163        return StringUtil.hasText(model) ? model : defaultModel;
164    }
165
166    public void setModel(String model) {
167        this.model = model;
168    }
169
170    public String getSeed() {
171        return seed;
172    }
173
174    public void setSeed(String seed) {
175        this.seed = seed;
176    }
177
178    public Float getTemperature() {
179        return temperature;
180    }
181
182    public void setTemperature(Float temperature) {
183        if (temperature != null && temperature < 0) {
184            throw new IllegalArgumentException("temperature must be greater than 0");
185        }
186        this.temperature = temperature;
187    }
188
189    public Float getTopP() {
190        return topP;
191    }
192
193    public void setTopP(Float topP) {
194        if (topP != null && (topP < 0 || topP > 1)) {
195            throw new IllegalArgumentException("topP must be between 0 and 1");
196        }
197        this.topP = topP;
198    }
199
200    public Integer getTopK() {
201        return topK;
202    }
203
204    public void setTopK(Integer topK) {
205        if (topK != null && topK < 0) {
206            throw new IllegalArgumentException("topK must be greater than 0");
207        }
208        this.topK = topK;
209    }
210
211    public Integer getMaxTokens() {
212        return maxTokens;
213    }
214
215    public void setMaxTokens(Integer maxTokens) {
216        if (maxTokens != null && maxTokens < 0) {
217            throw new IllegalArgumentException("maxTokens must be greater than 0");
218        }
219        this.maxTokens = maxTokens;
220    }
221
222    public List<String> getStop() {
223        return stop;
224    }
225
226    public void setStop(List<String> stop) {
227        this.stop = stop;
228    }
229
230    public Boolean getThinkingEnabled() {
231        return thinkingEnabled;
232    }
233
234    public Boolean getThinkingEnabledOrDefault(Boolean defaultValue) {
235        return thinkingEnabled != null ? thinkingEnabled : defaultValue;
236    }
237
238    public void setThinkingEnabled(Boolean thinkingEnabled) {
239        this.thinkingEnabled = thinkingEnabled;
240    }
241
242    public Boolean getIncludeUsage() {
243        return includeUsage;
244    }
245
246    public Boolean getIncludeUsageOrDefault(Boolean defaultValue) {
247        return includeUsage != null ? includeUsage : defaultValue;
248    }
249
250    public void setIncludeUsage(Boolean includeUsage) {
251        this.includeUsage = includeUsage;
252    }
253
254
255    public Map<String, Object> getExtraBody() {
256        return extraBody;
257    }
258
259    public void setExtraBody(Map<String, Object> extraBody) {
260        this.extraBody = extraBody;
261    }
262
263    /**
264     * 添加一个额外参数到 extra 映射中。
265     *
266     * @param key   参数名
267     * @param value 参数值
268     */
269    public void addExtraBody(String key, Object value) {
270        if (extraBody == null) {
271            extraBody = Maps.of(key, value);
272        } else {
273            extraBody.put(key, value);
274        }
275    }
276
277    public Boolean getRetryEnabled() {
278        return retryEnabled;
279    }
280
281    public boolean getRetryEnabledOrDefault(boolean defaultValue) {
282        return retryEnabled != null ? retryEnabled : defaultValue;
283    }
284
285    public void setRetryEnabled(Boolean retryEnabled) {
286        this.retryEnabled = retryEnabled;
287    }
288
289    public Integer getRetryCount() {
290        return retryCount;
291    }
292
293    public int getRetryCountOrDefault(int defaultValue) {
294        return retryCount != null ? retryCount : defaultValue;
295    }
296
297
298    public void setRetryCount(Integer retryCount) {
299        this.retryCount = retryCount;
300    }
301
302    public Integer getRetryInitialDelayMs() {
303        return retryInitialDelayMs;
304    }
305
306    public int getRetryInitialDelayMsOrDefault(int defaultValue) {
307        return retryInitialDelayMs != null ? retryInitialDelayMs : defaultValue;
308    }
309
310    public void setRetryInitialDelayMs(Integer retryInitialDelayMs) {
311        this.retryInitialDelayMs = retryInitialDelayMs;
312    }
313
314    public Map<String, Object> getResponseFormat() {
315        return responseFormat;
316    }
317
318    public void setResponseFormat(Map<String, Object> responseFormat) {
319        this.responseFormat = responseFormat;
320    }
321
322    public boolean isStreaming() {
323        return streaming;
324    }
325
326    public void setStreaming(boolean streaming) {
327        this.streaming = streaming;
328    }
329
330
331    /**
332     * 创建 ChatOptions 的 Builder 实例。
333     *
334     * @return 新的 Builder 对象
335     */
336    public static Builder builder() {
337        return new Builder();
338    }
339
340    /**
341     * ChatOptions 的构建器类,支持链式调用。
342     */
343    public static final class Builder {
344
345        private String model;
346        private String seed;
347        private Float temperature = 0.5f;
348        private Float topP;
349        private Integer topK;
350        private Integer maxTokens;
351        private List<String> stop;
352        private Boolean thinkingEnabled;
353        private Boolean includeUsage;
354        private Map<String, Object> extraBody;
355        private Boolean retryEnabled;
356        private int retryCount = 3;
357        private int retryInitialDelayMs = 1000;
358        public Map<String, Object> responseFormat;
359        public Map<String, Object> metadata;
360
361        public Builder model(String model) {
362            this.model = model;
363            return this;
364        }
365
366        public Builder seed(String seed) {
367            this.seed = seed;
368            return this;
369        }
370
371        public Builder temperature(Float temperature) {
372            this.temperature = temperature;
373            return this;
374        }
375
376        public Builder topP(Float topP) {
377            this.topP = topP;
378            return this;
379        }
380
381        public Builder topK(Integer topK) {
382            this.topK = topK;
383            return this;
384        }
385
386        public Builder maxTokens(Integer maxTokens) {
387            this.maxTokens = maxTokens;
388            return this;
389        }
390
391        public Builder stop(List<String> stop) {
392            this.stop = stop;
393            return this;
394        }
395
396        public Builder thinkingEnabled(Boolean thinkingEnabled) {
397            this.thinkingEnabled = thinkingEnabled;
398            return this;
399        }
400
401        public Builder includeUsage(Boolean includeUsage) {
402            this.includeUsage = includeUsage;
403            return this;
404        }
405
406        public Builder extraBody(Map<String, Object> extra) {
407            this.extraBody = extra;
408            return this;
409        }
410
411        public Builder addExtraBody(String key, Object value) {
412            if (this.extraBody == null) {
413                this.extraBody = Maps.of(key, value);
414            } else {
415                this.extraBody.put(key, value);
416            }
417            return this;
418        }
419
420        public Builder retryEnabled(Boolean retryEnabled) {
421            this.retryEnabled = retryEnabled;
422            return this;
423        }
424
425        public Builder retryCount(int retryCount) {
426            this.retryCount = retryCount;
427            return this;
428        }
429
430        public Builder retryInitialDelayMs(int retryInitialDelayMs) {
431            this.retryInitialDelayMs = retryInitialDelayMs;
432            return this;
433        }
434
435        public Builder responseFormat(Map<String, Object> responseFormat) {
436            this.responseFormat = responseFormat;
437            return this;
438        }
439
440        public Builder responseFormatToJsonObject() {
441            this.responseFormat = Maps.of("type", "json_object");
442            return this;
443        }
444
445        public Builder responseFormatToJsonSchema(Map<String, Object> json_schema) {
446            this.responseFormat = Maps.of("type", "json_schema").set("json_schema", json_schema);
447            return this;
448        }
449
450        public Builder metadata(Map<String, Object> metadata) {
451            this.metadata = metadata;
452            return this;
453        }
454
455        public Builder addMetadata(String key, Object value) {
456            if (this.metadata == null) {
457                this.metadata = Maps.of(key, value);
458            } else {
459                this.metadata.put(key, value);
460            }
461            return this;
462        }
463
464        /**
465         * 构建并返回 ChatOptions 实例。
466         *
467         * @return 配置完成的 ChatOptions 对象
468         */
469        public ChatOptions build() {
470            return new ChatOptions(this);
471        }
472    }
473}