feat: Support OLLAMA provider
- Added OllamaClient to handle conversations with the Ollama API. - Introduced OllamaDialect for managing thinking modes. - Created data classes: OllamaFunctionCall, OllamaListedModel, OllamaMessage, OllamaShowRequest, OllamaShowResponse, OllamaTagsResponse, OllamaToolCall, OllamaToolDefinition, and OllamaToolFunction for structured API communication. - Developed unit tests for OllamaClient to validate model listing, chat completion, streaming responses, and tool calling capabilities. - Configured test properties for local Ollama instance.
This commit is contained in:
@@ -31,7 +31,6 @@ import de.extio.lmlib.client.Conversation.Turn;
|
||||
import de.extio.lmlib.client.Conversation.TurnType;
|
||||
import de.extio.lmlib.profile.ModelCategory;
|
||||
import de.extio.lmlib.profile.ModelProfile;
|
||||
import de.extio.lmlib.profile.ModelProfile.ModelProvider;
|
||||
|
||||
public interface BaseAgent {
|
||||
|
||||
@@ -83,15 +82,7 @@ public interface BaseAgent {
|
||||
return ToolCallData.auto(toolDefinitions);
|
||||
}
|
||||
|
||||
default boolean supportsToolCalling(final AgentContext context) {
|
||||
final var modelProfile = this.modelProfile(context);
|
||||
return modelProfile != null && modelProfile.modelProvider() == ModelProvider.OAI_CHAT_COMPLETION;
|
||||
}
|
||||
|
||||
default boolean supportsToolCalling(final AgentContext context, final ClientService clientService) {
|
||||
if (clientService == null) {
|
||||
return this.supportsToolCalling(context);
|
||||
}
|
||||
final var modelProfile = this.modelProfile(context);
|
||||
if (modelProfile != null) {
|
||||
return clientService.supportsToolCalling(modelProfile);
|
||||
@@ -161,7 +152,9 @@ public interface BaseAgent {
|
||||
final boolean skipCache = split.context().isSkipCache() || split.context().isAlwaysSkipCache();
|
||||
split.context().setSkipCache(false);
|
||||
final var conversation = this.setupConversation(split);
|
||||
var toolCallData = completionClient.supportsToolCalling() ? this.toolCallData(split.context()) : null;
|
||||
final var requestedModelProfile = this.modelProfile(split.context());
|
||||
final var supportsToolCalling = requestedModelProfile != null ? completionClient.supportsToolCalling(requestedModelProfile) : completionClient.supportsToolCalling(this.modelCategory(split.context()));
|
||||
var toolCallData = supportsToolCalling ? this.toolCallData(split.context()) : null;
|
||||
this.rememberToolCallData(split.context(), toolCallData);
|
||||
|
||||
boolean parseable = false;
|
||||
|
||||
@@ -21,5 +21,7 @@ public interface Client {
|
||||
|
||||
ModelProvider getModelProvider();
|
||||
|
||||
boolean supportsToolCalling();
|
||||
boolean supportsToolCalling(ModelCategory modelCategory);
|
||||
|
||||
boolean supportsToolCalling(ModelProfile modelProfile);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
import de.extio.lmlib.client.ollama.OllamaClient;
|
||||
import de.extio.lmlib.client.oai.completion.chat.ChatCompletionClient;
|
||||
import de.extio.lmlib.client.oai.completion.text.TextCompletionClient;
|
||||
import de.extio.lmlib.profile.ProfileAutoConfiguration;
|
||||
@@ -33,5 +34,11 @@ public class ClientAutoConfiguration {
|
||||
ChatCompletionClient chatCompletionClient() {
|
||||
return new ChatCompletionClient();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
OllamaClient ollamaClient() {
|
||||
return new OllamaClient();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ public class ClientService {
|
||||
}
|
||||
|
||||
public boolean supportsToolCalling(final ModelCategory category) {
|
||||
return this.getClient(category).supportsToolCalling();
|
||||
return this.getClient(category).supportsToolCalling(category);
|
||||
}
|
||||
|
||||
@Cacheable("clients")
|
||||
@@ -56,7 +56,7 @@ public class ClientService {
|
||||
}
|
||||
|
||||
public boolean supportsToolCalling(final ModelProfile modelProfile) {
|
||||
return this.getClient(modelProfile).supportsToolCalling();
|
||||
return this.getClient(modelProfile).supportsToolCalling(modelProfile);
|
||||
}
|
||||
|
||||
private static class InterceptingClient implements Client {
|
||||
@@ -122,8 +122,13 @@ public class ClientService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsToolCalling() {
|
||||
return this.client.supportsToolCalling();
|
||||
public boolean supportsToolCalling(final ModelCategory modelCategory) {
|
||||
return this.client.supportsToolCalling(modelCategory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsToolCalling(final ModelProfile modelProfile) {
|
||||
return this.client.supportsToolCalling(modelProfile);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -61,7 +61,8 @@ public final class Conversation {
|
||||
if (completion == null) {
|
||||
throw new IllegalArgumentException("completion must not be null");
|
||||
}
|
||||
this.appendToolCallRound(completion.response(), completion.toolCalls(), toolResults);
|
||||
final var assistantText = completion.toolCalls().isEmpty() ? completion.response() : "";
|
||||
this.appendToolCallRound(assistantText, completion.toolCalls(), toolResults);
|
||||
}
|
||||
|
||||
public void appendToolCallRound(final String assistantText, final List<ToolCall> toolCalls, final List<ToolResult> toolResults) {
|
||||
|
||||
@@ -78,8 +78,13 @@ public class CachedClient implements Client {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsToolCalling() {
|
||||
return this.client.supportsToolCalling();
|
||||
public boolean supportsToolCalling(final ModelCategory modelCategory) {
|
||||
return this.client.supportsToolCalling(modelCategory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsToolCalling(final ModelProfile modelProfile) {
|
||||
return this.client.supportsToolCalling(modelProfile);
|
||||
}
|
||||
|
||||
private Completion getCachedResponse(final ModelCategory modelCategory, final ModelProfile modelProfile, final boolean skipCache, final Supplier<Completion> supplier, final String... keys) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import de.extio.lmlib.client.ToolCallData;
|
||||
import de.extio.lmlib.client.ToolDefinition;
|
||||
import de.extio.lmlib.client.Conversation.Turn;
|
||||
import de.extio.lmlib.client.oai.completion.AbstractCompletionClient;
|
||||
import de.extio.lmlib.profile.ModelCategory;
|
||||
import de.extio.lmlib.profile.ModelProfile;
|
||||
import de.extio.lmlib.profile.ModelProfile.ModelProvider;
|
||||
|
||||
@@ -36,8 +37,14 @@ public class ChatCompletionClient extends AbstractCompletionClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsToolCalling() {
|
||||
return true;
|
||||
public boolean supportsToolCalling(final ModelCategory modelCategory) {
|
||||
final var modelProfile = this.modelProfileService.getModelProfile(modelCategory.getModelProfile(), modelCategory);
|
||||
return this.supportsToolCalling(modelProfile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsToolCalling(final ModelProfile modelProfile) {
|
||||
return modelProfile != null && modelProfile.modelProvider() == ModelProvider.OAI_CHAT_COMPLETION;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -23,6 +23,7 @@ import de.extio.lmlib.client.Conversation;
|
||||
import de.extio.lmlib.client.ToolCallData;
|
||||
import de.extio.lmlib.client.Conversation.Turn;
|
||||
import de.extio.lmlib.client.oai.completion.AbstractCompletionClient;
|
||||
import de.extio.lmlib.profile.ModelCategory;
|
||||
import de.extio.lmlib.profile.ModelProfile;
|
||||
import de.extio.lmlib.profile.ModelProfile.ModelProvider;
|
||||
import de.extio.lmlib.prompt.PromptStrategy;
|
||||
@@ -41,7 +42,12 @@ public class TextCompletionClient extends AbstractCompletionClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsToolCalling() {
|
||||
public boolean supportsToolCalling(final ModelCategory modelCategory) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsToolCalling(final ModelProfile modelProfile) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package de.extio.lmlib.client.ollama;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
final class OllamaChatRequest {
|
||||
|
||||
private String model;
|
||||
|
||||
private List<OllamaMessage> messages;
|
||||
|
||||
private List<OllamaToolDefinition> tools;
|
||||
|
||||
private Map<String, Object> options;
|
||||
|
||||
private Boolean stream;
|
||||
|
||||
private Object think;
|
||||
|
||||
@JsonProperty("model")
|
||||
public String getModel() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
public void setModel(final String model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
@JsonProperty("messages")
|
||||
public List<OllamaMessage> getMessages() {
|
||||
return this.messages;
|
||||
}
|
||||
|
||||
public void setMessages(final List<OllamaMessage> messages) {
|
||||
this.messages = messages;
|
||||
}
|
||||
|
||||
@JsonProperty("tools")
|
||||
public List<OllamaToolDefinition> getTools() {
|
||||
return this.tools;
|
||||
}
|
||||
|
||||
public void setTools(final List<OllamaToolDefinition> tools) {
|
||||
this.tools = tools;
|
||||
}
|
||||
|
||||
@JsonProperty("options")
|
||||
public Map<String, Object> getOptions() {
|
||||
return this.options;
|
||||
}
|
||||
|
||||
public void setOptions(final Map<String, Object> options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
@JsonProperty("stream")
|
||||
public Boolean getStream() {
|
||||
return this.stream;
|
||||
}
|
||||
|
||||
public void setStream(final Boolean stream) {
|
||||
this.stream = stream;
|
||||
}
|
||||
|
||||
@JsonProperty("think")
|
||||
public Object getThink() {
|
||||
return this.think;
|
||||
}
|
||||
|
||||
public void setThink(final Object think) {
|
||||
this.think = think;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package de.extio.lmlib.client.ollama;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
final class OllamaChatResponse {
|
||||
|
||||
private String model;
|
||||
|
||||
private String createdAt;
|
||||
|
||||
private OllamaMessage message;
|
||||
|
||||
private Boolean done;
|
||||
|
||||
private String doneReason;
|
||||
|
||||
private Long totalDuration;
|
||||
|
||||
private Long loadDuration;
|
||||
|
||||
private Integer promptEvalCount;
|
||||
|
||||
private Long promptEvalDuration;
|
||||
|
||||
private Integer evalCount;
|
||||
|
||||
private Long evalDuration;
|
||||
|
||||
private String error;
|
||||
|
||||
@JsonProperty("model")
|
||||
public String getModel() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
public void setModel(final String model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
@JsonProperty("created_at")
|
||||
public String getCreatedAt() {
|
||||
return this.createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(final String createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
@JsonProperty("message")
|
||||
public OllamaMessage getMessage() {
|
||||
return this.message;
|
||||
}
|
||||
|
||||
public void setMessage(final OllamaMessage message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@JsonProperty("done")
|
||||
public Boolean getDone() {
|
||||
return this.done;
|
||||
}
|
||||
|
||||
public void setDone(final Boolean done) {
|
||||
this.done = done;
|
||||
}
|
||||
|
||||
@JsonProperty("done_reason")
|
||||
public String getDoneReason() {
|
||||
return this.doneReason;
|
||||
}
|
||||
|
||||
public void setDoneReason(final String doneReason) {
|
||||
this.doneReason = doneReason;
|
||||
}
|
||||
|
||||
@JsonProperty("total_duration")
|
||||
public Long getTotalDuration() {
|
||||
return this.totalDuration;
|
||||
}
|
||||
|
||||
public void setTotalDuration(final Long totalDuration) {
|
||||
this.totalDuration = totalDuration;
|
||||
}
|
||||
|
||||
@JsonProperty("load_duration")
|
||||
public Long getLoadDuration() {
|
||||
return this.loadDuration;
|
||||
}
|
||||
|
||||
public void setLoadDuration(final Long loadDuration) {
|
||||
this.loadDuration = loadDuration;
|
||||
}
|
||||
|
||||
@JsonProperty("prompt_eval_count")
|
||||
public Integer getPromptEvalCount() {
|
||||
return this.promptEvalCount;
|
||||
}
|
||||
|
||||
public void setPromptEvalCount(final Integer promptEvalCount) {
|
||||
this.promptEvalCount = promptEvalCount;
|
||||
}
|
||||
|
||||
@JsonProperty("prompt_eval_duration")
|
||||
public Long getPromptEvalDuration() {
|
||||
return this.promptEvalDuration;
|
||||
}
|
||||
|
||||
public void setPromptEvalDuration(final Long promptEvalDuration) {
|
||||
this.promptEvalDuration = promptEvalDuration;
|
||||
}
|
||||
|
||||
@JsonProperty("eval_count")
|
||||
public Integer getEvalCount() {
|
||||
return this.evalCount;
|
||||
}
|
||||
|
||||
public void setEvalCount(final Integer evalCount) {
|
||||
this.evalCount = evalCount;
|
||||
}
|
||||
|
||||
@JsonProperty("eval_duration")
|
||||
public Long getEvalDuration() {
|
||||
return this.evalDuration;
|
||||
}
|
||||
|
||||
public void setEvalDuration(final Long evalDuration) {
|
||||
this.evalDuration = evalDuration;
|
||||
}
|
||||
|
||||
@JsonProperty("error")
|
||||
public String getError() {
|
||||
return this.error;
|
||||
}
|
||||
|
||||
public void setError(final String error) {
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
void copyFrom(final OllamaChatResponse other) {
|
||||
if (other.model != null && !other.model.isBlank()) {
|
||||
this.model = other.model;
|
||||
}
|
||||
if (other.createdAt != null && !other.createdAt.isBlank()) {
|
||||
this.createdAt = other.createdAt;
|
||||
}
|
||||
if (other.done != null) {
|
||||
this.done = other.done;
|
||||
}
|
||||
if (other.doneReason != null && !other.doneReason.isBlank()) {
|
||||
this.doneReason = other.doneReason;
|
||||
}
|
||||
if (other.totalDuration != null) {
|
||||
this.totalDuration = other.totalDuration;
|
||||
}
|
||||
if (other.loadDuration != null) {
|
||||
this.loadDuration = other.loadDuration;
|
||||
}
|
||||
if (other.promptEvalCount != null) {
|
||||
this.promptEvalCount = other.promptEvalCount;
|
||||
}
|
||||
if (other.promptEvalDuration != null) {
|
||||
this.promptEvalDuration = other.promptEvalDuration;
|
||||
}
|
||||
if (other.evalCount != null) {
|
||||
this.evalCount = other.evalCount;
|
||||
}
|
||||
if (other.evalDuration != null) {
|
||||
this.evalDuration = other.evalDuration;
|
||||
}
|
||||
if (other.error != null && !other.error.isBlank()) {
|
||||
this.error = other.error;
|
||||
}
|
||||
}
|
||||
}
|
||||
577
src/main/java/de/extio/lmlib/client/ollama/OllamaClient.java
Normal file
577
src/main/java/de/extio/lmlib/client/ollama/OllamaClient.java
Normal file
@@ -0,0 +1,577 @@
|
||||
package de.extio.lmlib.client.ollama;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestClientResponseException;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
|
||||
import de.extio.lmlib.client.Chunk;
|
||||
import de.extio.lmlib.client.Client;
|
||||
import de.extio.lmlib.client.Completion;
|
||||
import de.extio.lmlib.client.CompletionFinishReason;
|
||||
import de.extio.lmlib.client.CompletionStatistics;
|
||||
import de.extio.lmlib.client.Conversation;
|
||||
import de.extio.lmlib.client.Conversation.Turn;
|
||||
import de.extio.lmlib.client.ToolCall;
|
||||
import de.extio.lmlib.client.ToolCallData;
|
||||
import de.extio.lmlib.client.ToolDefinition;
|
||||
import de.extio.lmlib.client.oai.completion.FinishReasons;
|
||||
import de.extio.lmlib.profile.ModelCategory;
|
||||
import de.extio.lmlib.profile.ModelProfile;
|
||||
import de.extio.lmlib.profile.ModelProfile.ModelProvider;
|
||||
import de.extio.lmlib.profile.ModelProfileService;
|
||||
import de.extio.lmlib.token.TokenizerResolver;
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.databind.DeserializationFeature;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
public class OllamaClient implements Client {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(OllamaClient.class);
|
||||
|
||||
private static final OllamaDialect DEFAULT_OLLAMA_DIALECT = new OllamaDialect() {
|
||||
};
|
||||
|
||||
private final Map<String, List<String>> resolvedModelNames = new ConcurrentHashMap<>();
|
||||
|
||||
private final Map<String, OllamaShowResponse> resolvedModelDetails = new ConcurrentHashMap<>();
|
||||
|
||||
private final ObjectMapper objectMapper = JsonMapper.builder()
|
||||
.changeDefaultPropertyInclusion(incl -> incl.withValueInclusion(JsonInclude.Include.NON_NULL))
|
||||
.changeDefaultPropertyInclusion(incl -> incl.withContentInclusion(JsonInclude.Include.NON_NULL))
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
.build();
|
||||
|
||||
@Autowired
|
||||
@Qualifier("lmLibRestClientBuilder")
|
||||
private RestClient.Builder restClientBuilder;
|
||||
|
||||
@Autowired
|
||||
private TokenizerResolver tokenizerResolver;
|
||||
|
||||
@Autowired
|
||||
private ModelProfileService modelProfileService;
|
||||
|
||||
@Autowired(required = false)
|
||||
private OllamaDialect ollamaDialect;
|
||||
|
||||
@Override
|
||||
public Completion conversation(final ModelCategory modelCategory, final Conversation conversation, final ToolCallData toolCallData, final boolean skipCache) {
|
||||
return this.streamConversation(modelCategory, conversation, null, toolCallData, skipCache);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Completion conversation(final ModelProfile modelProfile, final Conversation conversation, final ToolCallData toolCallData, final boolean skipCache) {
|
||||
return this.streamConversation(modelProfile, conversation, null, toolCallData, skipCache);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Completion streamConversation(final ModelCategory modelCategory_, final Conversation conversation, final Consumer<Chunk> chunkConsumer, final ToolCallData toolCallData, final boolean skipCache) {
|
||||
final var modelCategory = modelCategory_ == null ? ModelCategory.MEDIUM : modelCategory_;
|
||||
final var modelProfile = this.modelProfileService.getModelProfile(modelCategory.getModelProfile(), modelCategory);
|
||||
return this.streamConversation(modelProfile, conversation, chunkConsumer, toolCallData, skipCache);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Completion streamConversation(final ModelProfile modelProfile, final Conversation conversation, final Consumer<Chunk> chunkConsumer, final ToolCallData toolCallData, final boolean skipCache) {
|
||||
if (modelProfile == null || modelProfile.modelProvider() != ModelProvider.OLLAMA) {
|
||||
throw new IllegalArgumentException("Invalid ModelProfile");
|
||||
}
|
||||
|
||||
final var request = this.createRequest(conversation, modelProfile, chunkConsumer != null, toolCallData);
|
||||
final String requestBody;
|
||||
try {
|
||||
requestBody = this.objectMapper.writeValueAsString(request);
|
||||
}
|
||||
catch (final JacksonException e) {
|
||||
throw new IllegalStateException("Cannot serialize request body", e);
|
||||
}
|
||||
|
||||
LOGGER.debug("Requesting Ollama chat at {}", modelProfile.url());
|
||||
final var start = LocalDateTime.now();
|
||||
final var restClient = this.restClientBuilder.baseUrl(modelProfile.url()).build();
|
||||
|
||||
var requestSpec = restClient
|
||||
.method(HttpMethod.POST)
|
||||
.uri("/api/chat")
|
||||
.header("Content-Type", "application/json");
|
||||
if (modelProfile.apiKey() != null && !modelProfile.apiKey().isBlank()) {
|
||||
requestSpec = requestSpec.header("Authorization", "Bearer " + modelProfile.apiKey());
|
||||
}
|
||||
|
||||
final var response = requestSpec
|
||||
.body(requestBody)
|
||||
.exchange((clientRequest, clientResponse) -> {
|
||||
if (clientResponse.getStatusCode().isError()) {
|
||||
this.logErrorResponse(clientResponse.getStatusCode(), clientResponse.getBody());
|
||||
throw new IllegalStateException("Error response from server: " + clientResponse.getStatusCode());
|
||||
}
|
||||
if (chunkConsumer == null) {
|
||||
final var ollamaResponse = this.objectMapper.readValue(clientResponse.getBody(), OllamaChatResponse.class);
|
||||
this.throwIfEmbeddedError(ollamaResponse);
|
||||
return ollamaResponse;
|
||||
}
|
||||
return this.readStream(clientResponse.getBody(), chunkConsumer);
|
||||
});
|
||||
|
||||
this.throwIfEmbeddedError(response);
|
||||
final var content = response.getMessage() != null ? response.getMessage().getContent() : null;
|
||||
final var reasoning = response.getMessage() != null ? response.getMessage().getThinking() : null;
|
||||
final var toolCalls = this.toToolCalls(response.getMessage() != null ? response.getMessage().getToolCalls() : List.of());
|
||||
final var finishReason = this.mapFinishReason(response.getDoneReason(), toolCalls);
|
||||
final var statistics = this.createCompletionStatistics(modelProfile, start, response, conversation, content, reasoning);
|
||||
return new Completion(content, reasoning, finishReason, statistics, toolCalls);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getModelNames(final ModelProfile modelProfile, final boolean forceReload) {
|
||||
if (modelProfile == null || modelProfile.modelProvider() != ModelProvider.OLLAMA) {
|
||||
return List.of();
|
||||
}
|
||||
if (modelProfile.modelName() != null && !modelProfile.modelName().isBlank()) {
|
||||
return List.of(modelProfile.modelName());
|
||||
}
|
||||
if (modelProfile.url() == null || modelProfile.url().isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
final var cacheKey = modelProfile.category() + "|" + modelProfile.url() + "|" + modelProfile.apiKey();
|
||||
if (forceReload) {
|
||||
final var modelNames = this.loadModelNames(modelProfile);
|
||||
this.resolvedModelNames.put(cacheKey, modelNames);
|
||||
return modelNames;
|
||||
}
|
||||
return this.resolvedModelNames.computeIfAbsent(cacheKey, key -> this.loadModelNames(modelProfile));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelProvider getModelProvider() {
|
||||
return ModelProvider.OLLAMA;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsToolCalling(final ModelCategory modelCategory) {
|
||||
final var modelProfile = this.modelProfileService.getModelProfile(modelCategory.getModelProfile(), modelCategory);
|
||||
return this.supportsToolCalling(modelProfile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsToolCalling(final ModelProfile modelProfile) {
|
||||
if (modelProfile == null || modelProfile.modelProvider() != ModelProvider.OLLAMA) {
|
||||
return false;
|
||||
}
|
||||
final var modelName = this.getPrimaryModelName(modelProfile);
|
||||
if (modelName == null || modelName.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
return this.supportsToolCapability(this.getModelDetails(modelProfile, modelName));
|
||||
}
|
||||
|
||||
private OllamaChatRequest createRequest(final Conversation conversation, final ModelProfile modelProfile, final boolean streaming, final ToolCallData toolCallData) {
|
||||
final var modelName = this.getPrimaryModelName(modelProfile);
|
||||
if (modelName == null || modelName.isBlank()) {
|
||||
throw new IllegalStateException("No model name resolved for Ollama profile " + modelProfile.category());
|
||||
}
|
||||
final var modelDetails = this.getModelDetails(modelProfile, modelName);
|
||||
final var request = new OllamaChatRequest();
|
||||
request.setModel(modelName);
|
||||
request.setMessages(this.createMessages(conversation));
|
||||
request.setStream(streaming);
|
||||
final var think = this.createThinkRequestValue(modelProfile, modelDetails);
|
||||
if (think != null) {
|
||||
request.setThink(think);
|
||||
}
|
||||
request.setOptions(this.createOptions(modelProfile));
|
||||
if (toolCallData != null && toolCallData.hasTools()) {
|
||||
if (this.supportsToolCapability(modelDetails)) {
|
||||
request.setTools(this.createTools(toolCallData.tools()));
|
||||
}
|
||||
else {
|
||||
LOGGER.debug("Ollama model {} does not advertise tool capability. Tools will be omitted from the request.", modelName);
|
||||
}
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
private Object createThinkRequestValue(final ModelProfile modelProfile, final OllamaShowResponse modelDetails) {
|
||||
final var thinkMode = this.getOllamaDialect().think(modelProfile);
|
||||
if (thinkMode == null || !thinkMode.isEnabled()) {
|
||||
return null;
|
||||
}
|
||||
if (!this.supportsThinkingCapability(modelDetails)) {
|
||||
LOGGER.debug("Ollama model {} does not advertise thinking capability. Think will be omitted from the request.", this.getPrimaryModelName(modelProfile));
|
||||
return null;
|
||||
}
|
||||
return thinkMode;
|
||||
}
|
||||
|
||||
private List<OllamaMessage> createMessages(final Conversation conversation) {
|
||||
final List<OllamaMessage> messages = new ArrayList<>();
|
||||
final Map<String, String> toolCallNamesById = new LinkedHashMap<>();
|
||||
for (final Turn turn : conversation.getConversation()) {
|
||||
final var message = new OllamaMessage();
|
||||
message.setRole(this.mapRole(turn));
|
||||
message.setContent(turn.text() != null ? turn.text() : "");
|
||||
if (!turn.toolCalls().isEmpty()) {
|
||||
message.setToolCalls(turn.toolCalls().stream().map(toolCall -> {
|
||||
final var function = new OllamaFunctionCall();
|
||||
function.setName(toolCall.name());
|
||||
function.setArguments(this.parseJsonOrText(toolCall.arguments()));
|
||||
final var ollamaToolCall = new OllamaToolCall();
|
||||
ollamaToolCall.setId(toolCall.id());
|
||||
ollamaToolCall.setType(toolCall.type());
|
||||
ollamaToolCall.setFunction(function);
|
||||
if (toolCall.id() != null && !toolCall.id().isBlank() && toolCall.name() != null && !toolCall.name().isBlank()) {
|
||||
toolCallNamesById.put(toolCall.id(), toolCall.name());
|
||||
}
|
||||
return ollamaToolCall;
|
||||
}).toList());
|
||||
}
|
||||
message.setToolCallId(turn.toolCallId());
|
||||
if (turn.type() == Conversation.TurnType.TOOL && turn.toolCallId() != null && !turn.toolCallId().isBlank()) {
|
||||
message.setToolName(toolCallNamesById.get(turn.toolCallId()));
|
||||
}
|
||||
messages.add(message);
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
private String mapRole(final Turn turn) {
|
||||
return switch (turn.type()) {
|
||||
case SYSTEM -> "system";
|
||||
case USER -> "user";
|
||||
case ASSISTANT -> "assistant";
|
||||
case TOOL -> "tool";
|
||||
};
|
||||
}
|
||||
|
||||
private Map<String, Object> createOptions(final ModelProfile modelProfile) {
|
||||
final var options = new LinkedHashMap<String, Object>();
|
||||
options.put("num_predict", modelProfile.maxTokens());
|
||||
options.put("temperature", modelProfile.temperature());
|
||||
options.put("top_p", modelProfile.topP());
|
||||
return options;
|
||||
}
|
||||
|
||||
private List<OllamaToolDefinition> createTools(final List<ToolDefinition> toolDefinitions) {
|
||||
return toolDefinitions.stream().map(toolDefinition -> {
|
||||
final var function = new OllamaToolFunction();
|
||||
function.setName(toolDefinition.name());
|
||||
function.setDescription(toolDefinition.description());
|
||||
function.setParameters(toolDefinition.parameters());
|
||||
final var tool = new OllamaToolDefinition();
|
||||
tool.setType("function");
|
||||
tool.setFunction(function);
|
||||
return tool;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
private OllamaChatResponse readStream(final InputStream inputStream, final Consumer<Chunk> chunkConsumer) throws IOException {
|
||||
final var response = new OllamaChatResponse();
|
||||
final var contentBuilder = new StringBuilder();
|
||||
final var reasoningBuilder = new StringBuilder();
|
||||
final List<ToolCall> toolCalls = new ArrayList<>();
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
final var streamResponse = this.objectMapper.readValue(line, OllamaChatResponse.class);
|
||||
this.throwIfEmbeddedError(streamResponse);
|
||||
response.copyFrom(streamResponse);
|
||||
if (streamResponse.getMessage() != null) {
|
||||
String content = null;
|
||||
String reasoning = null;
|
||||
if (streamResponse.getMessage().getContent() != null && !streamResponse.getMessage().getContent().isEmpty()) {
|
||||
content = streamResponse.getMessage().getContent();
|
||||
contentBuilder.append(content);
|
||||
}
|
||||
if (streamResponse.getMessage().getThinking() != null && !streamResponse.getMessage().getThinking().isEmpty()) {
|
||||
reasoning = streamResponse.getMessage().getThinking();
|
||||
reasoningBuilder.append(reasoning);
|
||||
}
|
||||
if (content != null || reasoning != null) {
|
||||
chunkConsumer.accept(new Chunk(content, reasoning));
|
||||
}
|
||||
if (streamResponse.getMessage().getToolCalls() != null && !streamResponse.getMessage().getToolCalls().isEmpty()) {
|
||||
toolCalls.clear();
|
||||
toolCalls.addAll(this.toToolCalls(streamResponse.getMessage().getToolCalls()));
|
||||
}
|
||||
}
|
||||
if (Boolean.TRUE.equals(streamResponse.getDone())) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
final var message = new OllamaMessage();
|
||||
message.setRole("assistant");
|
||||
message.setContent(contentBuilder.toString());
|
||||
message.setThinking(reasoningBuilder.toString());
|
||||
message.setToolCalls(this.toOllamaToolCalls(toolCalls));
|
||||
response.setMessage(message);
|
||||
return response;
|
||||
}
|
||||
|
||||
private List<ToolCall> toToolCalls(final List<OllamaToolCall> ollamaToolCalls) {
|
||||
if (ollamaToolCalls == null || ollamaToolCalls.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final List<ToolCall> toolCalls = new ArrayList<>();
|
||||
for (int i = 0; i < ollamaToolCalls.size(); i++) {
|
||||
final var toolCall = ollamaToolCalls.get(i);
|
||||
if (toolCall == null || toolCall.getFunction() == null || toolCall.getFunction().getName() == null || toolCall.getFunction().getName().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
toolCalls.add(new ToolCall(
|
||||
toolCall.getId() != null && !toolCall.getId().isBlank() ? toolCall.getId() : "ollama-tool-call-" + (i + 1),
|
||||
toolCall.getType() != null && !toolCall.getType().isBlank() ? toolCall.getType() : "function",
|
||||
toolCall.getFunction().getName(),
|
||||
this.serializeArguments(toolCall.getFunction().getArguments())));
|
||||
}
|
||||
return List.copyOf(toolCalls);
|
||||
}
|
||||
|
||||
private List<OllamaToolCall> toOllamaToolCalls(final List<ToolCall> toolCalls) {
|
||||
return toolCalls.stream().map(toolCall -> {
|
||||
final var function = new OllamaFunctionCall();
|
||||
function.setName(toolCall.name());
|
||||
function.setArguments(this.parseJsonOrText(toolCall.arguments()));
|
||||
final var ollamaToolCall = new OllamaToolCall();
|
||||
ollamaToolCall.setId(toolCall.id());
|
||||
ollamaToolCall.setType(toolCall.type());
|
||||
ollamaToolCall.setFunction(function);
|
||||
return ollamaToolCall;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
private CompletionFinishReason mapFinishReason(final String finishReason, final List<ToolCall> toolCalls) {
|
||||
if (toolCalls != null && !toolCalls.isEmpty()) {
|
||||
return CompletionFinishReason.TOOL_CALLS;
|
||||
}
|
||||
return switch (finishReason) {
|
||||
case FinishReasons.FINISH_REASON_LENGTH -> CompletionFinishReason.TOKEN_LIMIT_REACHED;
|
||||
case FinishReasons.FINISH_REASON_CONTENT_FILTER -> CompletionFinishReason.CONTENT_FILTERED;
|
||||
case FinishReasons.FINISH_REASON_TOOL_CALLS -> CompletionFinishReason.TOOL_CALLS;
|
||||
default -> CompletionFinishReason.DONE;
|
||||
};
|
||||
}
|
||||
|
||||
private CompletionStatistics createCompletionStatistics(final ModelProfile modelProfile, final LocalDateTime start, final OllamaChatResponse response, final Conversation conversation, final String content, final String reasoning) {
|
||||
int inTokens = 0;
|
||||
int outTokens = 0;
|
||||
int reasoningOutTokens = 0;
|
||||
if (response.getPromptEvalCount() != null && response.getPromptEvalCount() > 0) {
|
||||
inTokens = response.getPromptEvalCount();
|
||||
}
|
||||
if (response.getEvalCount() != null && response.getEvalCount() > 0) {
|
||||
outTokens = response.getEvalCount();
|
||||
}
|
||||
if (reasoning != null && !reasoning.isBlank()) {
|
||||
reasoningOutTokens = this.tokenizerResolver.count(reasoning, modelProfile);
|
||||
outTokens = Math.max(0, outTokens - reasoningOutTokens);
|
||||
}
|
||||
if (inTokens <= 0) {
|
||||
inTokens = this.tokenizerResolver.count(this.createPromptForCounting(conversation), modelProfile);
|
||||
}
|
||||
if (outTokens <= 0 && content != null && !content.isBlank()) {
|
||||
outTokens = this.tokenizerResolver.count(content, modelProfile);
|
||||
}
|
||||
final var duration = response.getTotalDuration() != null && response.getTotalDuration() > 0
|
||||
? Duration.ofNanos(response.getTotalDuration())
|
||||
: Duration.between(start, LocalDateTime.now());
|
||||
var cost = BigDecimal.ZERO;
|
||||
cost = cost.add(BigDecimal.valueOf(inTokens).multiply(modelProfile.costPerInToken()));
|
||||
cost = cost.add(BigDecimal.valueOf(outTokens).multiply(modelProfile.costPerOutToken()));
|
||||
cost = cost.add(BigDecimal.valueOf(reasoningOutTokens).multiply(modelProfile.costPerReasoningOutToken()));
|
||||
return new CompletionStatistics(1, duration, inTokens, 0, outTokens, reasoningOutTokens, cost, false);
|
||||
}
|
||||
|
||||
private String createPromptForCounting(final Conversation conversation) {
|
||||
final var builder = new StringBuilder();
|
||||
for (final var turn : conversation.getConversation()) {
|
||||
builder.append(turn.type().name());
|
||||
builder.append(':');
|
||||
builder.append(turn.text() != null ? turn.text() : "");
|
||||
builder.append('\n');
|
||||
for (final var toolCall : turn.toolCalls()) {
|
||||
builder.append(toolCall.name());
|
||||
builder.append(':');
|
||||
builder.append(toolCall.arguments());
|
||||
builder.append('\n');
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String getPrimaryModelName(final ModelProfile modelProfile) {
|
||||
return this.getModelNames(modelProfile, false).stream().findFirst().orElse("");
|
||||
}
|
||||
|
||||
private List<String> loadModelNames(final ModelProfile modelProfile) {
|
||||
try {
|
||||
var requestSpec = this.restClientBuilder.baseUrl(modelProfile.url()).build()
|
||||
.method(HttpMethod.GET)
|
||||
.uri("/api/tags");
|
||||
if (modelProfile.apiKey() != null && !modelProfile.apiKey().isBlank()) {
|
||||
requestSpec = requestSpec.header("Authorization", "Bearer " + modelProfile.apiKey());
|
||||
}
|
||||
final var response = requestSpec.retrieve().body(OllamaTagsResponse.class);
|
||||
return response == null || response.getModels() == null ? List.of()
|
||||
: response.getModels().stream()
|
||||
.map(model -> model.getName() != null && !model.getName().isBlank() ? model.getName() : model.getModel())
|
||||
.filter(Objects::nonNull)
|
||||
.filter(modelName -> !modelName.isBlank())
|
||||
.toList();
|
||||
}
|
||||
catch (final RestClientResponseException e) {
|
||||
LOGGER.warn("Failed to load Ollama model names: {} {}", e.getStatusCode(), e.getStatusText());
|
||||
return List.of();
|
||||
}
|
||||
catch (final RestClientException e) {
|
||||
LOGGER.warn("Failed to load Ollama model names: {}", e.getMessage());
|
||||
return List.of();
|
||||
}
|
||||
catch (final RuntimeException e) {
|
||||
LOGGER.warn("Failed to load Ollama model names", e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private OllamaShowResponse getModelDetails(final ModelProfile modelProfile, final String modelName) {
|
||||
final var cacheKey = this.cacheKey(modelProfile, modelName);
|
||||
return this.resolvedModelDetails.computeIfAbsent(cacheKey, key -> this.loadModelDetails(modelProfile, modelName));
|
||||
}
|
||||
|
||||
private OllamaShowResponse loadModelDetails(final ModelProfile modelProfile, final String modelName) {
|
||||
try {
|
||||
final var request = new OllamaShowRequest();
|
||||
request.setModel(modelName);
|
||||
request.setVerbose(false);
|
||||
var requestSpec = this.restClientBuilder.baseUrl(modelProfile.url()).build()
|
||||
.method(HttpMethod.POST)
|
||||
.uri("/api/show")
|
||||
.header("Content-Type", "application/json");
|
||||
if (modelProfile.apiKey() != null && !modelProfile.apiKey().isBlank()) {
|
||||
requestSpec = requestSpec.header("Authorization", "Bearer " + modelProfile.apiKey());
|
||||
}
|
||||
final var requestBody = this.objectMapper.writeValueAsString(request);
|
||||
final var response = requestSpec.body(requestBody).retrieve().body(OllamaShowResponse.class);
|
||||
return response == null ? new OllamaShowResponse() : response;
|
||||
}
|
||||
catch (final RestClientResponseException e) {
|
||||
LOGGER.warn("Failed to load Ollama model details for {}: {} {}", modelName, e.getStatusCode(), e.getStatusText());
|
||||
return new OllamaShowResponse();
|
||||
}
|
||||
catch (final RestClientException e) {
|
||||
LOGGER.warn("Failed to load Ollama model details for {}: {}", modelName, e.getMessage());
|
||||
return new OllamaShowResponse();
|
||||
}
|
||||
catch (final JacksonException e) {
|
||||
LOGGER.warn("Failed to serialize Ollama show request for {}", modelName, e);
|
||||
return new OllamaShowResponse();
|
||||
}
|
||||
catch (final RuntimeException e) {
|
||||
LOGGER.warn("Failed to load Ollama model details for {}", modelName, e);
|
||||
return new OllamaShowResponse();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean supportsThinkingCapability(final OllamaShowResponse modelDetails) {
|
||||
return this.hasCapability(modelDetails, "thinking");
|
||||
}
|
||||
|
||||
private boolean supportsToolCapability(final OllamaShowResponse modelDetails) {
|
||||
return this.hasCapability(modelDetails, "tools")
|
||||
|| this.hasCapability(modelDetails, "tool")
|
||||
|| this.hasCapability(modelDetails, "tool-calling")
|
||||
|| this.hasCapability(modelDetails, "tool_calling");
|
||||
}
|
||||
|
||||
private boolean hasCapability(final OllamaShowResponse modelDetails, final String expectedCapability) {
|
||||
if (modelDetails == null || modelDetails.getCapabilities() == null || modelDetails.getCapabilities().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return modelDetails.getCapabilities().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(capability -> capability.toLowerCase(Locale.ROOT).trim())
|
||||
.anyMatch(capability -> capability.equals(expectedCapability) || capability.contains(expectedCapability));
|
||||
}
|
||||
|
||||
private String cacheKey(final ModelProfile modelProfile, final String modelName) {
|
||||
return modelProfile.category() + "|" + modelProfile.url() + "|" + modelProfile.apiKey() + "|" + modelName;
|
||||
}
|
||||
|
||||
private OllamaDialect getOllamaDialect() {
|
||||
return this.ollamaDialect == null ? DEFAULT_OLLAMA_DIALECT : this.ollamaDialect;
|
||||
}
|
||||
|
||||
private void throwIfEmbeddedError(final OllamaChatResponse response) {
|
||||
if (response != null && response.getError() != null && !response.getError().isBlank()) {
|
||||
LOGGER.warn("Error response from Ollama: {}", response.getError());
|
||||
throw new IllegalStateException(response.getError());
|
||||
}
|
||||
}
|
||||
|
||||
private void logErrorResponse(final HttpStatusCode statusCode, final InputStream responseBody) {
|
||||
try {
|
||||
LOGGER.warn("Error response from server: {} {}", statusCode, new String(responseBody.readAllBytes(), StandardCharsets.UTF_8));
|
||||
}
|
||||
catch (final IOException e) {
|
||||
LOGGER.warn("Error response from server: {} <failed to read response body: {}>", statusCode, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private Object parseJsonOrText(final String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return value;
|
||||
}
|
||||
try {
|
||||
return this.objectMapper.readValue(value, Object.class);
|
||||
}
|
||||
catch (final Exception e) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
private String serializeArguments(final Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof final String string) {
|
||||
return string;
|
||||
}
|
||||
try {
|
||||
return this.objectMapper.writeValueAsString(value);
|
||||
}
|
||||
catch (final JacksonException e) {
|
||||
throw new IllegalStateException("Cannot serialize tool call arguments", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package de.extio.lmlib.client.ollama;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
import de.extio.lmlib.profile.ModelProfile;
|
||||
|
||||
public interface OllamaDialect {
|
||||
|
||||
default ThinkMode think(final ModelProfile modelProfile) {
|
||||
return ThinkMode.ENABLED;
|
||||
}
|
||||
|
||||
enum ThinkMode {
|
||||
DISABLED(Boolean.FALSE),
|
||||
ENABLED(Boolean.TRUE),
|
||||
LOW("low"),
|
||||
MEDIUM("medium"),
|
||||
HIGH("high");
|
||||
|
||||
private final Object wireValue;
|
||||
|
||||
ThinkMode(final Object wireValue) {
|
||||
this.wireValue = wireValue;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return (this.wireValue instanceof final Boolean val && val) || (this.wireValue instanceof final String str && !str.isBlank());
|
||||
}
|
||||
|
||||
@JsonValue
|
||||
public Object getWireValue() {
|
||||
return this.wireValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package de.extio.lmlib.client.ollama;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
final class OllamaFunctionCall {
|
||||
|
||||
private String name;
|
||||
|
||||
private String description;
|
||||
|
||||
private Object arguments;
|
||||
|
||||
@JsonProperty("name")
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@JsonProperty("description")
|
||||
public String getDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
public void setDescription(final String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
@JsonProperty("arguments")
|
||||
public Object getArguments() {
|
||||
return this.arguments;
|
||||
}
|
||||
|
||||
public void setArguments(final Object arguments) {
|
||||
this.arguments = arguments;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package de.extio.lmlib.client.ollama;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
final class OllamaListedModel {
|
||||
|
||||
private String name;
|
||||
|
||||
private String model;
|
||||
|
||||
@JsonProperty("name")
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@JsonProperty("model")
|
||||
public String getModel() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
public void setModel(final String model) {
|
||||
this.model = model;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package de.extio.lmlib.client.ollama;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
final class OllamaMessage {
|
||||
|
||||
private String role;
|
||||
|
||||
private String content;
|
||||
|
||||
private String thinking;
|
||||
|
||||
private List<OllamaToolCall> toolCalls;
|
||||
|
||||
private String toolCallId;
|
||||
|
||||
private String toolName;
|
||||
|
||||
@JsonProperty("role")
|
||||
public String getRole() {
|
||||
return this.role;
|
||||
}
|
||||
|
||||
public void setRole(final String role) {
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
@JsonProperty("content")
|
||||
public String getContent() {
|
||||
return this.content;
|
||||
}
|
||||
|
||||
public void setContent(final String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
@JsonProperty("thinking")
|
||||
public String getThinking() {
|
||||
return this.thinking;
|
||||
}
|
||||
|
||||
public void setThinking(final String thinking) {
|
||||
this.thinking = thinking;
|
||||
}
|
||||
|
||||
@JsonProperty("tool_calls")
|
||||
public List<OllamaToolCall> getToolCalls() {
|
||||
return this.toolCalls;
|
||||
}
|
||||
|
||||
public void setToolCalls(final List<OllamaToolCall> toolCalls) {
|
||||
this.toolCalls = toolCalls;
|
||||
}
|
||||
|
||||
@JsonProperty("tool_call_id")
|
||||
public String getToolCallId() {
|
||||
return this.toolCallId;
|
||||
}
|
||||
|
||||
public void setToolCallId(final String toolCallId) {
|
||||
this.toolCallId = toolCallId;
|
||||
}
|
||||
|
||||
@JsonProperty("tool_name")
|
||||
public String getToolName() {
|
||||
return this.toolName;
|
||||
}
|
||||
|
||||
public void setToolName(final String toolName) {
|
||||
this.toolName = toolName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package de.extio.lmlib.client.ollama;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
final class OllamaShowRequest {
|
||||
|
||||
private String model;
|
||||
|
||||
private Boolean verbose;
|
||||
|
||||
@JsonProperty("model")
|
||||
public String getModel() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
public void setModel(final String model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
@JsonProperty("verbose")
|
||||
public Boolean getVerbose() {
|
||||
return this.verbose;
|
||||
}
|
||||
|
||||
public void setVerbose(final Boolean verbose) {
|
||||
this.verbose = verbose;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package de.extio.lmlib.client.ollama;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
final class OllamaShowResponse {
|
||||
|
||||
private List<String> capabilities;
|
||||
|
||||
@JsonProperty("capabilities")
|
||||
public List<String> getCapabilities() {
|
||||
return this.capabilities;
|
||||
}
|
||||
|
||||
public void setCapabilities(final List<String> capabilities) {
|
||||
this.capabilities = capabilities;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package de.extio.lmlib.client.ollama;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
final class OllamaTagsResponse {
|
||||
|
||||
private List<OllamaListedModel> models;
|
||||
|
||||
@JsonProperty("models")
|
||||
public List<OllamaListedModel> getModels() {
|
||||
return this.models;
|
||||
}
|
||||
|
||||
public void setModels(final List<OllamaListedModel> models) {
|
||||
this.models = models;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package de.extio.lmlib.client.ollama;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
final class OllamaToolCall {
|
||||
|
||||
private String id;
|
||||
|
||||
private String type;
|
||||
|
||||
private OllamaFunctionCall function;
|
||||
|
||||
@JsonProperty("id")
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public void setId(final String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@JsonProperty("type")
|
||||
public String getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
public void setType(final String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@JsonProperty("function")
|
||||
public OllamaFunctionCall getFunction() {
|
||||
return this.function;
|
||||
}
|
||||
|
||||
public void setFunction(final OllamaFunctionCall function) {
|
||||
this.function = function;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package de.extio.lmlib.client.ollama;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
final class OllamaToolDefinition {
|
||||
|
||||
private String type;
|
||||
|
||||
private OllamaToolFunction function;
|
||||
|
||||
@JsonProperty("type")
|
||||
public String getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
public void setType(final String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@JsonProperty("function")
|
||||
public OllamaToolFunction getFunction() {
|
||||
return this.function;
|
||||
}
|
||||
|
||||
public void setFunction(final OllamaToolFunction function) {
|
||||
this.function = function;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package de.extio.lmlib.client.ollama;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
final class OllamaToolFunction {
|
||||
|
||||
private String name;
|
||||
|
||||
private String description;
|
||||
|
||||
private Map<String, Object> parameters;
|
||||
|
||||
@JsonProperty("name")
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@JsonProperty("description")
|
||||
public String getDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
public void setDescription(final String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
@JsonProperty("parameters")
|
||||
public Map<String, Object> getParameters() {
|
||||
return this.parameters;
|
||||
}
|
||||
|
||||
public void setParameters(final Map<String, Object> parameters) {
|
||||
this.parameters = parameters;
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,6 @@ public record ModelProfile(
|
||||
}
|
||||
|
||||
public static enum ModelProvider {
|
||||
OAI_TEXT_COMPLETION, OAI_CHAT_COMPLETION;
|
||||
OAI_TEXT_COMPLETION, OAI_CHAT_COMPLETION, OLLAMA;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user