1package dev.rafex.ether.ai.deepseek.chat;
29import java.io.IOException;
30import java.net.http.HttpClient;
31import java.net.http.HttpRequest;
32import java.net.http.HttpResponse;
33import java.nio.charset.StandardCharsets;
34import java.util.LinkedHashMap;
36import java.util.Objects;
38import com.fasterxml.jackson.databind.JsonNode;
40import dev.rafex.ether.ai.core.chat.AiChatModel;
41import dev.rafex.ether.ai.core.chat.AiChatRequest;
42import dev.rafex.ether.ai.core.chat.AiChatResponse;
43import dev.rafex.ether.ai.core.error.AiHttpException;
44import dev.rafex.ether.ai.core.message.AiMessage;
45import dev.rafex.ether.ai.core.message.AiMessageRole;
46import dev.rafex.ether.ai.core.usage.AiUsage;
47import dev.rafex.ether.ai.deepseek.config.DeepSeekConfig;
48import dev.rafex.ether.json.JsonCodec;
49import dev.rafex.ether.json.JsonUtils;
61 private final DeepSeekConfig config;
64 private final HttpClient httpClient;
75 this(config, HttpClient.newBuilder().connectTimeout(config.timeout()).build(),
JsonUtils.
codec());
86 this.config = Objects.requireNonNull(config,
"config");
87 this.httpClient = Objects.requireNonNull(httpClient,
"httpClient");
88 this.jsonCodec = jsonCodec ==
null ?
JsonUtils.
codec() : jsonCodec;
101 public AiChatResponse
generate(
final AiChatRequest request)
throws IOException, InterruptedException {
102 final byte[] payload = jsonCodec.toJsonBytes(toPayload(request));
103 final var builder = HttpRequest.newBuilder(config.chatCompletionsUri()).timeout(config.timeout())
104 .header(
"Authorization",
"Bearer " + config.apiKey()).header(
"Content-Type",
"application/json")
105 .POST(HttpRequest.BodyPublishers.ofByteArray(payload));
106 config.defaultHeaders().forEach(builder::header);
108 final var response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofByteArray());
109 if (response.statusCode() < 200 || response.statusCode() >= 300) {
110 throw new AiHttpException(
"DeepSeek request failed with HTTP " + response.statusCode(),
111 response.statusCode(),
new String(response.body(), StandardCharsets.UTF_8));
114 final JsonNode root = jsonCodec.readTree(response.body());
115 final JsonNode choice = root.path(
"choices").path(0);
116 final JsonNode messageNode = choice.path(
"message");
118 text(messageNode,
"content"));
119 return new AiChatResponse(text(root,
"id"), text(root,
"model"), message, text(choice,
"finish_reason"),
120 usage(root.path(
"usage")));
127 private static Map<String, Object> toPayload(
final AiChatRequest request) {
128 final var payload =
new LinkedHashMap<String, Object>();
129 payload.put(
"model", request.model());
130 payload.put(
"messages", request.messages().stream()
131 .map(message -> Map.of(
"role", message.role().wireValue(),
"content", message.content())).toList());
132 if (request.temperature() !=
null) {
133 payload.put(
"temperature", request.temperature());
135 if (request.maxOutputTokens() !=
null) {
136 payload.put(
"max_tokens", request.maxOutputTokens());
146 private static String text(
final JsonNode node,
final String fieldName)
throws IOException {
147 final JsonNode field = node.path(fieldName);
148 if (field.isMissingNode() || field.isNull()) {
149 throw new IOException(
"Missing JSON field: " + fieldName);
151 return field.asText();
158 private static AiUsage usage(
final JsonNode node) {
159 if (node ==
null || node.isMissingNode() || node.isNull()) {
160 return AiUsage.empty();
162 return new AiUsage(node.path(
"prompt_tokens").asInt(0), node.path(
"completion_tokens").asInt(0),
163 node.path(
"total_tokens").asInt(0));
AiChatResponse generate(final AiChatRequest request)
Sends a chat completion request to the DeepSeek API and returns the parsed response.
DeepSeekChatModel(final DeepSeekConfig config, final HttpClient httpClient, final JsonCodec jsonCodec)
Creates a new model with a custom HTTP client and JSON codec.
DeepSeekChatModel(final DeepSeekConfig config)
Creates a new model with a default HTTP client and JSON codec.
static AiMessageRole fromWireValue(final String value)