|
|
@@ -1,12 +1,177 @@
|
|
|
package com.tzld.piaoquan.sde.integration;
|
|
|
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
+import com.alibaba.fastjson.JSONArray;
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
+import okhttp3.*;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
+import javax.annotation.PostConstruct;
|
|
|
+import java.io.IOException;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
+
|
|
|
/**
|
|
|
+ * OpenRouter API Client
|
|
|
+ * 支持通过 OpenRouter 调用 Gemini 和 ChatGPT 模型
|
|
|
+ *
|
|
|
* @author supeng
|
|
|
*/
|
|
|
@Slf4j
|
|
|
@Service
|
|
|
public class OpenRouterClient {
|
|
|
+
|
|
|
+ private static final MediaType JSON_MEDIA_TYPE = MediaType.get("application/json; charset=utf-8");
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 国内代理:<a href="https://openrouter.piaoquantv.com/api/v1/chat/completions">...</a>
|
|
|
+ * 官方链接:<a href="https://openrouter.ai/api/v1/chat/completions">...</a>
|
|
|
+ */
|
|
|
+ @Value("${openrouter.api.url:https://openrouter.piaoquantv.com/api/v1/chat/completions}")
|
|
|
+ private String url;
|
|
|
+
|
|
|
+ @Value("${openrouter.api.key:}")
|
|
|
+ private String apiKey;
|
|
|
+
|
|
|
+ @Value("${openrouter.timeout:60}")
|
|
|
+ private int timeout;
|
|
|
+
|
|
|
+ private OkHttpClient httpClient;
|
|
|
+
|
|
|
+ @PostConstruct
|
|
|
+ public void init() {
|
|
|
+ this.httpClient = new OkHttpClient.Builder()
|
|
|
+ .connectTimeout(timeout, TimeUnit.SECONDS)
|
|
|
+ .readTimeout(timeout, TimeUnit.SECONDS)
|
|
|
+ .writeTimeout(timeout, TimeUnit.SECONDS)
|
|
|
+ .build();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 调用 OpenRouter API
|
|
|
+ *
|
|
|
+ * @param model 模型名称,例如: "google/gemini-pro", "openai/gpt-4", "openai/gpt-3.5-turbo"
|
|
|
+ * @param messages 消息列表,每个消息包含 role 和 content
|
|
|
+ * @param temperature 温度参数,控制随机性 (0.0-2.0)
|
|
|
+ * @param maxTokens 最大生成 token 数
|
|
|
+ * @return API 响应内容
|
|
|
+ * @throws IOException 网络请求异常
|
|
|
+ */
|
|
|
+ public String chat(String model, List<Map<String, String>> messages, Double temperature, Integer maxTokens) throws IOException {
|
|
|
+ JSONObject requestBody = buildRequestBody(model, messages, temperature, maxTokens);
|
|
|
+
|
|
|
+ Request request = new Request.Builder()
|
|
|
+ .url(url)
|
|
|
+ .addHeader("Authorization", "Bearer " + apiKey)
|
|
|
+ .addHeader("Content-Type", "application/json")
|
|
|
+ .addHeader("HTTP-Referer", "https://github.com/tzld")
|
|
|
+ .addHeader("X-Title", "Supply Demand Engine")
|
|
|
+ .post(RequestBody.create(JSON_MEDIA_TYPE, requestBody.toJSONString()))
|
|
|
+ .build();
|
|
|
+
|
|
|
+ log.info("Calling OpenRouter API with model: {}", model);
|
|
|
+
|
|
|
+ try (Response response = httpClient.newCall(request).execute()) {
|
|
|
+ if (!response.isSuccessful()) {
|
|
|
+ String errorBody = response.body() != null ? response.body().string() : "No error body";
|
|
|
+ log.error("OpenRouter API call failed: {} - {}", response.code(), errorBody);
|
|
|
+ throw new IOException("OpenRouter API call failed: " + response.code() + " - " + errorBody);
|
|
|
+ }
|
|
|
+
|
|
|
+ String responseBody = response.body().string();
|
|
|
+ log.debug("OpenRouter API response: {}", responseBody);
|
|
|
+
|
|
|
+ return parseResponse(responseBody);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 简化的调用方法,使用默认参数
|
|
|
+ *
|
|
|
+ * @param model 模型名称
|
|
|
+ * @param messages 消息列表
|
|
|
+ * @return API 响应内容
|
|
|
+ * @throws IOException 网络请求异常
|
|
|
+ */
|
|
|
+ public String chat(String model, List<Map<String, String>> messages) throws IOException {
|
|
|
+ return chat(model, messages, 0.7, 2000);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 调用 Gemini 模型
|
|
|
+ *
|
|
|
+ * @param messages 消息列表
|
|
|
+ * @return API 响应内容
|
|
|
+ * @throws IOException 网络请求异常
|
|
|
+ */
|
|
|
+ public String chatWithGemini(List<Map<String, String>> messages) throws IOException {
|
|
|
+ return chat("google/gemini-pro", messages);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 调用 ChatGPT 模型
|
|
|
+ *
|
|
|
+ * @param messages 消息列表
|
|
|
+ * @param useGpt4 是否使用 GPT-4,false 则使用 GPT-3.5-turbo
|
|
|
+ * @return API 响应内容
|
|
|
+ * @throws IOException 网络请求异常
|
|
|
+ */
|
|
|
+ public String chatWithGPT(List<Map<String, String>> messages, boolean useGpt4) throws IOException {
|
|
|
+ String model = useGpt4 ? "openai/gpt-4" : "openai/gpt-3.5-turbo";
|
|
|
+ return chat(model, messages);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建请求体
|
|
|
+ */
|
|
|
+ private JSONObject buildRequestBody(String model, List<Map<String, String>> messages, Double temperature, Integer maxTokens) {
|
|
|
+ JSONObject requestBody = new JSONObject();
|
|
|
+ requestBody.put("model", model);
|
|
|
+
|
|
|
+ JSONArray messagesArray = new JSONArray();
|
|
|
+ for (Map<String, String> message : messages) {
|
|
|
+ JSONObject messageNode = new JSONObject();
|
|
|
+ messageNode.put("role", message.get("role"));
|
|
|
+ messageNode.put("content", message.get("content"));
|
|
|
+ messagesArray.add(messageNode);
|
|
|
+ }
|
|
|
+ requestBody.put("messages", messagesArray);
|
|
|
+
|
|
|
+ if (temperature != null) {
|
|
|
+ requestBody.put("temperature", temperature);
|
|
|
+ }
|
|
|
+ if (maxTokens != null) {
|
|
|
+ requestBody.put("max_tokens", maxTokens);
|
|
|
+ }
|
|
|
+
|
|
|
+ return requestBody;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析响应,提取生成的内容
|
|
|
+ */
|
|
|
+ private String parseResponse(String responseBody) throws IOException {
|
|
|
+ try {
|
|
|
+ JSONObject root = JSON.parseObject(responseBody);
|
|
|
+ JSONArray choices = root.getJSONArray("choices");
|
|
|
+ if (choices != null && !choices.isEmpty()) {
|
|
|
+ JSONObject firstChoice = choices.getJSONObject(0);
|
|
|
+ JSONObject message = firstChoice.getJSONObject("message");
|
|
|
+ if (message != null) {
|
|
|
+ String content = message.getString("content");
|
|
|
+ if (content != null) {
|
|
|
+ return content;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ log.warn("Unable to parse content from response, returning full response");
|
|
|
+ return responseBody;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("Error parsing response: {}", e.getMessage());
|
|
|
+ return responseBody;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|