mirror of
https://github.com/danieldemus/openhab-addons
synced 2026-07-29 12:34:21 +02:00
[gemini] Fix HTTP 400 when using tool calling with thinking models (#21097)
* [gemini] Fix HTTP 400 when using tool calling with thinking models Signed-off-by: Florian Hotze <dev@florianhotze.com>
This commit is contained in:
+25
-13
@@ -51,7 +51,6 @@ import org.openhab.binding.gemini.internal.api.dto.response.GeminiModelsResponse
|
|||||||
import org.openhab.binding.gemini.internal.api.dto.response.GeminiResponse;
|
import org.openhab.binding.gemini.internal.api.dto.response.GeminiResponse;
|
||||||
import org.openhab.core.voice.text.conversation.Conversation;
|
import org.openhab.core.voice.text.conversation.Conversation;
|
||||||
import org.openhab.core.voice.text.interpreter.llm.LLMTool;
|
import org.openhab.core.voice.text.interpreter.llm.LLMTool;
|
||||||
import org.openhab.core.voice.text.interpreter.llm.LLMToolCall;
|
|
||||||
import org.openhab.core.voice.text.interpreter.llm.LLMToolParam;
|
import org.openhab.core.voice.text.interpreter.llm.LLMToolParam;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -105,7 +104,7 @@ public class GeminiApiClient {
|
|||||||
GeminiContent systemInstruction = createSystemInstruction(systemMessage);
|
GeminiContent systemInstruction = createSystemInstruction(systemMessage);
|
||||||
|
|
||||||
// Contents
|
// Contents
|
||||||
GeminiPart userPart = new GeminiPart(prompt, null, null, null);
|
GeminiPart userPart = new GeminiPart(prompt, null, null, null, null);
|
||||||
GeminiContent userContent = new GeminiContent(ROLE_USER, List.of(userPart));
|
GeminiContent userContent = new GeminiContent(ROLE_USER, List.of(userPart));
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
@@ -141,21 +140,21 @@ public class GeminiApiClient {
|
|||||||
for (Conversation.Message msg : history) {
|
for (Conversation.Message msg : history) {
|
||||||
switch (msg.role()) {
|
switch (msg.role()) {
|
||||||
case USER: {
|
case USER: {
|
||||||
GeminiPart part = new GeminiPart(msg.content(), null, null, null);
|
GeminiPart part = new GeminiPart(msg.content(), null, null, null, null);
|
||||||
contents.add(new GeminiContent(ROLE_USER, List.of(part)));
|
contents.add(new GeminiContent(ROLE_USER, new ArrayList<>(List.of(part))));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case OPENHAB: {
|
case OPENHAB: {
|
||||||
GeminiPart part = new GeminiPart(msg.content(), null, null, null);
|
GeminiPart part = new GeminiPart(msg.content(), null, null, null, null);
|
||||||
contents.add(new GeminiContent(ROLE_MODEL, List.of(part)));
|
contents.add(new GeminiContent(ROLE_MODEL, new ArrayList<>(List.of(part))));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TOOL_CALL: {
|
case TOOL_CALL: {
|
||||||
LLMToolCall toolCall = LLMToolCall.fromJson(msg.content());
|
GeminiLLMToolCall toolCall = GeminiLLMToolCall.fromJson(msg.content());
|
||||||
String name = toolCall.tool().replaceAll("[^a-zA-Z0-9_-]", "_");
|
String name = toolCall.tool.replaceAll("[^a-zA-Z0-9_-]", "_");
|
||||||
pendingToolCallNames.add(name);
|
pendingToolCallNames.add(name);
|
||||||
GeminiFunctionCall fc = new GeminiFunctionCall(name, toolCall.params());
|
GeminiFunctionCall fc = new GeminiFunctionCall(name, toolCall.params, toolCall.id);
|
||||||
GeminiPart part = new GeminiPart(null, fc, null, null);
|
GeminiPart part = new GeminiPart(null, fc, null, null, toolCall.thoughtSignature);
|
||||||
contents.add(new GeminiContent(ROLE_MODEL, List.of(part)));
|
contents.add(new GeminiContent(ROLE_MODEL, List.of(part)));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -166,8 +165,21 @@ public class GeminiApiClient {
|
|||||||
break; // TOOL_RETURN without preceding TOOL_CALL - ignore
|
break; // TOOL_RETURN without preceding TOOL_CALL - ignore
|
||||||
}
|
}
|
||||||
GeminiFunctionResponse fr = new GeminiFunctionResponse(name, Map.of("result", msg.content()));
|
GeminiFunctionResponse fr = new GeminiFunctionResponse(name, Map.of("result", msg.content()));
|
||||||
GeminiPart part = new GeminiPart(null, null, fr, null);
|
GeminiPart part = new GeminiPart(null, null, fr, null, null);
|
||||||
contents.add(new GeminiContent(ROLE_USER, List.of(part)));
|
|
||||||
|
// Consolidate consecutive TOOL_RETURNs into the same ROLE_USER content
|
||||||
|
if (!contents.isEmpty() && ROLE_USER.equals(contents.getLast().role())) {
|
||||||
|
GeminiContent lastContent = contents.getLast();
|
||||||
|
List<GeminiPart> lastParts = lastContent.parts();
|
||||||
|
if (lastParts != null && !lastParts.isEmpty()
|
||||||
|
&& lastParts.getFirst().functionResponse() != null) {
|
||||||
|
lastParts.add(part);
|
||||||
|
} else {
|
||||||
|
contents.add(new GeminiContent(ROLE_USER, new ArrayList<>(List.of(part))));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
contents.add(new GeminiContent(ROLE_USER, new ArrayList<>(List.of(part))));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case THINKING:
|
case THINKING:
|
||||||
@@ -356,7 +368,7 @@ public class GeminiApiClient {
|
|||||||
|
|
||||||
private @Nullable GeminiContent createSystemInstruction(@Nullable String systemMessage) {
|
private @Nullable GeminiContent createSystemInstruction(@Nullable String systemMessage) {
|
||||||
if (systemMessage != null && !systemMessage.isBlank()) {
|
if (systemMessage != null && !systemMessage.isBlank()) {
|
||||||
GeminiPart sysPart = new GeminiPart(systemMessage, null, null, null);
|
GeminiPart sysPart = new GeminiPart(systemMessage, null, null, null, null);
|
||||||
return new GeminiContent(null, List.of(sysPart));
|
return new GeminiContent(null, List.of(sysPart));
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2026 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.gemini.internal.api;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.openhab.core.voice.text.interpreter.llm.LLMToolCall;
|
||||||
|
|
||||||
|
import com.google.gson.JsonSyntaxException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A DTO to store information about a Gemini tool call.
|
||||||
|
* Extends {@link LLMToolCall} with additional fields required by the Gemini API.
|
||||||
|
*
|
||||||
|
* @author Florian Hotze - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class GeminiLLMToolCall extends LLMToolCall {
|
||||||
|
public final @Nullable String id;
|
||||||
|
public final @Nullable String thoughtSignature;
|
||||||
|
|
||||||
|
public GeminiLLMToolCall(String tool, Map<String, Object> params, @Nullable String id,
|
||||||
|
@Nullable String thoughtSignature) {
|
||||||
|
super(tool, params);
|
||||||
|
this.id = id;
|
||||||
|
this.thoughtSignature = thoughtSignature;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GeminiLLMToolCall fromJson(String json) throws JsonSyntaxException {
|
||||||
|
GeminiLLMToolCall call = GSON.fromJson(json, GeminiLLMToolCall.class);
|
||||||
|
if (call == null) {
|
||||||
|
throw new JsonSyntaxException("Deserialized GeminiLLMToolCall is null.");
|
||||||
|
}
|
||||||
|
if (call.tool == null || call.params == null) {
|
||||||
|
throw new JsonSyntaxException("Deserialized GeminiLLMToolCall has null tool or params.");
|
||||||
|
}
|
||||||
|
return call;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -27,5 +27,5 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
|||||||
*/
|
*/
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public record GeminiFunctionCall(@Nullable String name, @Nullable Map<String, Object> args) {
|
public record GeminiFunctionCall(@Nullable String name, @Nullable Map<String, Object> args, @Nullable String id) {
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -27,5 +27,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
|||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public record GeminiPart(@Nullable String text, @Nullable GeminiFunctionCall functionCall,
|
public record GeminiPart(@Nullable String text, @Nullable GeminiFunctionCall functionCall,
|
||||||
@Nullable GeminiFunctionResponse functionResponse, @Nullable Boolean thought) {
|
@Nullable GeminiFunctionResponse functionResponse, @Nullable Boolean thought,
|
||||||
|
@Nullable String thoughtSignature) {
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -28,6 +28,7 @@ import org.openhab.binding.gemini.internal.GeminiConfiguration;
|
|||||||
import org.openhab.binding.gemini.internal.GeminiHandler;
|
import org.openhab.binding.gemini.internal.GeminiHandler;
|
||||||
import org.openhab.binding.gemini.internal.api.GeminiApiClient;
|
import org.openhab.binding.gemini.internal.api.GeminiApiClient;
|
||||||
import org.openhab.binding.gemini.internal.api.GeminiApiException;
|
import org.openhab.binding.gemini.internal.api.GeminiApiException;
|
||||||
|
import org.openhab.binding.gemini.internal.api.GeminiLLMToolCall;
|
||||||
import org.openhab.binding.gemini.internal.api.dto.GeminiContent;
|
import org.openhab.binding.gemini.internal.api.dto.GeminiContent;
|
||||||
import org.openhab.binding.gemini.internal.api.dto.GeminiFunctionCall;
|
import org.openhab.binding.gemini.internal.api.dto.GeminiFunctionCall;
|
||||||
import org.openhab.binding.gemini.internal.api.dto.GeminiPart;
|
import org.openhab.binding.gemini.internal.api.dto.GeminiPart;
|
||||||
@@ -43,7 +44,6 @@ import org.openhab.core.voice.text.conversation.Conversation;
|
|||||||
import org.openhab.core.voice.text.conversation.ConversationException;
|
import org.openhab.core.voice.text.conversation.ConversationException;
|
||||||
import org.openhab.core.voice.text.conversation.ConversationRole;
|
import org.openhab.core.voice.text.conversation.ConversationRole;
|
||||||
import org.openhab.core.voice.text.interpreter.llm.LLMTool;
|
import org.openhab.core.voice.text.interpreter.llm.LLMTool;
|
||||||
import org.openhab.core.voice.text.interpreter.llm.LLMToolCall;
|
|
||||||
import org.openhab.core.voice.text.interpreter.llm.LLMToolException;
|
import org.openhab.core.voice.text.interpreter.llm.LLMToolException;
|
||||||
import org.osgi.framework.Bundle;
|
import org.osgi.framework.Bundle;
|
||||||
import org.osgi.framework.BundleContext;
|
import org.osgi.framework.BundleContext;
|
||||||
@@ -252,8 +252,8 @@ public class GeminiHLIService implements ThingHandlerService, HumanLanguageInter
|
|||||||
String toolName = fc.name();
|
String toolName = fc.name();
|
||||||
Map<String, Object> args = fc.args();
|
Map<String, Object> args = fc.args();
|
||||||
|
|
||||||
LLMToolCall llmToolCall = new LLMToolCall(toolName != null ? toolName : "",
|
GeminiLLMToolCall llmToolCall = new GeminiLLMToolCall(toolName != null ? toolName : "",
|
||||||
args != null ? args : new HashMap<>());
|
args != null ? args : new HashMap<>(), fc.id(), part.thoughtSignature());
|
||||||
try {
|
try {
|
||||||
conversation.addMessage(ConversationRole.TOOL_CALL, llmToolCall.toJson());
|
conversation.addMessage(ConversationRole.TOOL_CALL, llmToolCall.toJson());
|
||||||
} catch (ConversationException e) {
|
} catch (ConversationException e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user