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