package com.zy.ai.service; import com.alibaba.fastjson.JSON; import com.zy.ai.entity.ChatCompletionRequest; import com.zy.ai.entity.ChatCompletionResponse; import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.web.client.RestClient; import org.springframework.web.reactive.function.client.WebClient; import io.netty.channel.ChannelOption; import reactor.netty.http.client.HttpClient; import reactor.core.publisher.Flux; import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; @Service public class LlmSpringAiClientService { @Value("${llm.connect-timeout-ms:10000}") private int connectTimeoutMs; @Value("${llm.read-timeout-ms:120000}") private int readTimeoutMs; public CompletionCallResult callCompletion(String baseUrl, String apiKey, ChatCompletionRequest req) { OpenAiApi api = buildOpenAiApi(baseUrl, apiKey); OpenAiApi.ChatCompletionRequest springReq = buildSpringAiRequest(req, false); ResponseEntity entity = api.chatCompletionEntity(springReq); OpenAiApi.ChatCompletion body = entity.getBody(); ChatCompletionResponse legacy = toLegacyResponse(body); return new CompletionCallResult(entity.getStatusCode().value(), serializeResponsePayload(body, legacy), legacy); } public Flux streamCompletion(String baseUrl, String apiKey, ChatCompletionRequest req) { OpenAiApi api = buildOpenAiApi(baseUrl, apiKey); OpenAiApi.ChatCompletionRequest springReq = buildSpringAiRequest(req, true); return api.chatCompletionStream(springReq) .flatMapIterable(chunk -> chunk == null || chunk.choices() == null ? List.of() : chunk.choices()) .map(OpenAiApi.ChatCompletionChunk.ChunkChoice::delta) .filter(delta -> delta != null) .handle((delta, sink) -> { String text = extractSpringAiContent(delta); if (text != null && !text.isEmpty()) { sink.next(text); } }); } public Integer statusCodeOf(Throwable ex) { if (ex == null || ex.getMessage() == null) { return null; } String text = ex.getMessage().trim(); int idx = text.indexOf(" - "); String prefix = idx >= 0 ? text.substring(0, idx).trim() : text; try { return Integer.parseInt(prefix); } catch (NumberFormatException ignore) { return null; } } public String responseBodyOf(Throwable ex, int maxLen) { if (ex == null || ex.getMessage() == null) { return null; } String text = ex.getMessage().trim(); int idx = text.indexOf(" - "); String body = idx >= 0 ? text.substring(idx + 3).trim() : text; if (body.length() > maxLen) { return body.substring(0, maxLen); } return body; } private OpenAiApi buildOpenAiApi(String baseUrl, String apiKey) { HttpClient httpClient = HttpClient.create() .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeoutMs) .responseTimeout(Duration.ofMillis(readTimeoutMs)); WebClient.Builder webClientBuilder = WebClient.builder() .clientConnector(new ReactorClientHttpConnector(httpClient)); RestClient.Builder restClientBuilder = RestClient.builder() .requestFactory(buildRequestFactory()); return OpenAiApi.builder() .baseUrl(baseUrl) .apiKey(apiKey) .restClientBuilder(restClientBuilder) .webClientBuilder(webClientBuilder) .build(); } private SimpleClientHttpRequestFactory buildRequestFactory() { SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(connectTimeoutMs); factory.setReadTimeout(readTimeoutMs); return factory; } private OpenAiApi.ChatCompletionRequest buildSpringAiRequest(ChatCompletionRequest req, boolean stream) { HashMap extraBody = new HashMap<>(); if (req != null && req.getThinking() != null) { HashMap thinking = new HashMap<>(); thinking.put("type", req.getThinking().getType() != null ? req.getThinking().getType() : "enable"); extraBody.put("thinking", thinking); } return new OpenAiApi.ChatCompletionRequest( toSpringAiMessages(req == null ? null : req.getMessages()), req == null ? null : req.getModel(), null, null, null, null, null, null, req == null ? null : req.getMax_tokens(), null, 1, null, null, null, null, null, null, null, stream, stream ? OpenAiApi.ChatCompletionRequest.StreamOptions.INCLUDE_USAGE : null, req == null ? null : req.getTemperature(), null, toSpringAiTools(req == null ? null : req.getTools()), toSpringAiToolChoice(req == null ? null : req.getTool_choice()), null, null, null, null, null, null, null, extraBody.isEmpty() ? null : extraBody ); } private List toSpringAiMessages(List messages) { ArrayList result = new ArrayList<>(); if (messages == null) { return result; } for (ChatCompletionRequest.Message message : messages) { if (message == null) { continue; } result.add(new OpenAiApi.ChatCompletionMessage( message.getContent(), toSpringAiRole(message.getRole()), message.getName(), message.getTool_call_id(), toSpringAiToolCalls(message.getTool_calls()), null, null, null, null )); } return result; } private List toSpringAiToolCalls(List toolCalls) { if (toolCalls == null || toolCalls.isEmpty()) { return null; } ArrayList result = new ArrayList<>(); for (ChatCompletionRequest.ToolCall toolCall : toolCalls) { if (toolCall == null) { continue; } ChatCompletionRequest.Function function = toolCall.getFunction(); OpenAiApi.ChatCompletionMessage.ChatCompletionFunction springFunction = new OpenAiApi.ChatCompletionMessage.ChatCompletionFunction( function == null ? null : function.getName(), function == null ? null : function.getArguments() ); result.add(new OpenAiApi.ChatCompletionMessage.ToolCall( toolCall.getId(), toolCall.getType(), springFunction )); } return result.isEmpty() ? null : result; } private OpenAiApi.ChatCompletionMessage.Role toSpringAiRole(String role) { if (role == null) { return OpenAiApi.ChatCompletionMessage.Role.USER; } switch (role.trim().toLowerCase(Locale.ROOT)) { case "system": return OpenAiApi.ChatCompletionMessage.Role.SYSTEM; case "assistant": return OpenAiApi.ChatCompletionMessage.Role.ASSISTANT; case "tool": return OpenAiApi.ChatCompletionMessage.Role.TOOL; default: return OpenAiApi.ChatCompletionMessage.Role.USER; } } private List toSpringAiTools(List tools) { if (tools == null || tools.isEmpty()) { return null; } ArrayList result = new ArrayList<>(); for (Object tool : tools) { OpenAiApi.FunctionTool mapped = toSpringAiTool(tool); if (mapped != null) { result.add(mapped); } } return result.isEmpty() ? null : result; } @SuppressWarnings("unchecked") private OpenAiApi.FunctionTool toSpringAiTool(Object tool) { if (tool instanceof OpenAiApi.FunctionTool) { return (OpenAiApi.FunctionTool) tool; } if (!(tool instanceof Map)) { return null; } Map toolMap = (Map) tool; Object functionObj = toolMap.get("function"); if (!(functionObj instanceof Map)) { return null; } Map functionMap = (Map) functionObj; Object name = functionMap.get("name"); if (name == null) { return null; } Object description = functionMap.get("description"); Object parameters = functionMap.get("parameters"); Map parametersMap; if (parameters instanceof Map) { parametersMap = new HashMap<>((Map) parameters); } else { parametersMap = Collections.emptyMap(); } OpenAiApi.FunctionTool.Function function = new OpenAiApi.FunctionTool.Function( String.valueOf(description == null ? "" : description), String.valueOf(name), parametersMap, null ); return new OpenAiApi.FunctionTool(OpenAiApi.FunctionTool.Type.FUNCTION, function); } @SuppressWarnings("unchecked") private Object toSpringAiToolChoice(Object toolChoice) { if (toolChoice == null) { return null; } if (toolChoice instanceof String) { String text = ((String) toolChoice).trim(); if (text.isEmpty()) { return null; } if ("auto".equalsIgnoreCase(text) || "none".equalsIgnoreCase(text)) { return text.toLowerCase(Locale.ROOT); } return OpenAiApi.ChatCompletionRequest.ToolChoiceBuilder.function(text); } if (toolChoice instanceof Map) { Map choiceMap = (Map) toolChoice; Object type = choiceMap.get("type"); Object function = choiceMap.get("function"); if ("function".equals(type) && function instanceof Map) { Object name = ((Map) function).get("name"); if (name != null) { return OpenAiApi.ChatCompletionRequest.ToolChoiceBuilder.function(String.valueOf(name)); } } } return toolChoice; } private ChatCompletionResponse toLegacyResponse(OpenAiApi.ChatCompletion completion) { if (completion == null) { return null; } ChatCompletionResponse response = new ChatCompletionResponse(); response.setId(completion.id()); response.setCreated(completion.created()); response.setObjectName(completion.object()); if (completion.usage() != null) { ChatCompletionResponse.Usage usage = new ChatCompletionResponse.Usage(); usage.setPromptTokens(completion.usage().promptTokens()); usage.setCompletionTokens(completion.usage().completionTokens()); usage.setTotalTokens(completion.usage().totalTokens()); response.setUsage(usage); } if (completion.choices() != null) { ArrayList choices = new ArrayList<>(); for (OpenAiApi.ChatCompletion.Choice choice : completion.choices()) { ChatCompletionResponse.Choice item = new ChatCompletionResponse.Choice(); item.setIndex(choice.index()); if (choice.finishReason() != null) { item.setFinishReason(choice.finishReason().name().toLowerCase(Locale.ROOT)); } item.setMessage(toLegacyMessage(choice.message())); choices.add(item); } response.setChoices(choices); } return response; } private String serializeResponsePayload(OpenAiApi.ChatCompletion body, ChatCompletionResponse legacy) { if (legacy != null) { String content = firstMessageContent(legacy); if (content != null && !content.trim().isEmpty()) { return content; } String legacyJson = JSON.toJSONString(legacy); if (legacyJson != null && !legacyJson.trim().isEmpty() && !"{}".equals(legacyJson.trim())) { return legacyJson; } } if (body != null) { String raw = body.toString(); if (raw != null && !raw.trim().isEmpty()) { return raw; } } return null; } private String firstMessageContent(ChatCompletionResponse response) { if (response == null || response.getChoices() == null || response.getChoices().isEmpty()) { return null; } ChatCompletionResponse.Choice choice = response.getChoices().get(0); if (choice == null || choice.getMessage() == null) { return null; } return choice.getMessage().getContent(); } private ChatCompletionRequest.Message toLegacyMessage(OpenAiApi.ChatCompletionMessage message) { if (message == null) { return null; } ChatCompletionRequest.Message result = new ChatCompletionRequest.Message(); result.setContent(extractSpringAiContent(message)); if (message.role() != null) { result.setRole(message.role().name().toLowerCase(Locale.ROOT)); } result.setName(message.name()); result.setTool_call_id(message.toolCallId()); result.setTool_calls(toLegacyToolCalls(message.toolCalls())); return result; } private List toLegacyToolCalls(List toolCalls) { if (toolCalls == null || toolCalls.isEmpty()) { return null; } ArrayList result = new ArrayList<>(); for (OpenAiApi.ChatCompletionMessage.ToolCall toolCall : toolCalls) { if (toolCall == null) { continue; } ChatCompletionRequest.ToolCall legacy = new ChatCompletionRequest.ToolCall(); legacy.setId(toolCall.id()); legacy.setType(toolCall.type()); if (toolCall.function() != null) { ChatCompletionRequest.Function function = new ChatCompletionRequest.Function(); function.setName(toolCall.function().name()); function.setArguments(toolCall.function().arguments()); legacy.setFunction(function); } result.add(legacy); } return result.isEmpty() ? null : result; } private String extractSpringAiContent(OpenAiApi.ChatCompletionMessage message) { if (message == null || message.rawContent() == null) { return null; } Object content = message.rawContent(); if (content instanceof String) { return (String) content; } if (content instanceof List) { try { @SuppressWarnings("unchecked") List media = (List) content; return OpenAiApi.getTextContent(media); } catch (ClassCastException ignore) { } } return String.valueOf(content); } public static class CompletionCallResult { private final int statusCode; private final String payload; private final ChatCompletionResponse response; public CompletionCallResult(int statusCode, String payload, ChatCompletionResponse response) { this.statusCode = statusCode; this.payload = payload; this.response = response; } public int getStatusCode() { return statusCode; } public String getPayload() { return payload; } public ChatCompletionResponse getResponse() { return response; } } }