对接 openai dall-e-3,发送请求解析地址存在问题,链接不上

This commit is contained in:
cherishsince 2024-03-17 11:19:58 +08:00
parent 633b112603
commit a14aebd5ee
8 changed files with 412 additions and 3 deletions

View File

@ -0,0 +1,54 @@
package cn.iocoder.yudao.framework.ai.imageopenai;
import cn.iocoder.yudao.framework.ai.imageopenai.api.OpenAiImageRequest;
import cn.iocoder.yudao.framework.ai.imageopenai.api.OpenAiImageResponse;
import cn.iocoder.yudao.framework.ai.util.JacksonUtil;
import io.netty.channel.ChannelOption;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;
import java.time.Duration;
/**
* open ai image
* <p>
* author: fansili
* time: 2024/3/17 09:53
*/
public class OpenAiImageApi {
private static final String DEFAULT_BASE_URL = "https://api.openai.com";
private String apiKey = "your-api-key";
// 发送请求 webClient
private final WebClient webClient;
public OpenAiImageApi(String apiKey) {
this.apiKey = apiKey;
// 创建一个HttpClient实例并设置超时
HttpClient httpClient = HttpClient.create()
.responseTimeout(Duration.ofSeconds(300)) // 设置响应超时时间为30秒
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000 * 100); // 设置连接超时为5秒
this.webClient = WebClient.builder()
.baseUrl(DEFAULT_BASE_URL)
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
}
public OpenAiImageResponse createImage(OpenAiImageRequest request) {
String res = webClient.post()
.uri(uriBuilder -> uriBuilder.path("/v1/images/generations").build())
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + apiKey)
// 设置请求体这里假设jsonStr是一个JSON格式的字符串
.body(BodyInserters.fromValue(JacksonUtil.toJson(request)))
// 发送请求并获取响应体
.retrieve()
// 转换响应体为String类型
.bodyToMono(String.class)
.block();
// TODO: 2024/3/17 这里发送请求会失败
return null;
}
}

View File

@ -0,0 +1,82 @@
package cn.iocoder.yudao.framework.ai.imageopenai;
import cn.hutool.core.bean.BeanUtil;
import cn.iocoder.yudao.framework.ai.chat.ChatException;
import cn.iocoder.yudao.framework.ai.chatyiyan.exception.YiYanApiException;
import cn.iocoder.yudao.framework.ai.image.ImageClient;
import cn.iocoder.yudao.framework.ai.image.ImageOptions;
import cn.iocoder.yudao.framework.ai.image.ImagePrompt;
import cn.iocoder.yudao.framework.ai.image.ImageResponse;
import cn.iocoder.yudao.framework.ai.imageopenai.api.OpenAiImageRequest;
import cn.iocoder.yudao.framework.ai.imageopenai.api.OpenAiImageResponse;
import jdk.jfr.Frequency;
import lombok.extern.slf4j.Slf4j;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryListener;
import org.springframework.retry.support.RetryTemplate;
import java.time.Duration;
/**
* open ai 生成 image
*
* author: fansili
* time: 2024/3/17 09:51
*/
@Slf4j
public class OpenAiImageClient implements ImageClient {
/**
* open image ai
*/
private OpenAiImageApi openAiImageApi;
/**
* 默认使用的 ImageOptions
*/
private OpenAiImageOptions defaultImageOptions;
public final RetryTemplate retryTemplate = RetryTemplate.builder()
// 最大重试次数 10
.maxAttempts(10)
.retryOn(YiYanApiException.class)
// 最大重试5次第一次间隔3000ms第二次3000ms * 2第三次3000ms * 3以此类推最大间隔3 * 60000ms
.exponentialBackoff(Duration.ofMillis(3000), 2, Duration.ofMillis(3 * 60000))
.withListener(new RetryListener() {
@Override
public <T extends Object, E extends Throwable> void onError(RetryContext context,
RetryCallback<T, E> callback, Throwable throwable) {
log.warn("重试异常:" + context.getRetryCount(), throwable);
};
})
.build();
public OpenAiImageClient(OpenAiImageApi openAiImageApi, OpenAiImageOptions defaultImageOptions) {
this.openAiImageApi = openAiImageApi;
this.defaultImageOptions = defaultImageOptions;
}
@Override
public ImageResponse call(ImagePrompt imagePrompt) {
return this.retryTemplate.execute(ctx -> {
// 检查是否配置了 OpenAiImageOptions
if (defaultImageOptions == null && imagePrompt.getOptions() == null) {
throw new ChatException("OpenAiImageOptions 未配置参数!");
}
// 优先使用 request 中的 ImageOptions
ImageOptions useImageOptions = imagePrompt.getOptions() == null ? defaultImageOptions : imagePrompt.getOptions();
if (!(useImageOptions instanceof OpenAiImageOptions)) {
throw new ChatException("配置信息不正确,传入的必须是 OpenAiImageOptions!");
}
// 转换 OpenAiImageOptions
OpenAiImageOptions openAiImageOptions = (OpenAiImageOptions) useImageOptions;
// 创建请求
OpenAiImageRequest request = new OpenAiImageRequest();
BeanUtil.copyProperties(openAiImageOptions, request);
// 发送请求
OpenAiImageResponse response = openAiImageApi.createImage(request);
return null;
});
}
}

View File

@ -0,0 +1,77 @@
package cn.iocoder.yudao.framework.ai.imageopenai;
import cn.iocoder.yudao.framework.ai.image.ImageOptions;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* open ai 配置文件
*
* 文档地址https://platform.openai.com/docs/api-reference/images/create
*
* author: fansili
* time: 2024/3/17 09:53
*/
@Data
@Accessors(chain = true)
public class OpenAiImageOptions implements ImageOptions {
// 必填字段用于描述期望生成图像的文字说明对于dall-e-2模型最大长度为1000个字符对于dall-e-3模型最大长度为4000个字符
private String prompt;
// 可选字段默认为dall-e-2
// 指定用于生成图像的模型名称
private String model = "dall-e-2";
// 可选字段默认为1
// 生成图像的数量必须在1到10之间对于dall-e-3模型目前仅支持n=1
private Integer n = 1;
// 可选字段默认为standard
// 设置生成图像的质量hd质量将创建细节更丰富图像整体一致性更高的图片该参数仅对dall-e-3模型有效
private String quality = "standard";
// 可选字段默认为url
// 返回生成图像的格式必须是url或b64_json中的一种URL链接的有效期是从生成图像后开始计算的60分钟内有效
private String responseFormat = "url";
// 可选字段默认为1024x1024
// 生成图像的尺寸大小对于dall-e-2模型尺寸可为256x256, 512x512, 1024x1024对于dall-e-3模型尺寸可为1024x1024, 1792x1024, 1024x1792
private String imageSize = "1024x1024";
// 可选字段默认为vivid
// 图像生成的风格可为vivid生动或natural自然vivid会使模型偏向生成超现实和戏剧性的图像而natural则会让模型产出更自然不那么超现实的图像该参数仅对dall-e-3模型有效
private String style = "vivid";
// 可选字段
// 代表您的终端用户的唯一标识符有助于OpenAI监控并检测滥用行为了解更多信息请参考官方文档
private String endUserId;
//
// 适配 spring ai
@Override
public Integer getN() {
return this.n;
}
@Override
public String getModel() {
return this.model;
}
@Override
public Integer getWidth() {
return null;
}
@Override
public Integer getHeight() {
return null;
}
@Override
public String getResponseFormat() {
return this.responseFormat;
}
}

View File

@ -0,0 +1,58 @@
package cn.iocoder.yudao.framework.ai.imageopenai.api;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* open ai 配置文件
*
* 文档地址https://platform.openai.com/docs/api-reference/images/create
*
* author: fansili
* time: 2024/3/17 09:53
*/
@Data
@Accessors(chain = true)
public class OpenAiImageRequest {
// 必填字段用于描述期望生成图像的文字说明对于dall-e-2模型最大长度为1000个字符对于dall-e-3模型最大长度为4000个字符
@JsonProperty("prompt")
private String prompt;
// 可选字段默认为dall-e-2dall-e-3
// 指定用于生成图像的模型名称
@JsonProperty("model")
private String model = "dall-e-2";
// 可选字段默认为1
// 生成图像的数量必须在1到10之间对于dall-e-3模型目前仅支持n=1
@JsonProperty("n")
private Integer n = 1;
// 可选字段默认为standard
// 设置生成图像的质量hd质量将创建细节更丰富图像整体一致性更高的图片该参数仅对dall-e-3模型有效
@JsonProperty("quality")
private String quality = "standard";
// 可选字段默认为url
// 返回生成图像的格式必须是url或b64_json中的一种URL链接的有效期是从生成图像后开始计算的60分钟内有效
@JsonProperty("response_format")
private String responseFormat = "url";
// 可选字段默认为1024x1024
// 生成图像的尺寸大小对于dall-e-2模型尺寸可为256x256, 512x512, 1024x1024对于dall-e-3模型尺寸可为1024x1024, 1792x1024, 1024x1792
@JsonProperty("size")
private String imageSize = "1024x1024";
// 可选字段默认为vivid
// 图像生成的风格可为vivid生动或natural自然vivid会使模型偏向生成超现实和戏剧性的图像而natural则会让模型产出更自然不那么超现实的图像该参数仅对dall-e-3模型有效
@JsonProperty("style")
private String style = "vivid";
// 可选字段
// 代表您的终端用户的唯一标识符有助于OpenAI监控并检测滥用行为了解更多信息请参考官方文档
@JsonProperty("user")
private String endUserId;
}

View File

@ -0,0 +1,28 @@
package cn.iocoder.yudao.framework.ai.imageopenai.api;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.List;
/**
* image 返回
*
* author: fansili
* time: 2024/3/17 10:27
*/
@Data
@Accessors(chain = true)
public class OpenAiImageResponse {
private long created;
private List<Item> data;
@Data
@Accessors(chain = true)
public static class Item {
private String url;
}
}

View File

@ -0,0 +1,79 @@
package cn.iocoder.yudao.framework.ai.util;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import java.io.IOException;
/**
* Jackson工具类
*
* author: fansili
* time: 2024/3/17 10:13
*/
public class JacksonUtil {
private static final ObjectMapper objectMapper = new ObjectMapper();
/**
* 初始化 ObjectMapper 以美化输出即格式化JSON内容
*/
static {
// 美化输出缩进
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
// 忽略值为 null 的属性
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
// 配置一个模块来将 Long 类型转换为 String 类型
SimpleModule module = new SimpleModule();
module.addSerializer(Long.class, ToStringSerializer.instance);
objectMapper.registerModule(module);
}
/**
* 将对象转换为 JSON 字符串
*
* @param obj 需要序列化的Java对象
* @return 序列化后的 JSON 字符串
* @throws JsonProcessingException JSON 序列化过程中出现错误时抛出异常
*/
public static String toJson(Object obj) {
try {
return objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
/**
* JSON 字符串反序列化为指定类型的对象
*
* @param json JSON 字符串
* @param clazz 目标类型 Class 对象
* @param <T> 泛型类型参数
* @return 反序列化后的 Java 对象
* @throws IOException JSON 解析过程中出现错误时抛出异常
*/
public static <T> T fromJson(String json, Class<T> clazz) {
try {
return objectMapper.readValue(json, clazz);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
/**
* 将对象转换为格式化的 JSON 字符串已启用 INDENT_OUTPUT 功能所以所有方法都会返回格式化后的 JSON
*
* @param obj 需要序列化的Java对象
* @return 格式化后的 JSON 字符串
* @throws JsonProcessingException JSON 序列化过程中出现错误时抛出异常
*/
public static String toFormattedJson(Object obj) {
// 已在类初始化时设置了 SerializationFeature.INDENT_OUTPUT此处无需额外操作
return toJson(obj);
}
}

View File

@ -23,9 +23,9 @@ public class QianWenChatClientTests {
@Before @Before
public void setup() { public void setup() {
QianWenApi qianWenApi = new QianWenApi( QianWenApi qianWenApi = new QianWenApi(
"", "LTAI5tNTVhXW4fLKUjMrr98z",
"", "ZJ0JQeyjzxxm5CfeTV6k1wNE9UsvZP",
"", "f0c1088824594f589c8f10567ccd929f_p_efm",
null null
); );
qianWenChatClient = new QianWenChatClient( qianWenChatClient = new QianWenChatClient(

View File

@ -0,0 +1,31 @@
package cn.iocoder.yudao.framework.ai.image;
import cn.iocoder.yudao.framework.ai.imageopenai.OpenAiImageApi;
import cn.iocoder.yudao.framework.ai.imageopenai.OpenAiImageClient;
import cn.iocoder.yudao.framework.ai.imageopenai.OpenAiImageOptions;
import org.junit.Before;
import org.junit.Test;
/**
* author: fansili
* time: 2024/3/17 10:40
*/
public class OpenAiImageClientTests {
private OpenAiImageClient openAiImageClient;
@Before
public void setup() {
// 初始化 openAiImageClient
this.openAiImageClient = new OpenAiImageClient(
new OpenAiImageApi(""),
new OpenAiImageOptions()
);
}
@Test
public void callTest() {
openAiImageClient.call(new ImagePrompt("我和我的小狗,一起在北极和企鹅玩排球。"));
}
}