From 3642aaa941a3ff88cec02be5e9ad62804dff15f6 Mon Sep 17 00:00:00 2001 From: kazusa <409053122@qq.com> Date: Tue, 16 Sep 2025 15:12:44 +0800 Subject: [PATCH] init --- pom.xml | 228 ++++++++++++ .../java/cn/kazusa/chatgpt/IOpenAiApi.java | 261 +++++++++++++ .../cn/kazusa/chatgpt/common/Constants.java | 34 ++ .../chatgpt/domain/billing/BillingUsage.java | 25 ++ .../chatgpt/domain/billing/DailyCost.java | 18 + .../chatgpt/domain/billing/LineItem.java | 17 + .../kazusa/chatgpt/domain/billing/Plan.java | 13 + .../chatgpt/domain/billing/Subscription.java | 54 +++ .../chatgpt/domain/chat/ChatChoice.java | 27 ++ .../domain/chat/ChatCompletionRequest.java | 74 ++++ .../domain/chat/ChatCompletionResponse.java | 49 +++ .../kazusa/chatgpt/domain/chat/Message.java | 68 ++++ .../chatgpt/domain/edits/EditRequest.java | 53 +++ .../chatgpt/domain/edits/EditResponse.java | 29 ++ .../domain/embedd/EmbeddingRequest.java | 45 +++ .../domain/embedd/EmbeddingResponse.java | 21 ++ .../cn/kazusa/chatgpt/domain/embedd/Item.java | 20 + .../domain/files/DeleteFileResponse.java | 26 ++ .../cn/kazusa/chatgpt/domain/files/File.java | 32 ++ .../domain/files/UploadFileResponse.java | 13 + .../domain/images/ImageEditRequest.java | 43 +++ .../chatgpt/domain/images/ImageEnum.java | 31 ++ .../chatgpt/domain/images/ImageRequest.java | 52 +++ .../chatgpt/domain/images/ImageResponse.java | 18 + .../cn/kazusa/chatgpt/domain/images/Item.java | 20 + .../kazusa/chatgpt/domain/other/Choice.java | 23 ++ .../chatgpt/domain/other/OpenAiResponse.java | 30 ++ .../cn/kazusa/chatgpt/domain/other/Usage.java | 49 +++ .../domain/qa/QACompletionRequest.java | 81 ++++ .../domain/qa/QACompletionResponse.java | 31 ++ .../domain/whisper/TranscriptionsRequest.java | 39 ++ .../domain/whisper/TranslationsRequest.java | 37 ++ .../chatgpt/domain/whisper/WhisperEnum.java | 36 ++ .../domain/whisper/WhisperResponse.java | 14 + .../interceptor/OpenAiInterceptor.java | 48 +++ .../kazusa/chatgpt/session/Configuration.java | 51 +++ .../kazusa/chatgpt/session/OpenAiSession.java | 254 +++++++++++++ .../chatgpt/session/OpenAiSessionFactory.java | 11 + .../defaults/DefaultOpenAiSession.java | 349 ++++++++++++++++++ .../defaults/DefaultOpenAiSessionFactory.java | 57 +++ .../java/cn/kazusa/chatgpt/test/ApiTest.java | 305 +++++++++++++++ .../kazusa/chatgpt/test/HttpClientTest.java | 140 +++++++ 42 files changed, 2826 insertions(+) create mode 100644 pom.xml create mode 100644 src/main/java/cn/kazusa/chatgpt/IOpenAiApi.java create mode 100644 src/main/java/cn/kazusa/chatgpt/common/Constants.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/billing/BillingUsage.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/billing/DailyCost.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/billing/LineItem.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/billing/Plan.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/billing/Subscription.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/chat/ChatChoice.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/chat/ChatCompletionRequest.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/chat/ChatCompletionResponse.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/chat/Message.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/edits/EditRequest.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/edits/EditResponse.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/embedd/EmbeddingRequest.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/embedd/EmbeddingResponse.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/embedd/Item.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/files/DeleteFileResponse.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/files/File.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/files/UploadFileResponse.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/images/ImageEditRequest.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/images/ImageEnum.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/images/ImageRequest.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/images/ImageResponse.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/images/Item.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/other/Choice.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/other/OpenAiResponse.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/other/Usage.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/qa/QACompletionRequest.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/qa/QACompletionResponse.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/whisper/TranscriptionsRequest.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/whisper/TranslationsRequest.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/whisper/WhisperEnum.java create mode 100644 src/main/java/cn/kazusa/chatgpt/domain/whisper/WhisperResponse.java create mode 100644 src/main/java/cn/kazusa/chatgpt/interceptor/OpenAiInterceptor.java create mode 100644 src/main/java/cn/kazusa/chatgpt/session/Configuration.java create mode 100644 src/main/java/cn/kazusa/chatgpt/session/OpenAiSession.java create mode 100644 src/main/java/cn/kazusa/chatgpt/session/OpenAiSessionFactory.java create mode 100644 src/main/java/cn/kazusa/chatgpt/session/defaults/DefaultOpenAiSession.java create mode 100644 src/main/java/cn/kazusa/chatgpt/session/defaults/DefaultOpenAiSessionFactory.java create mode 100644 src/test/java/cn/kazusa/chatgpt/test/ApiTest.java create mode 100644 src/test/java/cn/kazusa/chatgpt/test/HttpClientTest.java diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..ecbcb0e --- /dev/null +++ b/pom.xml @@ -0,0 +1,228 @@ + + + 4.0.0 + + cn.kazusa + chatgpt-sdk + 1.0-SNAPSHOT + + chatgpt-sdk-java + OpenAI Java SDK, ChatGPT Java SDK . + + + 1.8 + UTF-8 + 1.8 + 1.8 + 2.9.0 + 2.0.6 + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.slf4j + slf4j-simple + ${slf4j.version} + + + com.fasterxml.jackson.core + jackson-databind + 2.13.3 + + + cn.hutool + hutool-all + 5.8.12 + + + com.squareup.okhttp3 + okhttp-sse + 3.14.9 + + + com.squareup.okhttp3 + logging-interceptor + 3.14.9 + + + com.squareup.retrofit2 + retrofit + ${retrofit2.version} + + + com.squareup.retrofit2 + converter-jackson + ${retrofit2.version} + + + com.squareup.retrofit2 + adapter-rxjava2 + ${retrofit2.version} + + + junit + junit + 4.13.2 + test + + + org.jetbrains + annotations + RELEASE + compile + + + com.knuddels + jtokkit + 0.2.0 + + + org.projectlombok + lombok + 1.18.24 + compile + + + com.alibaba + fastjson + 2.0.28 + + + + + chatgpt-sdk-java + + + org.apache.maven.plugins + maven-surefire-plugin + 2.12.4 + + true + + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + + + org.apache.maven.plugins + maven-jar-plugin + 2.3.1 + + + + true + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/cn/kazusa/chatgpt/IOpenAiApi.java b/src/main/java/cn/kazusa/chatgpt/IOpenAiApi.java new file mode 100644 index 0000000..8094a17 --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/IOpenAiApi.java @@ -0,0 +1,261 @@ +package cn.kazusa.chatgpt; + + +import cn.kazusa.chatgpt.domain.billing.BillingUsage; +import cn.kazusa.chatgpt.domain.billing.Subscription; +import cn.kazusa.chatgpt.domain.chat.ChatCompletionRequest; +import cn.kazusa.chatgpt.domain.chat.ChatCompletionResponse; +import cn.kazusa.chatgpt.domain.edits.EditRequest; +import cn.kazusa.chatgpt.domain.edits.EditResponse; +import cn.kazusa.chatgpt.domain.embedd.EmbeddingRequest; +import cn.kazusa.chatgpt.domain.embedd.EmbeddingResponse; +import cn.kazusa.chatgpt.domain.files.DeleteFileResponse; +import cn.kazusa.chatgpt.domain.files.UploadFileResponse; +import cn.kazusa.chatgpt.domain.images.ImageRequest; +import cn.kazusa.chatgpt.domain.images.ImageResponse; +import cn.kazusa.chatgpt.domain.other.OpenAiResponse; +import cn.kazusa.chatgpt.domain.qa.QACompletionRequest; +import cn.kazusa.chatgpt.domain.qa.QACompletionResponse; +import cn.kazusa.chatgpt.domain.whisper.WhisperResponse; +import io.reactivex.Single; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; +import okhttp3.ResponseBody; +import retrofit2.http.Body; +import retrofit2.http.DELETE; +import retrofit2.http.GET; +import retrofit2.http.Multipart; +import retrofit2.http.POST; +import retrofit2.http.Part; +import retrofit2.http.PartMap; +import retrofit2.http.Path; +import retrofit2.http.Query; +import retrofit2.http.Streaming; + +import java.io.File; +import java.time.LocalDate; +import java.util.Map; + +/** + * @author kazusa + * @description 以 ChatGPT 官网 API 模型,定义接口。官网:https://platform.openai.com/playground + */ +public interface IOpenAiApi { + + String v1_completions = "v1/completions"; + + /** + * 文本问答 + * + * @param qaCompletionRequest 请求信息 + * @return 应答结果 + */ + @POST(v1_completions) + Single completions(@Body QACompletionRequest qaCompletionRequest); + + String v1_chat_completions = "v1/chat/completions"; + + /** + * 问答模型;默认 GPT-3.5 + * + * @param chatCompletionRequest 请求信息 + * @return 应答结果 + */ + @POST(v1_chat_completions) + Single completions(@Body ChatCompletionRequest chatCompletionRequest); + + /** + * 文本修复 + * + * @param editRequest 请求信息;编辑文本的参数 + * @return 应答结果 + */ + @POST("v1/edits") + Single edits(@Body EditRequest editRequest); + + /** + * 生成图片 + * curl https://api.openai.com/v1/images/generations \ + * -H "Content-Type: application/json" \ + * -H "Authorization: Bearer $OPENAI_API_KEY" \ + * -d '{ + * "prompt": "A cute baby sea otter", + * "n": 2, + * "size": "1024x1024" + * }' + *

+ * { + * "created": 1589478378, + * "data": [ + * { + * "url": "https://..." + * }, + * { + * "url": "https://..." + * } + * ] + * } + * + * @param imageRequest 图片对象 + * @return 应答结果 + */ + @POST("v1/images/generations") + Single genImages(@Body ImageRequest imageRequest); + + /** + * 修改图片 + *

+ * curl https://api.openai.com/v1/images/edits \ + * -H "Authorization: Bearer $OPENAI_API_KEY" \ + * -F image="@otter.png" \ + * -F mask="@mask.png" \ + * -F prompt="A cute baby sea otter wearing a beret" \ + * -F n=2 \ + * -F size="1024x1024" + *

+ * { + * "created": 1589478378, + * "data": [ + * { + * "url": "https://..." + * }, + * { + * "url": "https://..." + * } + * ] + * } + * + * @param image 图片对象 + * @param mask 图片对象 + * @param requestBodyMap 请求参数 + * @return 应答结果 + */ + @Multipart + @POST("v1/images/edits") + Single editImages(@Part MultipartBody.Part image, @Part MultipartBody.Part mask, @PartMap Map requestBodyMap); + + /** + * 向量计算 + * curl https://api.openai.com/v1/images/variations \ + * -H "Authorization: Bearer $OPENAI_API_KEY" \ + * -F image="@otter.png" \ + * -F n=2 \ + * -F size="1024x1024" + * + * @param embeddingRequest 请求对象 + * @return 应答结果 + */ + @POST("v1/embeddings") + Single embeddings(@Body EmbeddingRequest embeddingRequest); + + /** + * 文件列表;在你上传文件到服务端后,可以获取列表信息 + * curl https://api.openai.com/v1/files \ + * -H "Authorization: Bearer $OPENAI_API_KEY" + * + * @return 应答结果 + */ + @GET("v1/files") + Single> files(); + + /** + * 上传文件;上载一个文件,该文件包含要在各种端点/功能中使用的文档。目前,一个组织上传的所有文件的大小最多可达1GB。如果您需要增加存储限制,请与官网联系。 + * curl https://api.openai.com/v1/files \ + * -H "Authorization: Bearer $OPENAI_API_KEY" \ + * -F purpose="fine-tune" \ + * -F file="@mydata.jsonl" + * + * @param file 文件 + * @param purpose "fine-tune" + * @return 应答结果 + */ + @Multipart + @POST("v1/files") + Single uploadFile(@Part MultipartBody.Part file, @Part("purpose") RequestBody purpose); + + /** + * 删除文件 + * curl https://api.openai.com/v1/files/file-XjGxS3KTG0uNmNOK362iJua3 \ + * -X DELETE \ + * -H "Authorization: Bearer $OPENAI_API_KEY" + * + * @param fileId 文件ID + * @return 应答结果 + */ + @DELETE("v1/files/{file_id}") + Single deleteFile(@Path("file_id") String fileId); + + /** + * 检索文件 + * curl https://api.openai.com/v1/files/file-XjGxS3KTG0uNmNOK362iJua3 \ + * -H "Authorization: Bearer $OPENAI_API_KEY" + * + * @param fileId 文件ID + * @return 应答结果 + */ + @GET("v1/files/{file_id}") + Single retrieveFile(@Path("file_id") String fileId); + + /** + * 检索文件内容信息 + * curl https://api.openai.com/v1/files/file-XjGxS3KTG0uNmNOK362iJua3/content \ + * -H "Authorization: Bearer $OPENAI_API_KEY" > file.jsonl + * + * @param fileId 文件ID + * @return 应答结果 + */ + @Streaming + @GET("v1/files/{file_id}/content") + Single retrieveFileContent(@Path("file_id") String fileId); + + /** + * 语音转文字 + * curl https://api.openai.com/v1/audio/transcriptions \ + * -H "Authorization: Bearer $OPENAI_API_KEY" \ + * -H "Content-Type: multipart/form-data" \ + * -F file="@/path/to/file/audio.mp3" \ + * -F model="whisper-1" + * + * @param file 语音文件 + * @param requestBodyMap 请求信息 + * @return 应答结果 + */ + @Multipart + @POST("v1/audio/transcriptions") + Single speed2TextTranscriptions(@Part MultipartBody.Part file, @PartMap() Map requestBodyMap); + + /** + * 语音翻译 + * curl https://api.openai.com/v1/audio/translations \ + * -H "Authorization: Bearer $OPENAI_API_KEY" \ + * -H "Content-Type: multipart/form-data" \ + * -F file="@/path/to/file/german.m4a" \ + * -F model="whisper-1" + * + * @param file 语音文件 + * @param requestBodyMap 请求信息 + * @return 应答结果 + */ + @Multipart + @POST("v1/audio/translations") + Single speed2TextTranslations(@Part MultipartBody.Part file, @PartMap() Map requestBodyMap); + + /** + * 账单查询 + * + * @return 应答结果 + */ + @GET("v1/dashboard/billing/subscription") + Single subscription(); + + /** + * 消耗查询 + * + * @param starDate 开始时间 + * @param endDate 结束时间 + * @return 应答数据 + */ + @GET("v1/dashboard/billing/usage") + Single billingUsage(@Query("start_date") LocalDate starDate, @Query("end_date") LocalDate endDate); + +} diff --git a/src/main/java/cn/kazusa/chatgpt/common/Constants.java b/src/main/java/cn/kazusa/chatgpt/common/Constants.java new file mode 100644 index 0000000..1ee0190 --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/common/Constants.java @@ -0,0 +1,34 @@ +package cn.kazusa.chatgpt.common; + +/** + * @author kazusa + * @description 通用类 + */ +public class Constants { + + public final static String NULL = "NULL"; + + /** + * 官网支持的请求角色类型;system、user、assistant + * https://platform.openai.com/docs/guides/chat/introduction + */ + public enum Role { + + SYSTEM("system"), + USER("user"), + ASSISTANT("assistant"), + ; + + private String code; + + Role(String code) { + this.code = code; + } + + public String getCode() { + return code; + } + + } + +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/billing/BillingUsage.java b/src/main/java/cn/kazusa/chatgpt/domain/billing/BillingUsage.java new file mode 100644 index 0000000..bdfd7ac --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/billing/BillingUsage.java @@ -0,0 +1,25 @@ +package cn.kazusa.chatgpt.domain.billing; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +/** + * @author kazusa + * @description 消耗账单 + */ +@Data +public class BillingUsage { + + @JsonProperty("object") + private String object; + /** 账号金额消耗明细 */ + @JsonProperty("daily_costs") + private List dailyCosts; + /** 总使用金额/美分 */ + @JsonProperty("total_usage") + private BigDecimal totalUsage; + +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/billing/DailyCost.java b/src/main/java/cn/kazusa/chatgpt/domain/billing/DailyCost.java new file mode 100644 index 0000000..63a1c2f --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/billing/DailyCost.java @@ -0,0 +1,18 @@ +package cn.kazusa.chatgpt.domain.billing; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +/** + * @author kazusa + * @description 金额消耗 + */ +@Data +public class DailyCost { + @JsonProperty("timestamp") + private long timestamp; + @JsonProperty("line_items") + private List lineItems; +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/billing/LineItem.java b/src/main/java/cn/kazusa/chatgpt/domain/billing/LineItem.java new file mode 100644 index 0000000..7c33f75 --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/billing/LineItem.java @@ -0,0 +1,17 @@ +package cn.kazusa.chatgpt.domain.billing; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * @author kazusa + * @description 消耗列表数据 + */ +@Data +public class LineItem { + /** 模型 */ + private String name; + /** 金额 */ + private BigDecimal cost; +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/billing/Plan.java b/src/main/java/cn/kazusa/chatgpt/domain/billing/Plan.java new file mode 100644 index 0000000..4bf6626 --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/billing/Plan.java @@ -0,0 +1,13 @@ +package cn.kazusa.chatgpt.domain.billing; + +import lombok.Data; + +/** + * @author kazusa + * @description 账单 + */ +@Data +public class Plan { + private String title; + private String id; +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/billing/Subscription.java b/src/main/java/cn/kazusa/chatgpt/domain/billing/Subscription.java new file mode 100644 index 0000000..f03cf34 --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/billing/Subscription.java @@ -0,0 +1,54 @@ +package cn.kazusa.chatgpt.domain.billing; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.io.Serializable; + +/** + * @author kazusa + * @description 账户信息 + */ +@Data +public class Subscription implements Serializable { + + @JsonProperty("object") + private String object; + @JsonProperty("has_payment_method") + private boolean hasPaymentMethod; + @JsonProperty("canceled") + private boolean canceled; + @JsonProperty("canceled_at") + private Object canceledAt; + @JsonProperty("delinquent") + private Object delinquent; + @JsonProperty("access_until") + private long accessUntil; + @JsonProperty("soft_limit") + private long softLimit; + @JsonProperty("hard_limit") + private long hardLimit; + @JsonProperty("system_hard_limit") + private long systemHardLimit; + @JsonProperty("soft_limit_usd") + private double softLimitUsd; + @JsonProperty("hard_limit_usd") + private double hardLimitUsd; + @JsonProperty("system_hard_limit_usd") + private double systemHardLimitUsd; + @JsonProperty("plan") + private Plan plan; + @JsonProperty("account_name") + private String accountName; + @JsonProperty("po_number") + private Object poNumber; + @JsonProperty("billing_email") + private Object billingEmail; + @JsonProperty("tax_ids") + private Object taxIds; + @JsonProperty("billing_address") + private Object billingAddress; + @JsonProperty("business_address") + private Object businessAddress; + +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/chat/ChatChoice.java b/src/main/java/cn/kazusa/chatgpt/domain/chat/ChatChoice.java new file mode 100644 index 0000000..1976789 --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/chat/ChatChoice.java @@ -0,0 +1,27 @@ +package cn.kazusa.chatgpt.domain.chat; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.io.Serializable; + +/** + * @author kazusa + * @description 对话信息 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class ChatChoice implements Serializable { + + private long index; + /** stream = true 请求参数里返回的属性是 delta */ + @JsonProperty("delta") + private Message delta; + /** stream = false 请求参数里返回的属性是 delta */ + @JsonProperty("message") + private Message message; + @JsonProperty("finish_reason") + private String finishReason; + +} \ No newline at end of file diff --git a/src/main/java/cn/kazusa/chatgpt/domain/chat/ChatCompletionRequest.java b/src/main/java/cn/kazusa/chatgpt/domain/chat/ChatCompletionRequest.java new file mode 100644 index 0000000..77c9abe --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/chat/ChatCompletionRequest.java @@ -0,0 +1,74 @@ +package cn.kazusa.chatgpt.domain.chat; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * @author kazusa + * @description 对话聊天,请求信息依照;OpenAI官网API构建参数;https://platform.openai.com/playground + */ +@Data +@Builder +@Slf4j +@JsonInclude(JsonInclude.Include.NON_NULL) +@NoArgsConstructor +@AllArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class ChatCompletionRequest implements Serializable { + + /** 默认模型 */ + private String model = Model.GPT_3_5_TURBO.getCode(); + /** 问题描述 */ + private List messages; + /** 控制温度【随机性】;0到2之间。较高的值(如0.8)将使输出更加随机,而较低的值(如0.2)将使输出更加集中和确定 */ + private double temperature = 0.2; + /** 多样性控制;使用温度采样的替代方法称为核心采样,其中模型考虑具有top_p概率质量的令牌的结果。因此,0.1 意味着只考虑包含前 10% 概率质量的代币 */ + @JsonProperty("top_p") + private Double topP = 1d; + /** 为每个提示生成的完成次数 */ + private Integer n = 1; + /** 是否为流式输出;就是一蹦一蹦的,出来结果 */ + private boolean stream = false; + /** 停止输出标识 */ + private List stop; + /** 输出字符串限制;0 ~ 4096 */ + @JsonProperty("max_tokens") + private Integer maxTokens = 2048; + /** 频率惩罚;降低模型重复同一行的可能性 */ + @JsonProperty("frequency_penalty") + private double frequencyPenalty = 0; + /** 存在惩罚;增强模型谈论新话题的可能性 */ + @JsonProperty("presence_penalty") + private double presencePenalty = 0; + /** 生成多个调用结果,只显示最佳的。这样会更多的消耗你的 api token */ + @JsonProperty("logit_bias") + private Map logitBias; + /** 调用标识,避免重复调用 */ + private String user; + + @Getter + @AllArgsConstructor + public enum Model { + /** gpt-3.5-turbo */ + GPT_3_5_TURBO("gpt-3.5-turbo"), + /** GPT4.0 */ + GPT_4("gpt-4"), + GPT_4o("gpt-4o"), + /** GPT4.0 超长上下文 */ + GPT_4_32K("gpt-4-32k"), + ; + private String code; + } + +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/chat/ChatCompletionResponse.java b/src/main/java/cn/kazusa/chatgpt/domain/chat/ChatCompletionResponse.java new file mode 100644 index 0000000..39425db --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/chat/ChatCompletionResponse.java @@ -0,0 +1,49 @@ +package cn.kazusa.chatgpt.domain.chat; + +import cn.kazusa.chatgpt.domain.other.Usage; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * @author kazusa + * @description 对话请求结果信息 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class ChatCompletionResponse implements Serializable { + + /** + * ID + */ + private String id; + /** + * 对象 + */ + private String object; + /** + * 模型 + */ + private String model; + /** + * 对话 + */ + private List choices; + /** + * 创建 + */ + private long created; + /** + * 耗材 + */ + private Usage usage; + /** + * 该指纹代表模型运行时使用的后端配置。 + * https://platform.openai.com/docs/api-reference/chat + */ + @JsonProperty("system_fingerprint") + private String systemFingerprint; +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/chat/Message.java b/src/main/java/cn/kazusa/chatgpt/domain/chat/Message.java new file mode 100644 index 0000000..a8fdfb4 --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/chat/Message.java @@ -0,0 +1,68 @@ +package cn.kazusa.chatgpt.domain.chat; + +import cn.kazusa.chatgpt.common.Constants; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +import java.io.Serializable; + +/** + * @author kazusa + * @description 聊天消息对象 + */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class Message implements Serializable { + + private String role; + private String content; + private String name; + + public Message() { + } + + private Message(Builder builder) { + this.role = builder.role; + this.content = builder.content; + this.name = builder.name; + } + + public static Builder builder() { + return new Builder(); + } + + /** + * 建造者模式 + */ + public static final class Builder { + + private String role; + private String content; + private String name; + + public Builder() { + } + + public Builder role(Constants.Role role) { + this.role = role.getCode(); + return this; + } + + public Builder content(String content) { + this.content = content; + return this; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Message build() { + return new Message(this); + } + } + +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/edits/EditRequest.java b/src/main/java/cn/kazusa/chatgpt/domain/edits/EditRequest.java new file mode 100644 index 0000000..b5e77d4 --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/edits/EditRequest.java @@ -0,0 +1,53 @@ +package cn.kazusa.chatgpt.domain.edits; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +import java.io.Serializable; + +/** + * @author kazusa + * @description 文本编辑请求 + */ +@Slf4j +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class EditRequest implements Serializable { + + /** 模型 */ + @NonNull + private String model = Model.CODE_DAVINCI_EDIT_001.getCode(); + /** 输入 */ + @NonNull + private String input; + /** 修改描述 */ + @NonNull + private String instruction; + /** 控制温度【随机性】;0到2之间。较高的值(如0.8)将使输出更加随机,而较低的值(如0.2)将使输出更加集中和确定 */ + @Builder.Default + private double temperature = 0.2; + /** 多样性控制;使用温度采样的替代方法称为核心采样,其中模型考虑具有top_p概率质量的令牌的结果。因此,0.1 意味着只考虑包含前 10% 概率质量的代币 */ + @JsonProperty("top_p") + private Double topP = 1d; + /** 为每个提示生成的完成次数 */ + private Integer n = 1; + + @Getter + @AllArgsConstructor + public enum Model{ + /** text-davinci-edit-001 */ + TEXT_DAVINCI_EDIT_001("text-davinci-edit-001"), + /** code-davinci-edit-001 */ + CODE_DAVINCI_EDIT_001("code-davinci-edit-001"), + ; + private String code; + } + +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/edits/EditResponse.java b/src/main/java/cn/kazusa/chatgpt/domain/edits/EditResponse.java new file mode 100644 index 0000000..91baccb --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/edits/EditResponse.java @@ -0,0 +1,29 @@ +package cn.kazusa.chatgpt.domain.edits; + +import cn.kazusa.chatgpt.domain.other.Choice; +import cn.kazusa.chatgpt.domain.other.Usage; +import lombok.Data; + +import java.io.Serializable; + +/** + * @author kazusa + * @description 文本编辑响应 + */ +@Data +public class EditResponse implements Serializable { + + /** ID */ + private String id; + /** 对象 */ + private String object; + /** 模型 */ + private String model; + /** 对话 */ + private Choice[] choices; + /** 创建 */ + private long created; + /** 耗材 */ + private Usage usage; + +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/embedd/EmbeddingRequest.java b/src/main/java/cn/kazusa/chatgpt/domain/embedd/EmbeddingRequest.java new file mode 100644 index 0000000..5a90015 --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/embedd/EmbeddingRequest.java @@ -0,0 +1,45 @@ +package cn.kazusa.chatgpt.domain.embedd; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.io.Serializable; +import java.util.List; + +/** + * @author kazusa + * @description Creates a variation of a given image. + */ +@Slf4j +@Getter +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +@NoArgsConstructor +@AllArgsConstructor +public class EmbeddingRequest implements Serializable { + + /** 模型 */ + @NonNull + @Builder.Default + private String model = Model.TEXT_EMBEDDING_ADA_002.getCode(); + /** 输入信息 */ + @NonNull + private List input; + @Setter + private String user; + + @Getter + @AllArgsConstructor + public enum Model { + TEXT_EMBEDDING_ADA_002("text-embedding-ada-002"), + ; + private String code; + } + +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/embedd/EmbeddingResponse.java b/src/main/java/cn/kazusa/chatgpt/domain/embedd/EmbeddingResponse.java new file mode 100644 index 0000000..6438051 --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/embedd/EmbeddingResponse.java @@ -0,0 +1,21 @@ +package cn.kazusa.chatgpt.domain.embedd; + +import cn.kazusa.chatgpt.domain.other.Usage; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * @author kazusa + * @description 反馈对象 + */ +@Data +public class EmbeddingResponse implements Serializable { + + private String object; + private List data; + private String model; + private Usage usage; + +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/embedd/Item.java b/src/main/java/cn/kazusa/chatgpt/domain/embedd/Item.java new file mode 100644 index 0000000..4651edb --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/embedd/Item.java @@ -0,0 +1,20 @@ +package cn.kazusa.chatgpt.domain.embedd; + +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.List; + +/** + * @author kazusa + * @description 条目信息 + */ +@Data +public class Item implements Serializable { + + private String object; + private List embedding; + private Integer index; + +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/files/DeleteFileResponse.java b/src/main/java/cn/kazusa/chatgpt/domain/files/DeleteFileResponse.java new file mode 100644 index 0000000..48fa084 --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/files/DeleteFileResponse.java @@ -0,0 +1,26 @@ +package cn.kazusa.chatgpt.domain.files; + +import lombok.Data; + +import java.io.Serializable; + +/** + * @author kazusa + * @description 删除文件应答 + * { + * "id": "file-XjGxS3KTG0uNmNOK362iJua3", + * "object": "file", + * "deleted": true + * } + */ +@Data +public class DeleteFileResponse implements Serializable { + + /** 文件ID */ + private String id; + /** 对象;file */ + private String object; + /** 删除;true */ + private boolean deleted; + +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/files/File.java b/src/main/java/cn/kazusa/chatgpt/domain/files/File.java new file mode 100644 index 0000000..3ca4c0d --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/files/File.java @@ -0,0 +1,32 @@ +package cn.kazusa.chatgpt.domain.files; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.io.Serializable; + +/** + * @author kazusa + * @description 文件信息 + */ +@Data +public class File implements Serializable { + + /** ID */ + private String id; + /** 对象 */ + private String object; + /** 字节 */ + private long bytes; + /** 时间 */ + private long created_at; + /** 名称 */ + private String filename; + /** 类型;search */ + private String purpose; + /** 状态 */ + private String status; + @JsonProperty("status_details") + private String statusDetails; + +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/files/UploadFileResponse.java b/src/main/java/cn/kazusa/chatgpt/domain/files/UploadFileResponse.java new file mode 100644 index 0000000..cd2235d --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/files/UploadFileResponse.java @@ -0,0 +1,13 @@ +package cn.kazusa.chatgpt.domain.files; + +import lombok.Data; + +import java.io.Serializable; + +/** + * @author kazusa + * @description 上传文件应答;封装个空的文件应答结果对象 + */ +@Data +public class UploadFileResponse extends File implements Serializable { +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/images/ImageEditRequest.java b/src/main/java/cn/kazusa/chatgpt/domain/images/ImageEditRequest.java new file mode 100644 index 0000000..783826a --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/images/ImageEditRequest.java @@ -0,0 +1,43 @@ +package cn.kazusa.chatgpt.domain.images; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.io.Serializable; + +/** + * @author kazusa + * @description 修改图片 + */ +@Slf4j +@Getter +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +@NoArgsConstructor +@AllArgsConstructor +public class ImageEditRequest extends ImageEnum implements Serializable { + + /** 问题描述 */ + @NonNull + private String prompt; + /** 为每个提示生成的完成次数 */ + @Builder.Default + private Integer n = 1; + /** 图片大小 */ + @Builder.Default + private String size = Size.size_256.getCode(); + /** 图片格式化方式;URL、B64_JSON */ + @JsonProperty("response_format") + @Builder.Default + private String responseFormat = ResponseFormat.URL.getCode(); + @Setter + private String user; + +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/images/ImageEnum.java b/src/main/java/cn/kazusa/chatgpt/domain/images/ImageEnum.java new file mode 100644 index 0000000..67e9b0f --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/images/ImageEnum.java @@ -0,0 +1,31 @@ +package cn.kazusa.chatgpt.domain.images; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author kazusa + * @description 图片枚举配置 + */ +public class ImageEnum { + + @Getter + @AllArgsConstructor + public enum Size { + size_256("256x256"), + size_512("512x512"), + size_1024("1024x1024"), + ; + private String code; + } + + @Getter + @AllArgsConstructor + public enum ResponseFormat { + URL("url"), + B64_JSON("b64_json"), + ; + private String code; + } + +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/images/ImageRequest.java b/src/main/java/cn/kazusa/chatgpt/domain/images/ImageRequest.java new file mode 100644 index 0000000..c4fbb8c --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/images/ImageRequest.java @@ -0,0 +1,52 @@ +package cn.kazusa.chatgpt.domain.images; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +import java.io.Serializable; + +/** + * @author kazusa + * @description 图片请求 + */ +@Slf4j +@Getter +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +@NoArgsConstructor +@AllArgsConstructor +public class ImageRequest extends ImageEnum implements Serializable { + + /** 模型 */ + private String model = Model.DALL_E_3.code; + /** 问题描述 */ + @NonNull + private String prompt; + /** 为每个提示生成的完成次数 */ + @Builder.Default + private Integer n = 1; + /** 图片大小 */ + @Builder.Default + private String size = Size.size_1024.getCode(); + /** 图片格式化方式;URL、B64_JSON + @JsonProperty("response_format") + @Builder.Default + private String responseFormat = ResponseFormat.URL.getCode(); + @Setter + private String user; */ + + @Getter + @AllArgsConstructor + public enum Model { + DALL_E_2("dall-e-2"), + DALL_E_3("dall-e-3"), + ; + private final String code; + } + +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/images/ImageResponse.java b/src/main/java/cn/kazusa/chatgpt/domain/images/ImageResponse.java new file mode 100644 index 0000000..8563e67 --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/images/ImageResponse.java @@ -0,0 +1,18 @@ +package cn.kazusa.chatgpt.domain.images; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * @author kazusa + * @description 图片响应 + */ +@Data +public class ImageResponse implements Serializable { + /** 条目数据 */ + private List data; + /** 创建时间 */ + private long created; +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/images/Item.java b/src/main/java/cn/kazusa/chatgpt/domain/images/Item.java new file mode 100644 index 0000000..e6a3aab --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/images/Item.java @@ -0,0 +1,20 @@ +package cn.kazusa.chatgpt.domain.images; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.io.Serializable; + +/** + * @author kazusa + * @description 条目 + */ +@Data +public class Item implements Serializable { + + private String url; +// @JsonProperty("b64_json") +// private String b64Json; + @JsonProperty("revised_prompt") + private String revisedPrompt; +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/other/Choice.java b/src/main/java/cn/kazusa/chatgpt/domain/other/Choice.java new file mode 100644 index 0000000..1202c8d --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/other/Choice.java @@ -0,0 +1,23 @@ +package cn.kazusa.chatgpt.domain.other; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.io.Serializable; + +/** + * @author kazusa + * @description 对话信息 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class Choice implements Serializable { + + private long index; + private String text; + private Object logprobs; + @JsonProperty("finish_reason") + private String finishReason; + +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/other/OpenAiResponse.java b/src/main/java/cn/kazusa/chatgpt/domain/other/OpenAiResponse.java new file mode 100644 index 0000000..91aca7f --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/other/OpenAiResponse.java @@ -0,0 +1,30 @@ +package cn.kazusa.chatgpt.domain.other; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * @author kazusa + * @description OpenAI通用响应对象 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class OpenAiResponse implements Serializable { + + private String object; + private List data; + private Error error; + + + @Data + public class Error { + private String message; + private String type; + private String param; + private String code; + } + +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/other/Usage.java b/src/main/java/cn/kazusa/chatgpt/domain/other/Usage.java new file mode 100644 index 0000000..14bf4f2 --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/other/Usage.java @@ -0,0 +1,49 @@ +package cn.kazusa.chatgpt.domain.other; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; + +/** + * @author kazusa + * @description 使用量 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class Usage implements Serializable { + + /** 提示令牌 */ + @JsonProperty("prompt_tokens") + private long promptTokens; + /** 完成令牌 */ + @JsonProperty("completion_tokens") + private long completionTokens; + /** 总量令牌 */ + @JsonProperty("total_tokens") + private long totalTokens; + + public long getPromptTokens() { + return promptTokens; + } + + public void setPromptTokens(long promptTokens) { + this.promptTokens = promptTokens; + } + + public long getCompletionTokens() { + return completionTokens; + } + + public void setCompletionTokens(long completionTokens) { + this.completionTokens = completionTokens; + } + + public long getTotalTokens() { + return totalTokens; + } + + public void setTotalTokens(long totalTokens) { + this.totalTokens = totalTokens; + } + +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/qa/QACompletionRequest.java b/src/main/java/cn/kazusa/chatgpt/domain/qa/QACompletionRequest.java new file mode 100644 index 0000000..d0fa88b --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/qa/QACompletionRequest.java @@ -0,0 +1,81 @@ +package cn.kazusa.chatgpt.domain.qa; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * @author kazusa + * @description 文本问答,请求信息;OpenAI官网API构建参数;https://platform.openai.com/playground + */ +@Data +@Builder +@Slf4j +@JsonInclude(JsonInclude.Include.NON_NULL) +@NoArgsConstructor +@AllArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class QACompletionRequest implements Serializable { + + /** 默认模型 */ + @NonNull + @Builder.Default + private String model = Model.TEXT_DAVINCI_003.getCode(); + /** 问题描述 */ + @NonNull + private String prompt; + private String suffix; + /** 控制温度【随机性】;0到2之间。较高的值(如0.8)将使输出更加随机,而较低的值(如0.2)将使输出更加集中和确定 */ + private double temperature = 0.2; + /** 多样性控制;使用温度采样的替代方法称为核心采样,其中模型考虑具有top_p概率质量的令牌的结果。因此,0.1 意味着只考虑包含前 10% 概率质量的代币 */ + @JsonProperty("top_p") + private Double topP = 1d; + /** 为每个提示生成的完成次数 */ + private Integer n = 1; + /** 是否为流式输出;就是一蹦一蹦的,出来结果 */ + private boolean stream = false; + /** 停止输出标识 */ + private List stop; + /** 输出字符串限制;0 ~ 4096 */ + @JsonProperty("max_tokens") + private Integer maxTokens = 2048; + @Builder.Default + private boolean echo = false; + /** 频率惩罚;降低模型重复同一行的可能性 */ + @JsonProperty("frequency_penalty") + private double frequencyPenalty = 0; + /** 存在惩罚;增强模型谈论新话题的可能性 */ + @JsonProperty("presence_penalty") + private double presencePenalty = 0; + /** 生成多个调用结果,只显示最佳的。这样会更多的消耗你的 api token */ + @JsonProperty("best_of") + @Builder.Default + private Integer bestOf = 1; + private Integer logprobs; + @JsonProperty("logit_bias") + private Map logitBias; + /** 调用标识,避免重复调用 */ + private String user; + + @Getter + @AllArgsConstructor + public enum Model { + TEXT_DAVINCI_003("text-davinci-003"), + TEXT_DAVINCI_002("text-davinci-002"), + DAVINCI("davinci"), + ; + private String code; + } + +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/qa/QACompletionResponse.java b/src/main/java/cn/kazusa/chatgpt/domain/qa/QACompletionResponse.java new file mode 100644 index 0000000..decf01c --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/qa/QACompletionResponse.java @@ -0,0 +1,31 @@ +package cn.kazusa.chatgpt.domain.qa; + +import cn.kazusa.chatgpt.domain.other.Choice; +import cn.kazusa.chatgpt.domain.other.Usage; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +import java.io.Serializable; + +/** + * @author kazusa + * @description 问答完成响应 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class QACompletionResponse implements Serializable { + + /** ID */ + private String id; + /** 对象 */ + private String object; + /** 模型 */ + private String model; + /** 对话 */ + private Choice[] choices; + /** 创建 */ + private long created; + /** 耗材 */ + private Usage usage; + +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/whisper/TranscriptionsRequest.java b/src/main/java/cn/kazusa/chatgpt/domain/whisper/TranscriptionsRequest.java new file mode 100644 index 0000000..1068d4f --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/whisper/TranscriptionsRequest.java @@ -0,0 +1,39 @@ +package cn.kazusa.chatgpt.domain.whisper; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldNameConstants; + +/** + * @author kazusa + * @description 语音转录请求 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@FieldNameConstants +@JsonInclude(JsonInclude.Include.NON_NULL) +public class TranscriptionsRequest { + + /** 模型;WHISPER_1 */ + @Builder.Default + private String model = WhisperEnum.Model.WHISPER_1.getCode(); + /** 提示语 */ + private String prompt; + /** 输出格式 */ + @JsonProperty("response_format") + @Builder.Default + private String responseFormat = WhisperEnum.ResponseFormat.JSON.getCode(); + /** 控制温度【随机性】;0到2之间。较高的值(如0.8)将使输出更加随机,而较低的值(如0.2)将使输出更加集中和确定 */ + private double temperature = 0.2; + /** + * 音频语言;ISO-639-1 + */ + private String language; + +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/whisper/TranslationsRequest.java b/src/main/java/cn/kazusa/chatgpt/domain/whisper/TranslationsRequest.java new file mode 100644 index 0000000..0e07f9c --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/whisper/TranslationsRequest.java @@ -0,0 +1,37 @@ +package cn.kazusa.chatgpt.domain.whisper; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldNameConstants; + +import java.io.Serializable; + +/** + * @author kazusa + * @description 翻译请求 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@FieldNameConstants +@JsonInclude(JsonInclude.Include.NON_NULL) +public class TranslationsRequest implements Serializable { + + /** 模型;WHISPER_1 */ + @Builder.Default + private String model = WhisperEnum.Model.WHISPER_1.getCode(); + /** 提示语 */ + private String prompt; + /** 输出格式 */ + @JsonProperty("response_format") + @Builder.Default + private String responseFormat = WhisperEnum.ResponseFormat.JSON.getCode(); + /** 控制温度【随机性】;0到2之间。较高的值(如0.8)将使输出更加随机,而较低的值(如0.2)将使输出更加集中和确定 */ + private double temperature = 0.2; + +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/whisper/WhisperEnum.java b/src/main/java/cn/kazusa/chatgpt/domain/whisper/WhisperEnum.java new file mode 100644 index 0000000..6d76da1 --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/whisper/WhisperEnum.java @@ -0,0 +1,36 @@ +package cn.kazusa.chatgpt.domain.whisper; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; + +import java.io.Serializable; + +/** + * @author kazusa + * @description 请求信息;语音转文字 + */ +@Data +public class WhisperEnum implements Serializable { + + @Getter + @AllArgsConstructor + public enum Model { + WHISPER_1("whisper-1"), + ; + private String code; + } + + @Getter + @AllArgsConstructor + public enum ResponseFormat { + JSON("json"), + TEXT("text"), + SRT("srt"), + VERBOSE_JSON("verbose_json"), + VTT("vtt"), + ; + private String code; + } + +} diff --git a/src/main/java/cn/kazusa/chatgpt/domain/whisper/WhisperResponse.java b/src/main/java/cn/kazusa/chatgpt/domain/whisper/WhisperResponse.java new file mode 100644 index 0000000..405ddec --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/domain/whisper/WhisperResponse.java @@ -0,0 +1,14 @@ +package cn.kazusa.chatgpt.domain.whisper; + +import lombok.Data; + +import java.io.Serializable; + +/** + * @author kazusa + * @description 语音转文字响应 + */ +@Data +public class WhisperResponse implements Serializable { + private String text; +} diff --git a/src/main/java/cn/kazusa/chatgpt/interceptor/OpenAiInterceptor.java b/src/main/java/cn/kazusa/chatgpt/interceptor/OpenAiInterceptor.java new file mode 100644 index 0000000..22f0cec --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/interceptor/OpenAiInterceptor.java @@ -0,0 +1,48 @@ +package cn.kazusa.chatgpt.interceptor; + +import cn.hutool.http.ContentType; +import cn.hutool.http.Header; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; + +/** + * @author kazusa + * @description OpenAI API请求拦截器 + */ +public class OpenAiInterceptor implements Interceptor { + + /** OpenAi apiKey 需要在官网申请 */ + private final String apiKeyBySystem; + /** 访问授权接口的认证 Token */ + + public OpenAiInterceptor(String apiKeyBySystem) { + this.apiKeyBySystem = apiKeyBySystem; + } + + @NotNull + @Override + public Response intercept(Chain chain) throws IOException { + // 1. 获取原始 Request + Request original = chain.request(); + + // 2. 读取 apiKey;优先使用自己传递的 apiKey + String apiKeyByUser = original.header("apiKey"); + String apiKey = null == apiKeyByUser ? apiKeyBySystem : apiKeyByUser; + + // 3. 构建 Request + Request request = original.newBuilder() + .url(original.url()) + .header(Header.AUTHORIZATION.getValue(), "Bearer " + apiKey) + .header(Header.CONTENT_TYPE.getValue(), ContentType.JSON.getValue()) + .method(original.method(), original.body()) + .build(); + + // 4. 返回执行结果 + return chain.proceed(request); + } + +} diff --git a/src/main/java/cn/kazusa/chatgpt/session/Configuration.java b/src/main/java/cn/kazusa/chatgpt/session/Configuration.java new file mode 100644 index 0000000..3c69d60 --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/session/Configuration.java @@ -0,0 +1,51 @@ +package cn.kazusa.chatgpt.session; + +import cn.kazusa.chatgpt.IOpenAiApi; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import okhttp3.OkHttpClient; +import okhttp3.sse.EventSource; +import okhttp3.sse.EventSources; +import org.jetbrains.annotations.NotNull; + +/** + * @author kazusa + * @description 配置信息 + */ +@Slf4j +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Configuration { + + @Getter + @Setter + private IOpenAiApi openAiApi; + + @Getter + @Setter + private OkHttpClient okHttpClient; + + @Getter + @NotNull + private String apiKey; + + @Getter + private String apiHost; + + /** + * 字段废弃,不在使用 + */ + @Getter + @Deprecated + private String authToken; + + public EventSource.Factory createRequestFactory() { + return EventSources.createFactory(okHttpClient); + } + +} diff --git a/src/main/java/cn/kazusa/chatgpt/session/OpenAiSession.java b/src/main/java/cn/kazusa/chatgpt/session/OpenAiSession.java new file mode 100644 index 0000000..624e323 --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/session/OpenAiSession.java @@ -0,0 +1,254 @@ +package cn.kazusa.chatgpt.session; + + +import cn.kazusa.chatgpt.domain.billing.BillingUsage; +import cn.kazusa.chatgpt.domain.billing.Subscription; +import cn.kazusa.chatgpt.domain.chat.ChatCompletionRequest; +import cn.kazusa.chatgpt.domain.chat.ChatCompletionResponse; +import cn.kazusa.chatgpt.domain.edits.EditRequest; +import cn.kazusa.chatgpt.domain.edits.EditResponse; +import cn.kazusa.chatgpt.domain.embedd.EmbeddingRequest; +import cn.kazusa.chatgpt.domain.embedd.EmbeddingResponse; +import cn.kazusa.chatgpt.domain.files.DeleteFileResponse; +import cn.kazusa.chatgpt.domain.files.UploadFileResponse; +import cn.kazusa.chatgpt.domain.images.ImageEditRequest; +import cn.kazusa.chatgpt.domain.images.ImageRequest; +import cn.kazusa.chatgpt.domain.images.ImageResponse; +import cn.kazusa.chatgpt.domain.other.OpenAiResponse; +import cn.kazusa.chatgpt.domain.qa.QACompletionRequest; +import cn.kazusa.chatgpt.domain.qa.QACompletionResponse; +import cn.kazusa.chatgpt.domain.whisper.TranscriptionsRequest; +import cn.kazusa.chatgpt.domain.whisper.TranslationsRequest; +import cn.kazusa.chatgpt.domain.whisper.WhisperResponse; +import com.fasterxml.jackson.core.JsonProcessingException; +import okhttp3.sse.EventSource; +import okhttp3.sse.EventSourceListener; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.time.LocalDate; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * @author kazusa + * @description OpenAi 会话接口 + */ +public interface OpenAiSession { + + /** + * 文本问答;简单请求 + * + * @param question 请求信息 + * @return 应答结果 + */ + QACompletionResponse completions(String question); + + /** + * 文本问答 + * + * @param qaCompletionRequest 请求信息 + * @return 应答结果 + */ + QACompletionResponse completions(QACompletionRequest qaCompletionRequest); + + /** + * 文本问答 & 流式反馈 + * + * @param qaCompletionRequest 请求信息 + * @param eventSourceListener 实现监听;通过监听的 onEvent 方法接收数据 + */ + EventSource completions(QACompletionRequest qaCompletionRequest, EventSourceListener eventSourceListener) throws JsonProcessingException; + + /** + * 问答模型 GPT-3.5/4.0 + * + * @param chatCompletionRequest 请求信息 + * @return 应答结果 + */ + ChatCompletionResponse completions(ChatCompletionRequest chatCompletionRequest); + + /** + * 问答模型 GPT-3.5/4.0 & 流式反馈 + * + * @param chatCompletionRequest 请求信息 + * @param eventSourceListener 实现监听;通过监听的 onEvent 方法接收数据 + * @return 应答结果 + */ + EventSource chatCompletions(ChatCompletionRequest chatCompletionRequest, EventSourceListener eventSourceListener) throws JsonProcessingException; + + /** + * 问答模型 GPT-3.5/4.0 & 流式反馈 & 一次反馈 + * + * @param chatCompletionRequest 请求信息 + * @return 应答结果 + */ + CompletableFuture chatCompletions(ChatCompletionRequest chatCompletionRequest) throws InterruptedException, JsonProcessingException; + + /** + * 问答模型 GPT-3.5/4.0 & 流式反馈 + * + * @param apiHostByUser 自定义host + * @param apiKeyByUser 自定义Key + * @param chatCompletionRequest 请求信息 + * @param eventSourceListener 实现监听;通过监听的 onEvent 方法接收数据 + * @return 应答结果 + */ + EventSource chatCompletions(String apiHostByUser, String apiKeyByUser, ChatCompletionRequest chatCompletionRequest, EventSourceListener eventSourceListener) throws JsonProcessingException; + + /** + * 文本修复 + * + * @param editRequest 请求信息 + * @return 应答结果 + */ + EditResponse edit(EditRequest editRequest); + + /** + * 生成图片 + * + * @param prompt 图片描述 + * @return 应答结果 + */ + ImageResponse genImages(String prompt); + + /** + * 生成图片 + * + * @param imageRequest 图片描述 + * @return 应答结果 + */ + ImageResponse genImages(ImageRequest imageRequest); + + /** + * 修改图片 + * + * @param image 图片对象 + * @param prompt 修改描述 + * @return 应答结果 + */ + ImageResponse editImages(File image, String prompt); + + /** + * 修改图片 + * + * @param image 图片对象 + * @param imageEditRequest 图片参数 + * @return 应答结果 + */ + ImageResponse editImages(File image, ImageEditRequest imageEditRequest); + + /** + * 修改图片 + * + * @param image 图片对象,小于4M的PNG图片 + * @param mask 图片对象,小于4M的PNG图片 + * @param imageEditRequest 图片参数 + * @return 应答结果 + */ + ImageResponse editImages(File image, File mask, ImageEditRequest imageEditRequest); + + /** + * 向量计算;单个文本 + * 文本向量计算是一种在自然语言处理(NLP)领域中用于测量和比较文本相似性的技术。在这种方法中,每个单词或短语都被转换为一个向量,可以使用这些向量来比较不同文本之间的相似性,并在需要时进行推荐或分类 + * + * @param input 文本信息 + * @return 应答结果 + */ + EmbeddingResponse embeddings(String input); + + /** + * 向量计算;多个文本 + * 文本向量计算是一种在自然语言处理(NLP)领域中用于测量和比较文本相似性的技术。在这种方法中,每个单词或短语都被转换为一个向量,可以使用这些向量来比较不同文本之间的相似性,并在需要时进行推荐或分类 + * + * @param inputs 多个文本 + * @return 应答结果 + */ + EmbeddingResponse embeddings(String... inputs); + + + /** + * 向量计算;多个文本 + * 文本向量计算是一种在自然语言处理(NLP)领域中用于测量和比较文本相似性的技术。在这种方法中,每个单词或短语都被转换为一个向量,可以使用这些向量来比较不同文本之间的相似性,并在需要时进行推荐或分类 + * + * @param inputs 多个文本 + * @return 应答结果 + */ + EmbeddingResponse embeddings(List inputs); + + /** + * 向量计算;入参 + * 文本向量计算是一种在自然语言处理(NLP)领域中用于测量和比较文本相似性的技术。在这种方法中,每个单词或短语都被转换为一个向量,可以使用这些向量来比较不同文本之间的相似性,并在需要时进行推荐或分类 + * + * @param embeddingRequest 请求结果 + * @return 应答结果 + */ + EmbeddingResponse embeddings(EmbeddingRequest embeddingRequest); + + /** + * 获取文件 + * + * @return 应答结果 + */ + OpenAiResponse files(); + + /** + * 上传文件 + * + * @param file 文件 + * @return 应答结果 + */ + UploadFileResponse uploadFile(File file); + + /** + * 上传文件 + * + * @param purpose Use "fine-tune" for Fine-tuning. This allows us to validate the format of the uploaded file. + * @param file 文件 + * @return 应答结果 + */ + UploadFileResponse uploadFile(String purpose, File file); + + /** + * 删除文件 + * + * @param fileId 文件ID + * @return 应答结果 + */ + DeleteFileResponse deleteFile(String fileId); + + /** + * 语音转文字 + * + * @param file 语音文件 + * @param transcriptionsRequest 请求信息 + * @return 应答结果 + */ + WhisperResponse speed2TextTranscriptions(File file, TranscriptionsRequest transcriptionsRequest); + + /** + * 语音翻译 + * + * @param file 语音文件 + * @param translationsRequest 请求信息 + * @return 应答结果 + */ + WhisperResponse speed2TextTranslations(File file, TranslationsRequest translationsRequest); + + /** + * 账单查询 + * + * @return 应答结果 + */ + Subscription subscription(); + + /** + * 消耗查询 + * + * @param starDate 开始时间 + * @param endDate 结束时间 + * @return 应答数据 + */ + BillingUsage billingUsage(@NotNull LocalDate starDate, @NotNull LocalDate endDate); + +} diff --git a/src/main/java/cn/kazusa/chatgpt/session/OpenAiSessionFactory.java b/src/main/java/cn/kazusa/chatgpt/session/OpenAiSessionFactory.java new file mode 100644 index 0000000..a09705c --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/session/OpenAiSessionFactory.java @@ -0,0 +1,11 @@ +package cn.kazusa.chatgpt.session; + +/** + * @author kazusa + * @description OpenAI会话工厂接口 + */ +public interface OpenAiSessionFactory { + + OpenAiSession openSession(); + +} \ No newline at end of file diff --git a/src/main/java/cn/kazusa/chatgpt/session/defaults/DefaultOpenAiSession.java b/src/main/java/cn/kazusa/chatgpt/session/defaults/DefaultOpenAiSession.java new file mode 100644 index 0000000..159d503 --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/session/defaults/DefaultOpenAiSession.java @@ -0,0 +1,349 @@ +package cn.kazusa.chatgpt.session.defaults; + + +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.ContentType; +import cn.kazusa.chatgpt.IOpenAiApi; +import cn.kazusa.chatgpt.common.Constants; +import cn.kazusa.chatgpt.domain.billing.BillingUsage; +import cn.kazusa.chatgpt.domain.billing.Subscription; +import cn.kazusa.chatgpt.domain.chat.ChatChoice; +import cn.kazusa.chatgpt.domain.chat.ChatCompletionRequest; +import cn.kazusa.chatgpt.domain.chat.ChatCompletionResponse; +import cn.kazusa.chatgpt.domain.chat.Message; +import cn.kazusa.chatgpt.domain.edits.EditRequest; +import cn.kazusa.chatgpt.domain.edits.EditResponse; +import cn.kazusa.chatgpt.domain.embedd.EmbeddingRequest; +import cn.kazusa.chatgpt.domain.embedd.EmbeddingResponse; +import cn.kazusa.chatgpt.domain.files.DeleteFileResponse; +import cn.kazusa.chatgpt.domain.files.UploadFileResponse; +import cn.kazusa.chatgpt.domain.images.ImageEditRequest; +import cn.kazusa.chatgpt.domain.images.ImageRequest; +import cn.kazusa.chatgpt.domain.images.ImageResponse; +import cn.kazusa.chatgpt.domain.other.OpenAiResponse; +import cn.kazusa.chatgpt.domain.qa.QACompletionRequest; +import cn.kazusa.chatgpt.domain.qa.QACompletionResponse; +import cn.kazusa.chatgpt.domain.whisper.TranscriptionsRequest; +import cn.kazusa.chatgpt.domain.whisper.TranslationsRequest; +import cn.kazusa.chatgpt.domain.whisper.WhisperResponse; +import cn.kazusa.chatgpt.session.Configuration; +import cn.kazusa.chatgpt.session.OpenAiSession; +import com.alibaba.fastjson.JSON; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.reactivex.Single; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.sse.EventSource; +import okhttp3.sse.EventSourceListener; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +/** + * @author kazusa + * @description OpenAI 会话 + */ +public class DefaultOpenAiSession implements OpenAiSession { + + /** + * 配置信息 + */ + private final Configuration configuration; + + /** + * OpenAI 接口 + */ + private final IOpenAiApi openAiApi; + /** + * 工厂事件 + */ + private final EventSource.Factory factory; + + public DefaultOpenAiSession(Configuration configuration) { + this.configuration = configuration; + this.openAiApi = configuration.getOpenAiApi(); + this.factory = configuration.createRequestFactory(); + } + + @Override + public QACompletionResponse completions(QACompletionRequest qaCompletionRequest) { + return this.openAiApi.completions(qaCompletionRequest).blockingGet(); + } + + @Override + public EventSource completions(QACompletionRequest qaCompletionRequest, EventSourceListener eventSourceListener) throws JsonProcessingException { + // 核心参数校验;不对用户的传参做更改,只返回错误信息。 + if (!qaCompletionRequest.isStream()) { + throw new RuntimeException("illegal parameter stream is false!"); + } + + // 构建请求信息 + Request request = new Request.Builder() + .url(configuration.getApiHost().concat(IOpenAiApi.v1_completions)) + .post(RequestBody.create(MediaType.parse(ContentType.JSON.getValue()), new ObjectMapper().writeValueAsString(qaCompletionRequest))) + .build(); + + // 返回事件结果 + return factory.newEventSource(request, eventSourceListener); + } + + @Override + public QACompletionResponse completions(String question) { + QACompletionRequest request = QACompletionRequest + .builder() + .prompt(question) + .build(); + Single completions = this.openAiApi.completions(request); + return completions.blockingGet(); + } + + @Override + public ChatCompletionResponse completions(ChatCompletionRequest chatCompletionRequest) { + return this.openAiApi.completions(chatCompletionRequest).blockingGet(); + } + + @Override + public EventSource chatCompletions(ChatCompletionRequest chatCompletionRequest, EventSourceListener eventSourceListener) throws JsonProcessingException { + return chatCompletions(Constants.NULL, Constants.NULL, chatCompletionRequest, eventSourceListener); + } + + @Override + public CompletableFuture chatCompletions(ChatCompletionRequest chatCompletionRequest) throws InterruptedException, JsonProcessingException { + // 用于执行异步任务并获取结果 + CompletableFuture future = new CompletableFuture<>(); + StringBuffer dataBuffer = new StringBuffer(); + + chatCompletions(chatCompletionRequest, new EventSourceListener(){ + @Override + public void onEvent(EventSource eventSource, String id, String type, String data) { + if ("[DONE]".equalsIgnoreCase(data)) { + onClosed(eventSource); + future.complete(dataBuffer.toString()); + } + + ChatCompletionResponse chatCompletionResponse = JSON.parseObject(data, ChatCompletionResponse.class); + List choices = chatCompletionResponse.getChoices(); + for (ChatChoice chatChoice : choices) { + Message delta = chatChoice.getDelta(); + if (Constants.Role.ASSISTANT.getCode().equals(delta.getRole())) continue; + + // 应答完成 + String finishReason = chatChoice.getFinishReason(); + if ("stop".equalsIgnoreCase(finishReason)) { + onClosed(eventSource); + return; + } + + // 发送信息 + try { + dataBuffer.append(delta.getContent()); + } catch (Exception e) { + future.completeExceptionally(new RuntimeException("Request closed before completion")); + } + + } + } + + @Override + public void onClosed(EventSource eventSource) { + future.complete(dataBuffer.toString()); + } + + @Override + public void onFailure(EventSource eventSource, Throwable t, Response response) { + future.completeExceptionally(new RuntimeException("Request closed before completion")); + } + }); + + return future; + } + + @Override + public EventSource chatCompletions(String apiHostByUser, String apiKeyByUser, ChatCompletionRequest chatCompletionRequest, EventSourceListener eventSourceListener) throws JsonProcessingException { + // 核心参数校验;不对用户的传参做更改,只返回错误信息。 + if (!chatCompletionRequest.isStream()) { + throw new RuntimeException("illegal parameter stream is false!"); + } + + // 动态设置 Host、Key,便于用户传递自己的信息 + String apiHost = Constants.NULL.equals(apiHostByUser) ? configuration.getApiHost() : apiHostByUser; + String apiKey = Constants.NULL.equals(apiKeyByUser) ? configuration.getApiKey() : apiKeyByUser; + + // 构建请求信息 + Request request = new Request.Builder() + // url: https://api.openai.com/v1/chat/completions - 通过 IOpenAiApi 配置的 POST 接口,用这样的方式从统一的地方获取配置信息 + .url(apiHost.concat(IOpenAiApi.v1_chat_completions)) + .addHeader("Authorization", "Bearer " + apiKey) + // 封装请求参数信息,如果使用了 Fastjson 也可以替换 ObjectMapper 转换对象 + .post(RequestBody.create(MediaType.parse(ContentType.JSON.getValue()), new ObjectMapper().writeValueAsString(chatCompletionRequest))) + .build(); + + // 返回结果信息;EventSource 对象可以取消应答 + return factory.newEventSource(request, eventSourceListener); + } + + @Override + public EditResponse edit(EditRequest editRequest) { + return this.openAiApi.edits(editRequest).blockingGet(); + } + + @Override + public ImageResponse genImages(String prompt) { + ImageRequest imageRequest = ImageRequest.builder().prompt(prompt).build(); + return this.genImages(imageRequest); + } + + @Override + public ImageResponse genImages(ImageRequest imageRequest) { + return this.openAiApi.genImages(imageRequest).blockingGet(); + } + + @Override + public ImageResponse editImages(File image, String prompt) { + ImageEditRequest imageEditRequest = ImageEditRequest.builder().prompt(prompt).build(); + return this.editImages(image, null, imageEditRequest); + } + + @Override + public ImageResponse editImages(File image, ImageEditRequest imageEditRequest) { + return this.editImages(image, null, imageEditRequest); + } + + @Override + public ImageResponse editImages(File image, File mask, ImageEditRequest imageEditRequest) { + // 1. imageMultipartBody + RequestBody imageBody = RequestBody.create(MediaType.parse("multipart/form-data"), image); + MultipartBody.Part imageMultipartBody = MultipartBody.Part.createFormData("image", image.getName(), imageBody); + // 2. maskMultipartBody + MultipartBody.Part maskMultipartBody = null; + if (Objects.nonNull(mask)) { + RequestBody maskBody = RequestBody.create(MediaType.parse("multipart/form-data"), mask); + maskMultipartBody = MultipartBody.Part.createFormData("mask", mask.getName(), maskBody); + } + // requestBodyMap + Map requestBodyMap = new HashMap<>(); + requestBodyMap.put("prompt", RequestBody.create(MediaType.parse("multipart/form-data"), imageEditRequest.getPrompt())); + requestBodyMap.put("n", RequestBody.create(MediaType.parse("multipart/form-data"), imageEditRequest.getN().toString())); + requestBodyMap.put("size", RequestBody.create(MediaType.parse("multipart/form-data"), imageEditRequest.getSize())); + requestBodyMap.put("response_format", RequestBody.create(MediaType.parse("multipart/form-data"), imageEditRequest.getResponseFormat())); + if (!(Objects.isNull(imageEditRequest.getUser()) || "".equals(imageEditRequest.getUser()))) { + requestBodyMap.put("user", RequestBody.create(MediaType.parse("multipart/form-data"), imageEditRequest.getUser())); + } + return this.openAiApi.editImages(imageMultipartBody, maskMultipartBody, requestBodyMap).blockingGet(); + } + + @Override + public EmbeddingResponse embeddings(String input) { + EmbeddingRequest embeddingRequest = EmbeddingRequest.builder().input(new ArrayList() {{ + add(input); + }}).build(); + return this.embeddings(embeddingRequest); + } + + @Override + public EmbeddingResponse embeddings(String... inputs) { + EmbeddingRequest embeddingRequest = EmbeddingRequest.builder().input(Arrays.asList(inputs)).build(); + return this.embeddings(embeddingRequest); + } + + @Override + public EmbeddingResponse embeddings(List inputs) { + EmbeddingRequest embeddingRequest = EmbeddingRequest.builder().input(inputs).build(); + return this.embeddings(embeddingRequest); + } + + @Override + public EmbeddingResponse embeddings(EmbeddingRequest embeddingRequest) { + return this.openAiApi.embeddings(embeddingRequest).blockingGet(); + } + + @Override + public OpenAiResponse files() { + return this.openAiApi.files().blockingGet(); + } + + @Override + public UploadFileResponse uploadFile(File file) { + return this.uploadFile("fine-tune", file); + } + + @Override + public UploadFileResponse uploadFile(String purpose, File file) { + RequestBody fileBody = RequestBody.create(MediaType.parse("multipart/form-data"), file); + MultipartBody.Part multipartBody = MultipartBody.Part.createFormData("file", file.getName(), fileBody); + RequestBody purposeBody = RequestBody.create(MediaType.parse("multipart/form-data"), purpose); + return this.openAiApi.uploadFile(multipartBody, purposeBody).blockingGet(); + } + + @Override + public DeleteFileResponse deleteFile(String fileId) { + return this.openAiApi.deleteFile(fileId).blockingGet(); + } + + @Override + public WhisperResponse speed2TextTranscriptions(File file, TranscriptionsRequest transcriptionsRequest) { + // 1. 语音文件 + RequestBody fileBody = RequestBody.create(MediaType.parse("multipart/form-data"), file); + MultipartBody.Part multipartBody = MultipartBody.Part.createFormData("file", file.getName(), fileBody); + // 2. 参数封装 + Map requestBodyMap = new HashMap<>(); + if (StrUtil.isNotBlank(transcriptionsRequest.getLanguage())) { + requestBodyMap.put(TranscriptionsRequest.Fields.language, RequestBody.create(MediaType.parse("multipart/form-data"), transcriptionsRequest.getLanguage())); + } + if (StrUtil.isNotBlank(transcriptionsRequest.getModel())) { + requestBodyMap.put(TranscriptionsRequest.Fields.model, RequestBody.create(MediaType.parse("multipart/form-data"), transcriptionsRequest.getModel())); + } + if (StrUtil.isNotBlank(transcriptionsRequest.getPrompt())) { + requestBodyMap.put(TranscriptionsRequest.Fields.prompt, RequestBody.create(MediaType.parse("multipart/form-data"), transcriptionsRequest.getPrompt())); + } + if (StrUtil.isNotBlank(transcriptionsRequest.getResponseFormat())) { + requestBodyMap.put(TranscriptionsRequest.Fields.responseFormat, RequestBody.create(MediaType.parse("multipart/form-data"), transcriptionsRequest.getResponseFormat())); + } + requestBodyMap.put(TranscriptionsRequest.Fields.temperature, RequestBody.create(MediaType.parse("multipart/form-data"), String.valueOf(transcriptionsRequest.getTemperature()))); + return this.openAiApi.speed2TextTranscriptions(multipartBody, requestBodyMap).blockingGet(); + } + + @Override + public WhisperResponse speed2TextTranslations(File file, TranslationsRequest translationsRequest) { + // 1. 语音文件 + RequestBody fileBody = RequestBody.create(MediaType.parse("multipart/form-data"), file); + MultipartBody.Part multipartBody = MultipartBody.Part.createFormData("file", file.getName(), fileBody); + // 2. 参数封装 + Map requestBodyMap = new HashMap<>(); + if (StrUtil.isNotBlank(translationsRequest.getModel())) { + requestBodyMap.put(TranslationsRequest.Fields.model, RequestBody.create(MediaType.parse("multipart/form-data"), translationsRequest.getModel())); + } + if (StrUtil.isNotBlank(translationsRequest.getPrompt())) { + requestBodyMap.put(TranslationsRequest.Fields.prompt, RequestBody.create(MediaType.parse("multipart/form-data"), translationsRequest.getPrompt())); + } + if (StrUtil.isNotBlank(translationsRequest.getResponseFormat())) { + requestBodyMap.put(TranslationsRequest.Fields.responseFormat, RequestBody.create(MediaType.parse("multipart/form-data"), translationsRequest.getResponseFormat())); + } + requestBodyMap.put(TranslationsRequest.Fields.temperature, RequestBody.create(MediaType.parse("multipart/form-data"), String.valueOf(translationsRequest.getTemperature()))); + requestBodyMap.put(TranscriptionsRequest.Fields.temperature, RequestBody.create(MediaType.parse("multipart/form-data"), String.valueOf(translationsRequest.getTemperature()))); + return this.openAiApi.speed2TextTranscriptions(multipartBody, requestBodyMap).blockingGet(); + } + + @Override + public Subscription subscription() { + return this.openAiApi.subscription().blockingGet(); + } + + @Override + public BillingUsage billingUsage(@NotNull LocalDate starDate, @NotNull LocalDate endDate) { + return this.openAiApi.billingUsage(starDate, endDate).blockingGet(); + } + +} \ No newline at end of file diff --git a/src/main/java/cn/kazusa/chatgpt/session/defaults/DefaultOpenAiSessionFactory.java b/src/main/java/cn/kazusa/chatgpt/session/defaults/DefaultOpenAiSessionFactory.java new file mode 100644 index 0000000..2bc7966 --- /dev/null +++ b/src/main/java/cn/kazusa/chatgpt/session/defaults/DefaultOpenAiSessionFactory.java @@ -0,0 +1,57 @@ +package cn.kazusa.chatgpt.session.defaults; + +import cn.kazusa.chatgpt.IOpenAiApi; +import cn.kazusa.chatgpt.interceptor.OpenAiInterceptor; +import cn.kazusa.chatgpt.session.Configuration; +import cn.kazusa.chatgpt.session.OpenAiSession; +import cn.kazusa.chatgpt.session.OpenAiSessionFactory; +import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; +import retrofit2.Retrofit; +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; +import retrofit2.converter.jackson.JacksonConverterFactory; + +import java.util.concurrent.TimeUnit; + +/** + * @author kazusa + * @description OpenAi API Factory 会话工厂 + */ +public class DefaultOpenAiSessionFactory implements OpenAiSessionFactory { + + private final Configuration configuration; + + public DefaultOpenAiSessionFactory(Configuration configuration) { + this.configuration = configuration; + } + + @Override + public OpenAiSession openSession() { + // 1. 日志配置 + HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(); + httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); + + // 2. 开启 Http 客户端 + OkHttpClient okHttpClient = new OkHttpClient + .Builder() + .addInterceptor(httpLoggingInterceptor) + .addInterceptor(new OpenAiInterceptor(configuration.getApiKey())) + .connectTimeout(450, TimeUnit.SECONDS) + .writeTimeout(450, TimeUnit.SECONDS) + .readTimeout(450, TimeUnit.SECONDS) + .build(); + configuration.setOkHttpClient(okHttpClient); + + // 3. 创建 API 服务 + IOpenAiApi openAiApi = new Retrofit.Builder() + .baseUrl(configuration.getApiHost()) + .client(okHttpClient) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .addConverterFactory(JacksonConverterFactory.create()) + .build().create(IOpenAiApi.class); + configuration.setOpenAiApi(openAiApi); + + return new DefaultOpenAiSession(configuration); + } + +} diff --git a/src/test/java/cn/kazusa/chatgpt/test/ApiTest.java b/src/test/java/cn/kazusa/chatgpt/test/ApiTest.java new file mode 100644 index 0000000..75baa58 --- /dev/null +++ b/src/test/java/cn/kazusa/chatgpt/test/ApiTest.java @@ -0,0 +1,305 @@ +package cn.kazusa.chatgpt.test; + + +import cn.kazusa.chatgpt.common.Constants; +import cn.kazusa.chatgpt.domain.billing.BillingUsage; +import cn.kazusa.chatgpt.domain.billing.Subscription; +import cn.kazusa.chatgpt.domain.chat.ChatCompletionRequest; +import cn.kazusa.chatgpt.domain.chat.ChatCompletionResponse; +import cn.kazusa.chatgpt.domain.chat.Message; +import cn.kazusa.chatgpt.domain.edits.EditRequest; +import cn.kazusa.chatgpt.domain.edits.EditResponse; +import cn.kazusa.chatgpt.domain.embedd.EmbeddingResponse; +import cn.kazusa.chatgpt.domain.files.DeleteFileResponse; +import cn.kazusa.chatgpt.domain.files.UploadFileResponse; +import cn.kazusa.chatgpt.domain.images.ImageResponse; +import cn.kazusa.chatgpt.domain.other.OpenAiResponse; +import cn.kazusa.chatgpt.domain.qa.QACompletionRequest; +import cn.kazusa.chatgpt.domain.qa.QACompletionResponse; +import cn.kazusa.chatgpt.session.Configuration; +import cn.kazusa.chatgpt.session.OpenAiSession; +import cn.kazusa.chatgpt.session.OpenAiSessionFactory; +import cn.kazusa.chatgpt.session.defaults.DefaultOpenAiSessionFactory; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import okhttp3.Response; +import okhttp3.sse.EventSource; +import okhttp3.sse.EventSourceListener; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.concurrent.CountDownLatch; + +/** + * @author kazusa + * @description OpenAI API单元测试类 + */ +@Slf4j +public class ApiTest { + + private OpenAiSession openAiSession; + + @Before + public void test_OpenAiSessionFactory() { + // 1. 配置文件 [联系小傅哥获取key] + // 1.1 官网原始 apiHost https://api.openai.com/ - 官网的Key可直接使用 + // 1.2 三方公司 apiHost https://pro-share-aws-api.zcyai.com/ - 需要找我获得 Key 【支持3.5\4.0流式问答模型调用,有些模型已废弃不对接使用】 + String gptSdkHost = System.getProperty("GPT_SDK_HOST"); + String gptSdkKey = System.getProperty("GPT_SDK_KEY"); + Configuration configuration = new Configuration(); + configuration.setApiHost(gptSdkHost); + configuration.setApiKey(gptSdkKey); + // 2. 会话工厂 + OpenAiSessionFactory factory = new DefaultOpenAiSessionFactory(configuration); + // 3. 开启会话 + this.openAiSession = factory.openSession(); + } + + /** + * 【常用对话模式,推荐使用此模型进行测试】 + * 此对话模型 3.5/4.0 接近于官网体验 & 流式应答 + */ + @Test + public void test_chat_completions_stream_channel() throws JsonProcessingException, InterruptedException { + // 1. 创建参数 + ChatCompletionRequest chatCompletion = ChatCompletionRequest + .builder() + .stream(true) + .messages(Collections.singletonList(Message.builder().role(Constants.Role.USER).content("1+1").build())) + .model(ChatCompletionRequest.Model.GPT_3_5_TURBO.getCode()) + .maxTokens(1024) + .build(); + + // 2. 用户配置 【可选参数,支持不同渠道的 apiHost、apiKey】- 方便给每个用户都分配了自己的key,用于售卖场景 + String apiHost = ""; + String apiKey = ""; + + // 3. 发起请求 + EventSource eventSource = openAiSession.chatCompletions(apiHost, apiKey, chatCompletion, new EventSourceListener() { + @Override + public void onEvent(EventSource eventSource, String id, String type, String data) { + log.info("测试结果 id:{} type:{} data:{}", id, type, data); + } + + @Override + public void onFailure(EventSource eventSource, Throwable t, Response response) { + log.error("失败 throwable:{}", t != null ? t.getMessage() : "Unknown error", t); + if (response != null) { + log.error("响应 code:{} message:{}", response.code(), response.message()); + } + } + }); + // 等待 + new CountDownLatch(1).await(); + } + + /** + * 【常用对话模式,推荐使用此模型进行测试】 + * 此对话模型 3.5/4.0 接近于官网体验 & 流式应答 + */ + @Test + public void test_chat_completions_stream() throws JsonProcessingException, InterruptedException { + // 1. 创建参数 + ChatCompletionRequest chatCompletion = ChatCompletionRequest + .builder() + .stream(true) + .messages(Collections.singletonList(Message.builder().role(Constants.Role.USER).content("1+1").build())) + .model(ChatCompletionRequest.Model.GPT_3_5_TURBO.getCode()) + .maxTokens(1024) + .build(); + + // 2. 发起请求 + EventSource eventSource = openAiSession.chatCompletions(chatCompletion, new EventSourceListener() { + @Override + public void onEvent(EventSource eventSource, String id, String type, String data) { + log.info("测试结果 id:{} type:{} data:{}", id, type, data); + } + + @Override + public void onFailure(EventSource eventSource, Throwable t, Response response) { + log.error("失败 throwable:{}", t != null ? t.getMessage() : "Unknown error", t); + if (response != null) { + log.error("响应 code:{} message:{}", response.code(), response.message()); + } + } + }); + + // 等待 + new CountDownLatch(1).await(); + } + + + /** + * 简单问答模式 + */ + @Test + public void test_qa_completions() throws JsonProcessingException { + QACompletionResponse response01 = openAiSession.completions("写个java冒泡排序"); + log.info("测试结果:{}", new ObjectMapper().writeValueAsString(response01.getChoices())); + } + + /** + * 简单问答模式 * 流式应答 + */ + @Test + public void test_qa_completions_stream() throws JsonProcessingException, InterruptedException { + // 1. 创建参数 + QACompletionRequest request = QACompletionRequest + .builder() + .prompt("写个java冒泡排序") + .stream(true) + .build(); + + for (int i = 0; i < 1; i++) { + // 2. 发起请求 + EventSource eventSource = openAiSession.completions(request, new EventSourceListener() { + @Override + public void onEvent(EventSource eventSource, String id, String type, String data) { + log.info("测试结果:{}", data); + } + }); + } + + // 等待 + new CountDownLatch(1).await(); + } + + /** + * 此对话模型 3.5 接近于官网体验 + */ + @Test + public void test_chat_completions() { + // 1. 创建参数 + ChatCompletionRequest chatCompletion = ChatCompletionRequest + .builder() + .messages(Collections.singletonList(Message.builder().role(Constants.Role.USER).content("写一个java冒泡排序").build())) + .model(ChatCompletionRequest.Model.GPT_3_5_TURBO.getCode()) + .build(); + // 2. 发起请求 + ChatCompletionResponse chatCompletionResponse = openAiSession.completions(chatCompletion); + // 3. 解析结果 + chatCompletionResponse.getChoices().forEach(e -> { + log.info("测试结果:{}", e.getMessage()); + }); + } + + /** + * 上下文对话 + */ + @Test + public void test_chat_completions_context() { + // 1-1. 创建参数 + ChatCompletionRequest chatCompletion = ChatCompletionRequest + .builder() + .messages(new ArrayList<>()) + .model(ChatCompletionRequest.Model.GPT_3_5_TURBO.getCode()) + .user("testUser01") + .build(); + // 写入请求信息 + chatCompletion.getMessages().add(Message.builder().role(Constants.Role.USER).content("写一个java冒泡排序").build()); + + // 1-2. 发起请求 + ChatCompletionResponse chatCompletionResponse01 = openAiSession.completions(chatCompletion); + log.info("测试结果:{}", chatCompletionResponse01.getChoices()); + + // 写入请求信息 + chatCompletion.getMessages().add(Message.builder().role(Constants.Role.USER).content(chatCompletionResponse01.getChoices().get(0).getMessage().getContent()).build()); + chatCompletion.getMessages().add(Message.builder().role(Constants.Role.USER).content("换一种写法").build()); + + ChatCompletionResponse chatCompletionResponse02 = openAiSession.completions(chatCompletion); + log.info("测试结果:{}", chatCompletionResponse02.getChoices()); + } + + /** + * 文本修复 + */ + @Test + public void test_edit() { + // 文本请求 + EditRequest textRequest = EditRequest.builder() + .input("码农会锁") + .instruction("帮我修改错字") + .model(EditRequest.Model.TEXT_DAVINCI_EDIT_001.getCode()).build(); + EditResponse textResponse = openAiSession.edit(textRequest); + log.info("测试结果:{}", textResponse); + + // 代码请求 + EditRequest codeRequest = EditRequest.builder() + // j <= 10 应该修改为 i <= 10 + .input("for (int i = 1; j <= 10; i++) {\n" + + " System.out.println(i);\n" + + "}") + .instruction("这段代码执行时报错,请帮我修改").model(EditRequest.Model.CODE_DAVINCI_EDIT_001.getCode()).build(); + EditResponse codeResponse = openAiSession.edit(codeRequest); + log.info("测试结果:{}", codeResponse); + } + + /** + * 生成图片 + */ + @Test + public void test_genImages() { + // 方式1,简单调用 + ImageResponse imageResponse01 = openAiSession.genImages("画一个996加班的程序员"); + log.info("测试结果:{}", imageResponse01); + +// // 方式2,调参调用 +// ImageResponse imageResponse02 = openAiSession.genImages(ImageRequest.builder() +// .prompt("画一个996加班的程序员") +// .size(ImageEnum.Size.size_256.getCode()) +// .responseFormat(ImageEnum.ResponseFormat.B64_JSON.getCode()).build()); +// log.info("测试结果:{}", imageResponse02); + } + + /** + * 修改图片,有3个方法,入参不同。 + */ + @Test + public void test_editImages() throws IOException { + ImageResponse imageResponse = openAiSession.editImages(new File("/Users/fuzhengwei/1024/KnowledgePlanet/chatgpt/chatgpt-sdk-java/docs/images/996.png"), "去除图片中的文字"); + log.info("测试结果:{}", imageResponse); + } + + @Test + public void test_embeddings() { + EmbeddingResponse embeddingResponse = openAiSession.embeddings("哈喽", "嗨", "hi!"); + log.info("测试结果:{}", embeddingResponse); + } + + @Test + public void test_files() { + OpenAiResponse openAiResponse = openAiSession.files(); + log.info("测试结果:{}", openAiResponse); + } + + @Test + public void test_uploadFile() { + UploadFileResponse uploadFileResponse = openAiSession.uploadFile(new File("/Users/fuzhengwei/1024/KnowledgePlanet/chatgpt/chatgpt-sdk-java/docs/files/introduce.md")); + log.info("测试结果:{}", uploadFileResponse); + } + + @Test + public void test_deleteFile() { + DeleteFileResponse deleteFileResponse = openAiSession.deleteFile("file id 上传后才能获得"); + log.info("测试结果:{}", deleteFileResponse); + } + + @Test + public void test_subscription() { + Subscription subscription = openAiSession.subscription(); + log.info("测试结果:{}", subscription); + } + + @Test + public void test_billingUsage() { + BillingUsage billingUsage = openAiSession.billingUsage(LocalDate.of(2023, 3, 20), LocalDate.now()); + log.info("测试结果:{}", billingUsage.getTotalUsage()); + } + +} diff --git a/src/test/java/cn/kazusa/chatgpt/test/HttpClientTest.java b/src/test/java/cn/kazusa/chatgpt/test/HttpClientTest.java new file mode 100644 index 0000000..e40babf --- /dev/null +++ b/src/test/java/cn/kazusa/chatgpt/test/HttpClientTest.java @@ -0,0 +1,140 @@ +package cn.kazusa.chatgpt.test; + + +import cn.hutool.http.ContentType; +import cn.hutool.http.Header; +import cn.kazusa.chatgpt.IOpenAiApi; +import cn.kazusa.chatgpt.common.Constants; +import cn.kazusa.chatgpt.domain.chat.ChatCompletionRequest; +import cn.kazusa.chatgpt.domain.chat.ChatCompletionResponse; +import cn.kazusa.chatgpt.domain.chat.Message; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.reactivex.Single; +import lombok.extern.slf4j.Slf4j; +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.logging.HttpLoggingInterceptor; +import okhttp3.sse.EventSource; +import okhttp3.sse.EventSourceListener; +import okhttp3.sse.EventSources; +import org.junit.Test; +import retrofit2.Retrofit; +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; +import retrofit2.converter.jackson.JacksonConverterFactory; + +import java.util.Collections; +import java.util.concurrent.CountDownLatch; + +/** + * @author kazusa + * @description HTTP客户端测试类 + */ +@Slf4j +public class HttpClientTest { + + @Test + public void test_client() { + HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(); + httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); + + OkHttpClient okHttpClient = new OkHttpClient + .Builder() + .addInterceptor(httpLoggingInterceptor) + .addInterceptor(chain -> { + Request original = chain.request(); + + // 从请求中获取 token 参数,并将其添加到请求路径中 + HttpUrl url = original.url().newBuilder() + .addQueryParameter("token", "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ4ZmciLCJleHAiOjE2ODM5NDU3NDcsImlhdCI6MTY4Mzk0MjE0NywianRpIjoiM2QyMDExMTYtNmVjMS00Y2UzLWJhYzgtYzYxYmVmN2ZmNWE5IiwidXNlcm5hbWUiOiJ4ZmcifQ.3FDvUNuNoGemKLhcgagy8WH7xHwRU37t--BuH0N9skg") + .build(); + + Request request = original.newBuilder() + .url(url) + .header(Header.AUTHORIZATION.getValue(), "Bearer " + "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") + .header(Header.CONTENT_TYPE.getValue(), ContentType.JSON.getValue()) + .method(original.method(), original.body()) + .build(); + return chain.proceed(request); + }) + .build(); + + IOpenAiApi openAiApi = new Retrofit.Builder() + .baseUrl("https://api.xfg.im/b8b6/") + .client(okHttpClient) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .addConverterFactory(JacksonConverterFactory.create()) + .build().create(IOpenAiApi.class); + + Message message = Message.builder().role(Constants.Role.USER).content("写一个java冒泡排序").build(); + ChatCompletionRequest chatCompletion = ChatCompletionRequest + .builder() + .messages(Collections.singletonList(message)) + .model(ChatCompletionRequest.Model.GPT_3_5_TURBO.getCode()) + .build(); + + Single chatCompletionResponseSingle = openAiApi.completions(chatCompletion); + ChatCompletionResponse chatCompletionResponse = chatCompletionResponseSingle.blockingGet(); + chatCompletionResponse.getChoices().forEach(e -> { + System.out.println(e.getMessage()); + }); + + } + + @Test + public void test_client_stream() throws JsonProcessingException, InterruptedException { + HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(); + httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); + + OkHttpClient okHttpClient = new OkHttpClient + .Builder() + .addInterceptor(httpLoggingInterceptor) + .addInterceptor(chain -> { + Request original = chain.request(); + + // 从请求中获取 token 参数,并将其添加到请求路径中 + HttpUrl url = original.url().newBuilder() + .addQueryParameter("token", "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ4ZmciLCJleHAiOjE2ODM5NDU3NDcsImlhdCI6MTY4Mzk0MjE0NywianRpIjoiM2QyMDExMTYtNmVjMS00Y2UzLWJhYzgtYzYxYmVmN2ZmNWE5IiwidXNlcm5hbWUiOiJ4ZmcifQ.3FDvUNuNoGemKLhcgagy8WH7xHwRU37t--BuH0N9skg") + .build(); + + Request request = original.newBuilder() + .url(url) + .header(Header.AUTHORIZATION.getValue(), "Bearer " + "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") + .header(Header.CONTENT_TYPE.getValue(), ContentType.JSON.getValue()) + .method(original.method(), original.body()) + .build(); + return chain.proceed(request); + }) + .build(); + + Message message = Message.builder().role(Constants.Role.USER).content("写一个java冒泡排序").build(); + ChatCompletionRequest chatCompletion = ChatCompletionRequest + .builder() + .messages(Collections.singletonList(message)) + .model(ChatCompletionRequest.Model.GPT_3_5_TURBO.getCode()) + .stream(true) + .build(); + + EventSource.Factory factory = EventSources.createFactory(okHttpClient); + String requestBody = new ObjectMapper().writeValueAsString(chatCompletion); + + Request request = new Request.Builder() + .url("https://api.xfg.im/b8b6/v1/chat/completions") + .post(RequestBody.create(MediaType.parse(ContentType.JSON.getValue()), requestBody)) + .build(); + + EventSource eventSource = factory.newEventSource(request, new EventSourceListener() { + @Override + public void onEvent(EventSource eventSource, String id, String type, String data) { + log.info("测试结果:{}", data); + } + }); + + // 等待 + new CountDownLatch(1).await(); + } + +}