Skip to content

fix(opencode): repair tool pairing and strip thoughts for GPT-OSS models - #34

Open
2103271 wants to merge 1 commit into
cortexkit:mainfrom
2103271:fix/opencode-gpt-oss-tool-pairing
Open

2103271 wants to merge 1 commit into
cortexkit:mainfrom
2103271:fix/opencode-gpt-oss-tool-pairing

Conversation

@2103271

@2103271 2103271 commented Oct 12, 2026 •

Copy link
Copy Markdown

Problem

When using Google Antigravity with GPT-OSS models (such as \gpt-oss-120b-medium) in OpenCode:

  1. Multi-turn interactions and interactive tool calls (e.g. \question\ with options) require matching \id\ values between \ unctionCall\ and \ unctionResponse. Without paired IDs, calls fail or responses cannot be matched.
  2. The Antigravity backend can return \ hought\ and \ houghtSignature\ blocks, which GPT-OSS models do not support, causing subsequent turn rejections.
  3. If thoughts are stripped leaving completely empty text parts, Gemini schema validation fails on empty contents.

Solution

  • Added
    epairGptOssToolPairing\ to \packages/opencode/src/plugin/request-helpers.ts:
    • Pairs function calls and function responses with the same name in chronological document order, ensuring earlier orphaned responses cannot steal future call IDs.
    • Generates or preserves consistent \ ool-call-N\ identifiers across calls and matching responses.
    • Strips \ hought, \ houghtSignature, and \ hought_signature\ from parts when sending to \gpt-oss-*\ models.
    • Preserves media parts (\inlineData, \ ileData) and ensures parts that become completely empty receive fallback \ ext: '.'\ to satisfy schema validation.
  • Integrated
    epairGptOssToolPairing\ into \prepareAntigravityRequestIn\ in \packages/opencode/src/plugin/request.ts\ when target model is GPT-OSS.
  • Added comprehensive unit tests in
    equest-helpers.test.ts\ and integration tests in
    equest.test.ts.

Verification

  • All 182 tests passed in
    equest-helpers.test.ts\
  • All 149 tests passed in
    equest.test.ts\
  • Biome check passed with 0 errors and 0 warnings

Summary by cubic

Fixes multi-turn tool calls and rejected turns when using Google Antigravity with GPT-OSS models like gpt-oss-120b-medium.

  • Pairs functionCall and functionResponse parts with matching tool-call-N IDs in document order, preserving existing IDs and avoiding collisions.
  • Strips thought and thoughtSignature from parts for GPT-OSS models, which reject them.
  • Replaces parts left empty after stripping with a . text fallback unless they carry inlineData or fileData.

Written for commit fc9f3f1. Summary will update on new commits.

View guided diff

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

3 issues found across 4 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/opencode/src/plugin/request.ts">

<violation number="1" location="packages/opencode/src/plugin/request.ts:2456">
P1: `sanitizeRequestPayloadForAntigravity` runs after this repair and re-adds `thoughtSignature: SKIP_THOUGHT_SIGNATURE` to the first function call in each content, so GPT-OSS requests can still be rejected for an unsupported signature. Skip this injection for GPT-OSS or strip the signature after sanitization.</violation>
</file>

<file name="packages/opencode/src/plugin/request-helpers.ts">

<violation number="1" location="packages/opencode/src/plugin/request-helpers.ts:3106">
P2: Valid `executableCode` and `codeExecutionResult` parts reach this fallback as if they were empty, so it adds `text: '.'` alongside their payloads. Include both fields in the emptiness check to avoid sending malformed Gemini parts.</violation>

<violation number="2" location="packages/opencode/src/plugin/request-helpers.ts:3295">
P2: This can reuse an ID already owned by a later call, giving two calls the same ID and making their responses ambiguous. Track IDs owned by other calls and allocate a fresh ID when the response ID is already claimed.</violation>
</file>

Reply to a comment to ask cubic a question or push back. It learns from your replies.

View guided diff | Re-trigger cubic

)
}

if (isGptOss && Array.isArray(requestPayload.contents)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: sanitizeRequestPayloadForAntigravity runs after this repair and re-adds thoughtSignature: SKIP_THOUGHT_SIGNATURE to the first function call in each content, so GPT-OSS requests can still be rejected for an unsupported signature. Skip this injection for GPT-OSS or strip the signature after sanitization.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/opencode/src/plugin/request.ts, line 2456:

<comment>`sanitizeRequestPayloadForAntigravity` runs after this repair and re-adds `thoughtSignature: SKIP_THOUGHT_SIGNATURE` to the first function call in each content, so GPT-OSS requests can still be rejected for an unsupported signature. Skip this injection for GPT-OSS or strip the signature after sanitization.</comment>

<file context>
@@ -2447,6 +2453,12 @@ function prepareAntigravityRequestIn(
           )
         }
 
+        if (isGptOss && Array.isArray(requestPayload.contents)) {
+          requestPayload.contents = repairGptOssToolPairing(
+            requestPayload.contents,
</file context>

if (pairedResponses.has(respHolder)) continue
if (isResponseAfterCall(respHolder, callHolder)) {
let pairedId: string | undefined =
callHolder.call.id || respHolder.resp.id

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: This can reuse an ID already owned by a later call, giving two calls the same ID and making their responses ambiguous. Track IDs owned by other calls and allocate a fresh ID when the response ID is already claimed.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/opencode/src/plugin/request-helpers.ts, line 3295:

<comment>This can reuse an ID already owned by a later call, giving two calls the same ID and making their responses ambiguous. Track IDs owned by other calls and allocate a fresh ID when the response ID is already claimed.</comment>

<file context>
@@ -3063,6 +3063,266 @@ export function applyToolPairingFixes(
+        if (pairedResponses.has(respHolder)) continue
+        if (isResponseAfterCall(respHolder, callHolder)) {
+          let pairedId: string | undefined =
+            callHolder.call.id || respHolder.resp.id
+          if (!pairedId) {
+            pairedId = allocateId()
</file context>

if (cloned.functionCall && typeof cloned.functionCall === 'object') {
cloned.functionCall = { ...cloned.functionCall }
}
if (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Valid executableCode and codeExecutionResult parts reach this fallback as if they were empty, so it adds text: '.' alongside their payloads. Include both fields in the emptiness check to avoid sending malformed Gemini parts.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/opencode/src/plugin/request-helpers.ts, line 3106:

<comment>Valid `executableCode` and `codeExecutionResult` parts reach this fallback as if they were empty, so it adds `text: '.'` alongside their payloads. Include both fields in the emptiness check to avoid sending malformed Gemini parts.</comment>

<file context>
@@ -3063,6 +3063,266 @@ export function applyToolPairingFixes(
+      if (cloned.functionCall && typeof cloned.functionCall === 'object') {
+        cloned.functionCall = { ...cloned.functionCall }
+      }
+      if (
+        cloned.functionResponse &&
+        typeof cloned.functionResponse === 'object'
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant