Skip to content

Commit 2f543ab

Browse files
google-genai-botcopybara-github
authored andcommitted
feat: Add thought signature support for chat completions
- https://ai.google.dev/gemini-api/docs/thought-signatures - Also adds logging that's similar to the GenAI flow - Also fixes some issues that appeared when testing streaming - Final tool call no longer returns the accumulated text - Add thought signature support PiperOrigin-RevId: 919115462
1 parent 9a06dd3 commit 2f543ab

4 files changed

Lines changed: 225 additions & 66 deletions

File tree

core/src/main/java/com/google/adk/models/chat/ChatCompletionsHttpClient.java

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -190,14 +190,21 @@ private static Duration resolveCallTimeout(HttpOptions httpOptions) {
190190
public Flowable<LlmResponse> complete(LlmRequest llmRequest, boolean stream) {
191191
return Flowable.defer(
192192
() -> {
193+
String effectiveModelName = llmRequest.model().orElse("?");
194+
logger.trace("Chat Completion Request Contents: {}", llmRequest.contents());
195+
llmRequest.config().ifPresent(c -> logger.trace("Chat Completion Request Config: {}", c));
196+
193197
ChatCompletionsRequest dtoRequest =
194198
ChatCompletionsRequest.fromLlmRequest(llmRequest, stream);
195199
String jsonPayload = objectMapper.writeValueAsString(dtoRequest);
196-
logger.trace(
197-
"Chat Completion Request: model={}, stream={}, messagesCount={}",
198-
dtoRequest.model,
199-
dtoRequest.stream,
200-
dtoRequest.messages != null ? dtoRequest.messages.size() : 0);
200+
logger.trace("Chat Completion Request JSON: {}", jsonPayload);
201+
202+
if (stream) {
203+
logger.debug(
204+
"Sending streaming chat-completion request to model {}", effectiveModelName);
205+
} else {
206+
logger.debug("Sending chat-completion request to model {}", effectiveModelName);
207+
}
201208

202209
Request.Builder requestBuilder =
203210
new Request.Builder().url(completionsUrl).post(RequestBody.create(jsonPayload, JSON));
@@ -209,11 +216,7 @@ public Flowable<LlmResponse> complete(LlmRequest llmRequest, boolean stream) {
209216
requestBuilder.header("Content-Type", JSON.toString());
210217

211218
Request request = requestBuilder.build();
212-
if (stream) {
213-
return createStreamingFlowable(request);
214-
} else {
215-
return createNonStreamingFlowable(request);
216-
}
219+
return stream ? createStreamingFlowable(request) : createNonStreamingFlowable(request);
217220
});
218221
}
219222

@@ -274,10 +277,14 @@ public void onResponse(Call call, Response response) {
274277
// A single malformed chunk must not abort the entire stream. Log a
275278
// warning and continue.
276279
try {
280+
logger.trace("Raw streaming chat-completion chunk: {}", data);
277281
ChatCompletionsResponse.ChatCompletionChunk chunk =
278282
objectMapper.readValue(
279283
data, ChatCompletionsResponse.ChatCompletionChunk.class);
280284
ImmutableList<LlmResponse> responses = collection.processChunk(chunk);
285+
if (!responses.isEmpty()) {
286+
logger.debug("Responses to emit: {}", responses);
287+
}
281288
for (LlmResponse resp : responses) {
282289
emitter.onNext(resp);
283290
}
@@ -341,9 +348,12 @@ public void onResponse(Call call, Response response) {
341348
}
342349

343350
String jsonResponse = body.string();
351+
logger.trace("Raw non-streaming chat-completion response: {}", jsonResponse);
344352
ChatCompletionsResponse.ChatCompletion completion =
345353
objectMapper.readValue(jsonResponse, ChatCompletionsResponse.ChatCompletion.class);
346-
emitter.onNext(completion.toLlmResponse());
354+
LlmResponse llmResponse = completion.toLlmResponse();
355+
logger.debug("Response to emit: {}", llmResponse);
356+
emitter.onNext(llmResponse);
347357
emitter.onComplete();
348358
} catch (Exception e) {
349359
emitter.tryOnError(e);

core/src/main/java/com/google/adk/models/chat/ChatCompletionsRequest.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.google.adk.JsonBaseModel;
2727
import com.google.adk.models.LlmRequest;
2828
import com.google.common.collect.ImmutableList;
29+
import com.google.common.collect.ImmutableMap;
2930
import com.google.genai.types.Content;
3031
import com.google.genai.types.FunctionDeclaration;
3132
import com.google.genai.types.FunctionResponse;
@@ -351,6 +352,9 @@ private static List<Message> processContent(Content content) {
351352
List<ChatCompletionsCommon.ToolCall> toolCalls = new ArrayList<>();
352353
List<Message> toolResponses = new ArrayList<>();
353354
List<String> refusals = new ArrayList<>();
355+
// Capture a message-level thought_signature from the first text Part that carries one.
356+
// This signature must be echoed back on subsequent turns to ensure proper round-tripping.
357+
byte[][] textThoughtSignature = new byte[][] {null};
354358

355359
content
356360
.parts()
@@ -370,6 +374,9 @@ private static List<Message> processContent(Content content) {
370374
if (split.refusal() != null) {
371375
refusals.add(split.refusal());
372376
}
377+
if (textThoughtSignature[0] == null && part.thoughtSignature().isPresent()) {
378+
textThoughtSignature[0] = part.thoughtSignature().get();
379+
}
373380
} else if (part.inlineData().isPresent()) {
374381
contentParts.add(processInlineDataPart(part));
375382
} else if (part.fileData().isPresent()) {
@@ -403,6 +410,15 @@ private static List<Message> processContent(Content content) {
403410
msg.content = new MessageContent(ImmutableList.copyOf(contentParts));
404411
}
405412
}
413+
// Round-trip the message-level thought_signature for assistant text responses.
414+
if (textThoughtSignature[0] != null) {
415+
msg.extraContent =
416+
ImmutableMap.of(
417+
"google",
418+
ImmutableMap.of(
419+
"thought_signature",
420+
Base64.getEncoder().encodeToString(textThoughtSignature[0])));
421+
}
406422
List<Message> messages = new ArrayList<>();
407423
messages.add(msg);
408424
return messages;
@@ -446,6 +462,10 @@ private static ContentPart processFileDataPart(Part part) {
446462
/**
447463
* Processes a function call part and returns a mapped ToolCall.
448464
*
465+
* <p>If the source {@link Part} carries a {@code thoughtSignature}, it is round-tripped back out
466+
* as a base64-encoded string in {@code extra_content.google.thought_signature} to satisfy
467+
* endpoint requirements.
468+
*
449469
* @param part The input part containing a requested function call or invocation.
450470
* @return The mapped function call tool call.
451471
*/
@@ -464,6 +484,13 @@ private static ChatCompletionsCommon.ToolCall processFunctionCallPart(Part part)
464484
}
465485
}
466486
toolCall.function = function;
487+
part.thoughtSignature()
488+
.ifPresent(
489+
sigBytes -> {
490+
String sig = Base64.getEncoder().encodeToString(sigBytes);
491+
toolCall.extraContent =
492+
ImmutableMap.of("google", ImmutableMap.of("thought_signature", sig));
493+
});
467494
return toolCall;
468495
}
469496

@@ -616,6 +643,13 @@ static class Message {
616643

617644
/** See class definition for more details. */
618645
public String refusal;
646+
647+
/**
648+
* Message-level additional parameters used by some providers. Used for round-tripping data like
649+
* {@code extra_content.google.thought_signature}.
650+
*/
651+
@JsonProperty("extra_content")
652+
public Map<String, Object> extraContent;
619653
}
620654

621655
/**

0 commit comments

Comments
 (0)