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.client;
017
018import com.agentsflex.core.message.Message;
019import com.agentsflex.core.message.UserMessage;
020import com.agentsflex.core.model.chat.ChatConfig;
021import com.agentsflex.core.model.chat.ChatOptions;
022import com.agentsflex.core.prompt.Prompt;
023import com.agentsflex.core.util.Maps;
024import com.agentsflex.core.util.MessageUtil;
025
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029
030public class OpenAIChatRequestSpecBuilder implements ChatRequestSpecBuilder {
031
032    protected ChatMessageSerializer chatMessageSerializer;
033
034    public OpenAIChatRequestSpecBuilder() {
035        this(new OpenAIChatMessageSerializer());
036    }
037
038    public OpenAIChatRequestSpecBuilder(ChatMessageSerializer chatMessageSerializer) {
039        this.chatMessageSerializer = chatMessageSerializer;
040    }
041
042    @Override
043    public ChatRequestSpec buildRequest(Prompt prompt, ChatOptions options, ChatConfig config) {
044
045        String url = buildRequestUrl(prompt, options, config);
046        Map<String, String> headers = buildRequestHeaders(prompt, options, config);
047        String body = buildRequestBody(prompt, options, config);
048
049        boolean retryEnabled = options.getRetryEnabledOrDefault(config.isRetryEnabled());
050        int retryCountOrDefault = options.getRetryCountOrDefault(config.getRetryCount());
051        int retryInitialDelayMsOrDefault = options.getRetryInitialDelayMsOrDefault(config.getRetryInitialDelayMs());
052
053        return new ChatRequestSpec(url, headers, body, retryEnabled ? retryCountOrDefault : 0, retryEnabled ? retryInitialDelayMsOrDefault : 0);
054    }
055
056    protected String buildRequestUrl(Prompt prompt, ChatOptions options, ChatConfig config) {
057        return config.getFullUrl();
058    }
059
060    protected Map<String, String> buildRequestHeaders(Prompt prompt, ChatOptions options, ChatConfig config) {
061        Map<String, String> headers = new HashMap<>();
062        headers.put("Content-Type", "application/json");
063        headers.put("Authorization", "Bearer " + config.getApiKey());
064        return headers;
065    }
066
067
068    protected String buildRequestBody(Prompt prompt, ChatOptions options, ChatConfig config) {
069        List<Message> messages = prompt.getMessages();
070        UserMessage userMessage = MessageUtil.findLastUserMessage(messages);
071
072        Maps baseBodyJsonMap = buildBaseParamsOfRequestBody(prompt, options, config);
073
074        Maps bodyJsonMap = baseBodyJsonMap
075            .set("messages", chatMessageSerializer.serializeMessages(messages, config))
076            .setIfNotEmpty("tools", chatMessageSerializer.serializeTools(prompt, config))
077            .setIfContainsKey("tools", "tool_choice", prompt.getToolChoice());
078
079        if (options.isStreaming() && options.getIncludeUsageOrDefault(true)) {
080            bodyJsonMap.set("stream_options", Maps.of("include_usage", true));
081        }
082
083        buildThinkingBody(options, config, bodyJsonMap);
084
085        if (options.getExtraBody() != null) {
086            bodyJsonMap.putAll(options.getExtraBody());
087        }
088
089        return bodyJsonMap.toJSON();
090    }
091
092    protected void buildThinkingBody(ChatOptions options, ChatConfig config, Maps bodyJsonMap) {
093        if (!config.isSupportThinking()) {
094            return;
095        }
096        Boolean thinkingEnabled = options.getThinkingEnabled();
097        if (thinkingEnabled == null) {
098            return;
099        }
100
101        String thinkingProtocol = config.getThinkingProtocol();
102        if (thinkingProtocol == null || "none".equals(thinkingProtocol)) {
103            return;
104        }
105
106        switch (thinkingProtocol) {
107            case "qwen":
108                bodyJsonMap.set("enable_thinking", thinkingEnabled);
109                break;
110            case "deepseek":
111                bodyJsonMap.set("thinking", Maps.of("type", thinkingEnabled ? "enabled" : "disabled"));
112                break;
113            case "ollama":
114                bodyJsonMap.set("thinking", thinkingEnabled);
115            default:
116                // do nothing
117        }
118    }
119
120
121    protected Maps buildBaseParamsOfRequestBody(Prompt prompt, ChatOptions options, ChatConfig config) {
122        return Maps.of("model", options.getModelOrDefault(config.getModel()))
123            .setIf(options.isStreaming(), "stream", true)
124            .setIfNotNull("top_p", options.getTopP())
125//            .setIfNotNull("top_k", options.getTopK()) // openAI 不支持 top_k 标识
126            .setIfNotNull("temperature", options.getTemperature())
127            .setIfNotNull("max_tokens", options.getMaxTokens())
128            .setIfNotEmpty("stop", options.getStop())
129            .setIfNotEmpty("response_format", options.getResponseFormat());
130
131    }
132
133    public ChatMessageSerializer getChatMessageSerializer() {
134        return chatMessageSerializer;
135    }
136
137    public void setChatMessageSerializer(ChatMessageSerializer chatMessageSerializer) {
138        this.chatMessageSerializer = chatMessageSerializer;
139    }
140}