Skip to content

KG-596 Fix thought signature on tool call with gemini 3.0#1317

Merged
kpavlov merged 2 commits intoJetBrains:developfrom
mltheuser:mltheuser/kg-596
Jan 6, 2026
Merged

KG-596 Fix thought signature on tool call with gemini 3.0#1317
kpavlov merged 2 commits intoJetBrains:developfrom
mltheuser:mltheuser/kg-596

Conversation

@mltheuser
Copy link
Copy Markdown
Contributor

@mltheuser mltheuser commented Jan 4, 2026

Motivation and Context

Related to: KG-596

Gemini 3.0 models enforce stricter validation of thoughtSignature for function calls. When the model returns parallel tool calls, only the first call in a turn receives a thoughtSignature. On subsequent turns, the API expects this signature to be echoed back exactly. Without proper handling, multi-turn agentic conversations with parallel tools fail with cryptic API errors.

The Problem: The GoogleLLMClient wasn't preserving thoughtSignature across turns, and wasn't correctly re-grouping parallel tool calls/results when constructing requests—leading to malformed conversation structures that newer Gemini models reject.

How It's Solved

  1. Preserve thoughtSignature: When processing model responses, we now extract the thoughtSignature from GooglePart.FunctionCall and store it in Message.Tool.Call.metaInfo.metadata. When building subsequent requests, we restore it.

  2. Correctly batch parallel tool calls/results: The createGoogleRequest function now uses a buffering strategy to re-group interleaved messages. The key insight: if a tool call has a thoughtSignature, it starts a new turn; if it doesn't, it's a parallel call in the same turn. This lets us batch calls into a single model role GoogleContent and results into a single user role GoogleContent, as the API requires.

  3. Clean, idiomatic implementation: The buffering logic uses a when expression to make the three states explicit (new turn, starting fresh, parallel call), keeping the code readable and maintainable.

Breaking Changes

None. This is a backward-compatible fix that enables correct behavior with Gemini 3.0+ while remaining compatible with earlier models.


Type of the changes

  • New feature (non-breaking change which adds functionality)
  • Bug fix (non-breaking change which fixes an issue)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Tests improvement
  • Refactoring

Checklist

  • The pull request has a description of the proposed change
  • I read the Contributing Guidelines before opening the pull request
  • The pull request uses develop as the base branch
  • Tests for the changes have been added
  • All new and existing tests passed
Additional steps for pull requests adding a new feature
  • An issue describing the proposed change exists
  • The pull request includes a link to the issue
  • The change was discussed and approved in the issue
  • Docs have been added / updated

Copy link
Copy Markdown
Contributor

@kpavlov kpavlov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, @mltheuser, very useful fix. Brings value
I added a couple of comments on test scenarios, and making it more defensive.

@kpavlov kpavlov added the bugfix Something was fixed 🎉 label Jan 5, 2026
Copy link
Copy Markdown
Collaborator

@antoniibelyshev antoniibelyshev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! Looks almost ready. I saw a couple issues which might need attention.

  1. The reasoning inside a function call seems to be duplicated: it is added as a separate Reasoning message, and then also as part of the function call.
  2. Probably, it's better to not introduce a new field in the metaInfo, and just use the Resoning messages as delimiters indicating where a new api response starts.
    Not sure about both of the points though. I'm open for a discussion here.

@mltheuser
Copy link
Copy Markdown
Contributor Author

mltheuser commented Jan 5, 2026

Thank you everyone for the in-depth review. I have switched to the pattern where we store thought signatures in reasoning messages as suggested by @antoniibelyshev and I have extended the tests as suggested by @kpavlov (your point about verifying the structural logic (especially for parallel calls) was also spot on)

Copy link
Copy Markdown
Collaborator

@antoniibelyshev antoniibelyshev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! LGTM

@kpavlov kpavlov merged commit 48a66d9 into JetBrains:develop Jan 6, 2026
14 checks passed
karloti pushed a commit to karloti/koog that referenced this pull request Jan 7, 2026
…1317)

## Motivation and Context

Related to:
[KG-596](https://youtrack.jetbrains.com/projects/KG/issues/KG-596)

Gemini 3.0 models enforce stricter validation of
[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429)
for function calls. When the model returns parallel tool calls, only the
*first* call in a turn receives a
[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429).
On subsequent turns, the API expects this signature to be echoed back
exactly. Without proper handling, multi-turn agentic conversations with
parallel tools fail with cryptic API errors.

**The Problem:** The
[GoogleLLMClient](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/commonMain/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClient.kt#101-844)
wasn't preserving
[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429)
across turns, and wasn't correctly re-grouping parallel tool
calls/results when constructing requests—leading to malformed
conversation structures that newer Gemini models reject.

## How It's Solved

1. **Preserve
[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429):**
When processing model responses, we now extract the
[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429)
from `GooglePart.FunctionCall` and store it in
`Message.Tool.Call.metaInfo.metadata`. When building subsequent
requests, we restore it.

2. **Correctly batch parallel tool calls/results:** The
[createGoogleRequest](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/commonMain/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClient.kt#280-477)
function now uses a buffering strategy to re-group interleaved messages.
The key insight: if a tool call has a
[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429),
it starts a new turn; if it doesn't, it's a parallel call in the same
turn. This lets us batch calls into a single
[model](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/commonMain/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClient.kt#775-805)
role
[GoogleContent](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/commonMain/kotlin/ai/koog/prompt/executor/clients/google/models/GoogleGenerateContent.kt#48-53)
and results into a single
[user](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#59-74)
role
[GoogleContent](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/commonMain/kotlin/ai/koog/prompt/executor/clients/google/models/GoogleGenerateContent.kt#48-53),
as the API requires.

3. **Clean, idiomatic implementation:** The buffering logic uses a
[when](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#59-74)
expression to make the three states explicit (new turn, starting fresh,
parallel call), keeping the code readable and maintainable.

## Breaking Changes

None. This is a backward-compatible fix that enables correct behavior
with Gemini 3.0+ while remaining compatible with earlier models.

---

#### Type of the changes
- [ ] New feature (non-breaking change which adds functionality)
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Tests improvement
- [ ] Refactoring

#### Checklist
- [x] The pull request has a description of the proposed change
- [x] I read the [Contributing
Guidelines](https://github.com/JetBrains/koog/blob/main/CONTRIBUTING.md)
before opening the pull request
- [x] The pull request uses **`develop`** as the base branch
- [x] Tests for the changes have been added
- [x] All new and existing tests passed

##### Additional steps for pull requests adding a new feature
- [x] An issue describing the proposed change exists
- [x] The pull request includes a link to the issue
- [ ] The change was discussed and approved in the issue
- [ ] Docs have been added / updated

---------

Co-authored-by: Malte Heuser <malte.heuser@ing.com>
karloti pushed a commit to karloti/koog that referenced this pull request Jan 7, 2026
…1317)

## Motivation and Context

Related to:
[KG-596](https://youtrack.jetbrains.com/projects/KG/issues/KG-596)

Gemini 3.0 models enforce stricter validation of
[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429)
for function calls. When the model returns parallel tool calls, only the
*first* call in a turn receives a
[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429).
On subsequent turns, the API expects this signature to be echoed back
exactly. Without proper handling, multi-turn agentic conversations with
parallel tools fail with cryptic API errors.

**The Problem:** The
[GoogleLLMClient](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/commonMain/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClient.kt#101-844)
wasn't preserving
[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429)
across turns, and wasn't correctly re-grouping parallel tool
calls/results when constructing requests—leading to malformed
conversation structures that newer Gemini models reject.

## How It's Solved

1. **Preserve
[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429):**
When processing model responses, we now extract the
[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429)
from `GooglePart.FunctionCall` and store it in
`Message.Tool.Call.metaInfo.metadata`. When building subsequent
requests, we restore it.

2. **Correctly batch parallel tool calls/results:** The
[createGoogleRequest](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/commonMain/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClient.kt#280-477)
function now uses a buffering strategy to re-group interleaved messages.
The key insight: if a tool call has a
[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429),
it starts a new turn; if it doesn't, it's a parallel call in the same
turn. This lets us batch calls into a single
[model](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/commonMain/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClient.kt#775-805)
role
[GoogleContent](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/commonMain/kotlin/ai/koog/prompt/executor/clients/google/models/GoogleGenerateContent.kt#48-53)
and results into a single
[user](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#59-74)
role
[GoogleContent](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/commonMain/kotlin/ai/koog/prompt/executor/clients/google/models/GoogleGenerateContent.kt#48-53),
as the API requires.

3. **Clean, idiomatic implementation:** The buffering logic uses a
[when](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#59-74)
expression to make the three states explicit (new turn, starting fresh,
parallel call), keeping the code readable and maintainable.

## Breaking Changes

None. This is a backward-compatible fix that enables correct behavior
with Gemini 3.0+ while remaining compatible with earlier models.

---

#### Type of the changes
- [ ] New feature (non-breaking change which adds functionality)
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Tests improvement
- [ ] Refactoring

#### Checklist
- [x] The pull request has a description of the proposed change
- [x] I read the [Contributing
Guidelines](https://github.com/JetBrains/koog/blob/main/CONTRIBUTING.md)
before opening the pull request
- [x] The pull request uses **`develop`** as the base branch
- [x] Tests for the changes have been added
- [x] All new and existing tests passed

##### Additional steps for pull requests adding a new feature
- [x] An issue describing the proposed change exists
- [x] The pull request includes a link to the issue
- [ ] The change was discussed and approved in the issue
- [ ] Docs have been added / updated

---------

Co-authored-by: Malte Heuser <malte.heuser@ing.com>
@jay-anbaric
Copy link
Copy Markdown

Is there somewhere official that the public can track what will be release? We are keen to see this fix being part of the next release as this is stopping us from using gemini 3 models with tool calls.

antoniibelyshev pushed a commit that referenced this pull request Jan 16, 2026
## Motivation and Context

Related to:
[KG-596](https://youtrack.jetbrains.com/projects/KG/issues/KG-596)

Gemini 3.0 models enforce stricter validation of
[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429)
for function calls. When the model returns parallel tool calls, only the
*first* call in a turn receives a
[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429).
On subsequent turns, the API expects this signature to be echoed back
exactly. Without proper handling, multi-turn agentic conversations with
parallel tools fail with cryptic API errors.

**The Problem:** The
[GoogleLLMClient](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/commonMain/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClient.kt#101-844)
wasn't preserving
[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429)
across turns, and wasn't correctly re-grouping parallel tool
calls/results when constructing requests—leading to malformed
conversation structures that newer Gemini models reject.

## How It's Solved

1. **Preserve
[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429):**
When processing model responses, we now extract the
[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429)
from `GooglePart.FunctionCall` and store it in
`Message.Tool.Call.metaInfo.metadata`. When building subsequent
requests, we restore it.

2. **Correctly batch parallel tool calls/results:** The
[createGoogleRequest](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/commonMain/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClient.kt#280-477)
function now uses a buffering strategy to re-group interleaved messages.
The key insight: if a tool call has a
[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429),
it starts a new turn; if it doesn't, it's a parallel call in the same
turn. This lets us batch calls into a single
[model](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/commonMain/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClient.kt#775-805)
role
[GoogleContent](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/commonMain/kotlin/ai/koog/prompt/executor/clients/google/models/GoogleGenerateContent.kt#48-53)
and results into a single
[user](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#59-74)
role
[GoogleContent](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/commonMain/kotlin/ai/koog/prompt/executor/clients/google/models/GoogleGenerateContent.kt#48-53),
as the API requires.

3. **Clean, idiomatic implementation:** The buffering logic uses a
[when](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#59-74)
expression to make the three states explicit (new turn, starting fresh,
parallel call), keeping the code readable and maintainable.

## Breaking Changes

None. This is a backward-compatible fix that enables correct behavior
with Gemini 3.0+ while remaining compatible with earlier models.

---

#### Type of the changes
- [ ] New feature (non-breaking change which adds functionality)
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Tests improvement
- [ ] Refactoring

#### Checklist
- [x] The pull request has a description of the proposed change
- [x] I read the [Contributing
Guidelines](https://github.com/JetBrains/koog/blob/main/CONTRIBUTING.md)
before opening the pull request
- [x] The pull request uses **`develop`** as the base branch
- [x] Tests for the changes have been added
- [x] All new and existing tests passed

##### Additional steps for pull requests adding a new feature
- [x] An issue describing the proposed change exists
- [x] The pull request includes a link to the issue
- [ ] The change was discussed and approved in the issue
- [ ] Docs have been added / updated

---------

Co-authored-by: Malte Heuser <malte.heuser@ing.com>
antoniibelyshev added a commit that referenced this pull request Jan 16, 2026
## Motivation and Context

Related to:
[KG-596](https://youtrack.jetbrains.com/projects/KG/issues/KG-596)

Gemini 3.0 models enforce stricter validation of

[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429)
for function calls. When the model returns parallel tool calls, only the
*first* call in a turn receives a

[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429).
On subsequent turns, the API expects this signature to be echoed back
exactly. Without proper handling, multi-turn agentic conversations with
parallel tools fail with cryptic API errors.

**The Problem:** The

[GoogleLLMClient](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/commonMain/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClient.kt#101-844)
wasn't preserving

[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429)
across turns, and wasn't correctly re-grouping parallel tool
calls/results when constructing requests—leading to malformed
conversation structures that newer Gemini models reject.

## How It's Solved

1. **Preserve
[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429):**
When processing model responses, we now extract the
[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429)
from `GooglePart.FunctionCall` and store it in
`Message.Tool.Call.metaInfo.metadata`. When building subsequent
requests, we restore it.

2. **Correctly batch parallel tool calls/results:** The
[createGoogleRequest](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/commonMain/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClient.kt#280-477)
function now uses a buffering strategy to re-group interleaved messages.
The key insight: if a tool call has a

[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429),
it starts a new turn; if it doesn't, it's a parallel call in the same
turn. This lets us batch calls into a single

[model](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/commonMain/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClient.kt#775-805)
role

[GoogleContent](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/commonMain/kotlin/ai/koog/prompt/executor/clients/google/models/GoogleGenerateContent.kt#48-53)
and results into a single

[user](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#59-74)
role

[GoogleContent](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/commonMain/kotlin/ai/koog/prompt/executor/clients/google/models/GoogleGenerateContent.kt#48-53),
as the API requires.

3. **Clean, idiomatic implementation:** The buffering logic uses a
[when](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#59-74)
expression to make the three states explicit (new turn, starting fresh,
parallel call), keeping the code readable and maintainable.

## Breaking Changes

None. This is a backward-compatible fix that enables correct behavior
with Gemini 3.0+ while remaining compatible with earlier models.

---

#### Type of the changes
- [ ] New feature (non-breaking change which adds functionality)
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Tests improvement
- [ ] Refactoring

#### Checklist
- [x] The pull request has a description of the proposed change
- [x] I read the [Contributing
Guidelines](https://github.com/JetBrains/koog/blob/main/CONTRIBUTING.md)
before opening the pull request
- [x] The pull request uses **`develop`** as the base branch
- [x] Tests for the changes have been added
- [x] All new and existing tests passed

##### Additional steps for pull requests adding a new feature
- [x] An issue describing the proposed change exists
- [x] The pull request includes a link to the issue
- [ ] The change was discussed and approved in the issue
- [ ] Docs have been added / updated

---------

<!--
Thank you for opening a pull request!

Please add a brief description of the proposed change here.
Also, please tick the appropriate points in the checklist below.
-->

## Motivation and Context
<!-- Why is this change needed? What problem does it solve? -->

## Breaking Changes
<!-- Will users need to update their code or configurations? -->

---

#### Type of the changes
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Tests improvement
- [ ] Refactoring
- [ ] CI/CD changes
- [ ] Dependencies update

#### Checklist
- [ ] The pull request has a description of the proposed change
- [ ] I read the [Contributing
Guidelines](https://github.com/JetBrains/koog/blob/main/CONTRIBUTING.md)
before opening the pull request
- [ ] The pull request uses **`develop`** as the base branch
- [ ] Tests for the changes have been added
- [ ] All new and existing tests passed

##### Additional steps for pull requests adding a new feature
- [ ] An issue describing the proposed change exists
- [ ] The pull request includes a link to the issue
- [ ] The change was discussed and approved in the issue
- [ ] Docs have been added / updated

Co-authored-by: Malte Heuser <mltheuser@gmail.com>
Co-authored-by: Malte Heuser <malte.heuser@ing.com>
vova-jb pushed a commit that referenced this pull request Jan 27, 2026
## Motivation and Context

Related to:
[KG-596](https://youtrack.jetbrains.com/projects/KG/issues/KG-596)

Gemini 3.0 models enforce stricter validation of
[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429)
for function calls. When the model returns parallel tool calls, only the
*first* call in a turn receives a
[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429).
On subsequent turns, the API expects this signature to be echoed back
exactly. Without proper handling, multi-turn agentic conversations with
parallel tools fail with cryptic API errors.

**The Problem:** The
[GoogleLLMClient](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/commonMain/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClient.kt#101-844)
wasn't preserving
[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429)
across turns, and wasn't correctly re-grouping parallel tool
calls/results when constructing requests—leading to malformed
conversation structures that newer Gemini models reject.

## How It's Solved

1. **Preserve
[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429):**
When processing model responses, we now extract the
[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429)
from `GooglePart.FunctionCall` and store it in
`Message.Tool.Call.metaInfo.metadata`. When building subsequent
requests, we restore it.

2. **Correctly batch parallel tool calls/results:** The
[createGoogleRequest](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/commonMain/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClient.kt#280-477)
function now uses a buffering strategy to re-group interleaved messages.
The key insight: if a tool call has a
[thoughtSignature](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#401-429),
it starts a new turn; if it doesn't, it's a parallel call in the same
turn. This lets us batch calls into a single
[model](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/commonMain/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClient.kt#775-805)
role
[GoogleContent](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/commonMain/kotlin/ai/koog/prompt/executor/clients/google/models/GoogleGenerateContent.kt#48-53)
and results into a single
[user](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#59-74)
role
[GoogleContent](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/commonMain/kotlin/ai/koog/prompt/executor/clients/google/models/GoogleGenerateContent.kt#48-53),
as the API requires.

3. **Clean, idiomatic implementation:** The buffering logic uses a
[when](file:///Users/ku76uh/Developer/jetbrains/fork/koog/prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClientTest.kt#59-74)
expression to make the three states explicit (new turn, starting fresh,
parallel call), keeping the code readable and maintainable.

## Breaking Changes

None. This is a backward-compatible fix that enables correct behavior
with Gemini 3.0+ while remaining compatible with earlier models.

---

#### Type of the changes
- [ ] New feature (non-breaking change which adds functionality)
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Tests improvement
- [ ] Refactoring

#### Checklist
- [x] The pull request has a description of the proposed change
- [x] I read the [Contributing
Guidelines](https://github.com/JetBrains/koog/blob/main/CONTRIBUTING.md)
before opening the pull request
- [x] The pull request uses **`develop`** as the base branch
- [x] Tests for the changes have been added
- [x] All new and existing tests passed

##### Additional steps for pull requests adding a new feature
- [x] An issue describing the proposed change exists
- [x] The pull request includes a link to the issue
- [ ] The change was discussed and approved in the issue
- [ ] Docs have been added / updated

---------

Co-authored-by: Malte Heuser <malte.heuser@ing.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Something was fixed 🎉

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants