Merge pull request #611 from soilSpoon/feature/antigravity

feat(antigravity): Improve Claude model compatibility
This commit is contained in:
Luis Pater
2025-12-21 16:27:29 +08:00
committed by GitHub
14 changed files with 2162 additions and 80 deletions
+10
View File
@@ -0,0 +1,10 @@
package util
import "strings"
// IsClaudeThinkingModel checks if the model is a Claude thinking model
// that requires the interleaved-thinking beta header.
func IsClaudeThinkingModel(model string) bool {
lower := strings.ToLower(model)
return strings.Contains(lower, "claude") && strings.Contains(lower, "thinking")
}
+41
View File
@@ -0,0 +1,41 @@
package util
import "testing"
func TestIsClaudeThinkingModel(t *testing.T) {
tests := []struct {
name string
model string
expected bool
}{
// Claude thinking models - should return true
{"claude-sonnet-4-5-thinking", "claude-sonnet-4-5-thinking", true},
{"claude-opus-4-5-thinking", "claude-opus-4-5-thinking", true},
{"Claude-Sonnet-Thinking uppercase", "Claude-Sonnet-4-5-Thinking", true},
{"claude thinking mixed case", "Claude-THINKING-Model", true},
// Non-thinking Claude models - should return false
{"claude-sonnet-4-5 (no thinking)", "claude-sonnet-4-5", false},
{"claude-opus-4-5 (no thinking)", "claude-opus-4-5", false},
{"claude-3-5-sonnet", "claude-3-5-sonnet-20240620", false},
// Non-Claude models - should return false
{"gemini-3-pro-preview", "gemini-3-pro-preview", false},
{"gemini-thinking model", "gemini-3-pro-thinking", false}, // not Claude
{"gpt-4o", "gpt-4o", false},
{"empty string", "", false},
// Edge cases
{"thinking without claude", "thinking-model", false},
{"claude without thinking", "claude-model", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := IsClaudeThinkingModel(tt.model)
if result != tt.expected {
t.Errorf("IsClaudeThinkingModel(%q) = %v, expected %v", tt.model, result, tt.expected)
}
})
}
}
+53 -3
View File
@@ -12,10 +12,10 @@ import (
var gjsonPathKeyReplacer = strings.NewReplacer(".", "\\.", "*", "\\*", "?", "\\?")
// CleanJSONSchemaForGemini transforms a JSON schema to be compatible with Gemini/Antigravity API.
// CleanJSONSchemaForAntigravity transforms a JSON schema to be compatible with Antigravity API.
// It handles unsupported keywords, type flattening, and schema simplification while preserving
// semantic information as description hints.
func CleanJSONSchemaForGemini(jsonStr string) string {
func CleanJSONSchemaForAntigravity(jsonStr string) string {
// Phase 1: Convert and add hints
jsonStr = convertRefsToHints(jsonStr)
jsonStr = convertConstToEnum(jsonStr)
@@ -32,6 +32,9 @@ func CleanJSONSchemaForGemini(jsonStr string) string {
jsonStr = removeUnsupportedKeywords(jsonStr)
jsonStr = cleanupRequiredFields(jsonStr)
// Phase 4: Add placeholder for empty object schemas (Claude VALIDATED mode requirement)
jsonStr = addEmptySchemaPlaceholder(jsonStr)
return jsonStr
}
@@ -105,7 +108,8 @@ func addAdditionalPropertiesHints(jsonStr string) string {
var unsupportedConstraints = []string{
"minLength", "maxLength", "exclusiveMinimum", "exclusiveMaximum",
"pattern", "minItems", "maxItems",
"pattern", "minItems", "maxItems", "format",
"default", "examples", // Claude rejects these in VALIDATED mode
}
func moveConstraintsToDescription(jsonStr string) string {
@@ -339,6 +343,52 @@ func cleanupRequiredFields(jsonStr string) string {
return jsonStr
}
// addEmptySchemaPlaceholder adds a placeholder "reason" property to empty object schemas.
// Claude VALIDATED mode requires at least one property in tool schemas.
func addEmptySchemaPlaceholder(jsonStr string) string {
// Find all "type" fields
paths := findPaths(jsonStr, "type")
// Process from deepest to shallowest (to handle nested objects properly)
sortByDepth(paths)
for _, p := range paths {
typeVal := gjson.Get(jsonStr, p)
if typeVal.String() != "object" {
continue
}
// Get the parent path (the object containing "type")
parentPath := trimSuffix(p, ".type")
// Check if properties exists and is empty or missing
propsPath := joinPath(parentPath, "properties")
propsVal := gjson.Get(jsonStr, propsPath)
needsPlaceholder := false
if !propsVal.Exists() {
// No properties field at all
needsPlaceholder = true
} else if propsVal.IsObject() && len(propsVal.Map()) == 0 {
// Empty properties object
needsPlaceholder = true
}
if needsPlaceholder {
// Add placeholder "reason" property
reasonPath := joinPath(propsPath, "reason")
jsonStr, _ = sjson.Set(jsonStr, reasonPath+".type", "string")
jsonStr, _ = sjson.Set(jsonStr, reasonPath+".description", "Brief explanation of why you are calling this tool")
// Add to required array
reqPath := joinPath(parentPath, "required")
jsonStr, _ = sjson.Set(jsonStr, reqPath, []string{"reason"})
}
}
return jsonStr
}
// --- Helpers ---
func findPaths(jsonStr, field string) []string {
+249 -44
View File
@@ -5,9 +5,11 @@ import (
"reflect"
"strings"
"testing"
"github.com/tidwall/gjson"
)
func TestCleanJSONSchemaForGemini_ConstToEnum(t *testing.T) {
func TestCleanJSONSchemaForAntigravity_ConstToEnum(t *testing.T) {
input := `{
"type": "object",
"properties": {
@@ -28,11 +30,11 @@ func TestCleanJSONSchemaForGemini_ConstToEnum(t *testing.T) {
}
}`
result := CleanJSONSchemaForGemini(input)
result := CleanJSONSchemaForAntigravity(input)
compareJSON(t, expected, result)
}
func TestCleanJSONSchemaForGemini_TypeFlattening_Nullable(t *testing.T) {
func TestCleanJSONSchemaForAntigravity_TypeFlattening_Nullable(t *testing.T) {
input := `{
"type": "object",
"properties": {
@@ -60,11 +62,11 @@ func TestCleanJSONSchemaForGemini_TypeFlattening_Nullable(t *testing.T) {
"required": ["other"]
}`
result := CleanJSONSchemaForGemini(input)
result := CleanJSONSchemaForAntigravity(input)
compareJSON(t, expected, result)
}
func TestCleanJSONSchemaForGemini_ConstraintsToDescription(t *testing.T) {
func TestCleanJSONSchemaForAntigravity_ConstraintsToDescription(t *testing.T) {
input := `{
"type": "object",
"properties": {
@@ -81,7 +83,7 @@ func TestCleanJSONSchemaForGemini_ConstraintsToDescription(t *testing.T) {
}
}`
result := CleanJSONSchemaForGemini(input)
result := CleanJSONSchemaForAntigravity(input)
// minItems should be REMOVED and moved to description
if strings.Contains(result, `"minItems"`) {
@@ -100,7 +102,7 @@ func TestCleanJSONSchemaForGemini_ConstraintsToDescription(t *testing.T) {
}
}
func TestCleanJSONSchemaForGemini_AnyOfFlattening_SmartSelection(t *testing.T) {
func TestCleanJSONSchemaForAntigravity_AnyOfFlattening_SmartSelection(t *testing.T) {
input := `{
"type": "object",
"properties": {
@@ -131,11 +133,11 @@ func TestCleanJSONSchemaForGemini_AnyOfFlattening_SmartSelection(t *testing.T) {
}
}`
result := CleanJSONSchemaForGemini(input)
result := CleanJSONSchemaForAntigravity(input)
compareJSON(t, expected, result)
}
func TestCleanJSONSchemaForGemini_OneOfFlattening(t *testing.T) {
func TestCleanJSONSchemaForAntigravity_OneOfFlattening(t *testing.T) {
input := `{
"type": "object",
"properties": {
@@ -158,11 +160,11 @@ func TestCleanJSONSchemaForGemini_OneOfFlattening(t *testing.T) {
}
}`
result := CleanJSONSchemaForGemini(input)
result := CleanJSONSchemaForAntigravity(input)
compareJSON(t, expected, result)
}
func TestCleanJSONSchemaForGemini_AllOfMerging(t *testing.T) {
func TestCleanJSONSchemaForAntigravity_AllOfMerging(t *testing.T) {
input := `{
"type": "object",
"allOf": [
@@ -190,11 +192,11 @@ func TestCleanJSONSchemaForGemini_AllOfMerging(t *testing.T) {
"required": ["a", "b"]
}`
result := CleanJSONSchemaForGemini(input)
result := CleanJSONSchemaForAntigravity(input)
compareJSON(t, expected, result)
}
func TestCleanJSONSchemaForGemini_RefHandling(t *testing.T) {
func TestCleanJSONSchemaForAntigravity_RefHandling(t *testing.T) {
input := `{
"definitions": {
"User": {
@@ -210,21 +212,29 @@ func TestCleanJSONSchemaForGemini_RefHandling(t *testing.T) {
}
}`
// After $ref is converted to placeholder object, empty schema placeholder is also added
expected := `{
"type": "object",
"properties": {
"customer": {
"type": "object",
"description": "See: User"
"description": "See: User",
"properties": {
"reason": {
"type": "string",
"description": "Brief explanation of why you are calling this tool"
}
},
"required": ["reason"]
}
}
}`
result := CleanJSONSchemaForGemini(input)
result := CleanJSONSchemaForAntigravity(input)
compareJSON(t, expected, result)
}
func TestCleanJSONSchemaForGemini_RefHandling_DescriptionEscaping(t *testing.T) {
func TestCleanJSONSchemaForAntigravity_RefHandling_DescriptionEscaping(t *testing.T) {
input := `{
"definitions": {
"User": {
@@ -243,21 +253,29 @@ func TestCleanJSONSchemaForGemini_RefHandling_DescriptionEscaping(t *testing.T)
}
}`
// After $ref is converted, empty schema placeholder is also added
expected := `{
"type": "object",
"properties": {
"customer": {
"type": "object",
"description": "He said \"hi\"\\nsecond line (See: User)"
"description": "He said \"hi\"\\nsecond line (See: User)",
"properties": {
"reason": {
"type": "string",
"description": "Brief explanation of why you are calling this tool"
}
},
"required": ["reason"]
}
}
}`
result := CleanJSONSchemaForGemini(input)
result := CleanJSONSchemaForAntigravity(input)
compareJSON(t, expected, result)
}
func TestCleanJSONSchemaForGemini_CyclicRefDefaults(t *testing.T) {
func TestCleanJSONSchemaForAntigravity_CyclicRefDefaults(t *testing.T) {
input := `{
"definitions": {
"Node": {
@@ -270,7 +288,7 @@ func TestCleanJSONSchemaForGemini_CyclicRefDefaults(t *testing.T) {
"$ref": "#/definitions/Node"
}`
result := CleanJSONSchemaForGemini(input)
result := CleanJSONSchemaForAntigravity(input)
var resMap map[string]interface{}
json.Unmarshal([]byte(result), &resMap)
@@ -285,7 +303,7 @@ func TestCleanJSONSchemaForGemini_CyclicRefDefaults(t *testing.T) {
}
}
func TestCleanJSONSchemaForGemini_RequiredCleanup(t *testing.T) {
func TestCleanJSONSchemaForAntigravity_RequiredCleanup(t *testing.T) {
input := `{
"type": "object",
"properties": {
@@ -304,11 +322,11 @@ func TestCleanJSONSchemaForGemini_RequiredCleanup(t *testing.T) {
"required": ["a", "b"]
}`
result := CleanJSONSchemaForGemini(input)
result := CleanJSONSchemaForAntigravity(input)
compareJSON(t, expected, result)
}
func TestCleanJSONSchemaForGemini_AllOfMerging_DotKeys(t *testing.T) {
func TestCleanJSONSchemaForAntigravity_AllOfMerging_DotKeys(t *testing.T) {
input := `{
"type": "object",
"allOf": [
@@ -336,11 +354,11 @@ func TestCleanJSONSchemaForGemini_AllOfMerging_DotKeys(t *testing.T) {
"required": ["my.param", "b"]
}`
result := CleanJSONSchemaForGemini(input)
result := CleanJSONSchemaForAntigravity(input)
compareJSON(t, expected, result)
}
func TestCleanJSONSchemaForGemini_PropertyNameCollision(t *testing.T) {
func TestCleanJSONSchemaForAntigravity_PropertyNameCollision(t *testing.T) {
// A tool has an argument named "pattern" - should NOT be treated as a constraint
input := `{
"type": "object",
@@ -364,7 +382,7 @@ func TestCleanJSONSchemaForGemini_PropertyNameCollision(t *testing.T) {
"required": ["pattern"]
}`
result := CleanJSONSchemaForGemini(input)
result := CleanJSONSchemaForAntigravity(input)
compareJSON(t, expected, result)
var resMap map[string]interface{}
@@ -375,7 +393,7 @@ func TestCleanJSONSchemaForGemini_PropertyNameCollision(t *testing.T) {
}
}
func TestCleanJSONSchemaForGemini_DotKeys(t *testing.T) {
func TestCleanJSONSchemaForAntigravity_DotKeys(t *testing.T) {
input := `{
"type": "object",
"properties": {
@@ -389,7 +407,7 @@ func TestCleanJSONSchemaForGemini_DotKeys(t *testing.T) {
}
}`
result := CleanJSONSchemaForGemini(input)
result := CleanJSONSchemaForAntigravity(input)
var resMap map[string]interface{}
if err := json.Unmarshal([]byte(result), &resMap); err != nil {
@@ -414,7 +432,7 @@ func TestCleanJSONSchemaForGemini_DotKeys(t *testing.T) {
}
}
func TestCleanJSONSchemaForGemini_AnyOfAlternativeHints(t *testing.T) {
func TestCleanJSONSchemaForAntigravity_AnyOfAlternativeHints(t *testing.T) {
input := `{
"type": "object",
"properties": {
@@ -428,7 +446,7 @@ func TestCleanJSONSchemaForGemini_AnyOfAlternativeHints(t *testing.T) {
}
}`
result := CleanJSONSchemaForGemini(input)
result := CleanJSONSchemaForAntigravity(input)
if !strings.Contains(result, "Accepts:") {
t.Errorf("Expected alternative types hint, got: %s", result)
@@ -438,7 +456,7 @@ func TestCleanJSONSchemaForGemini_AnyOfAlternativeHints(t *testing.T) {
}
}
func TestCleanJSONSchemaForGemini_NullableHint(t *testing.T) {
func TestCleanJSONSchemaForAntigravity_NullableHint(t *testing.T) {
input := `{
"type": "object",
"properties": {
@@ -450,7 +468,7 @@ func TestCleanJSONSchemaForGemini_NullableHint(t *testing.T) {
"required": ["name"]
}`
result := CleanJSONSchemaForGemini(input)
result := CleanJSONSchemaForAntigravity(input)
if !strings.Contains(result, "(nullable)") {
t.Errorf("Expected nullable hint, got: %s", result)
@@ -460,7 +478,7 @@ func TestCleanJSONSchemaForGemini_NullableHint(t *testing.T) {
}
}
func TestCleanJSONSchemaForGemini_TypeFlattening_Nullable_DotKey(t *testing.T) {
func TestCleanJSONSchemaForAntigravity_TypeFlattening_Nullable_DotKey(t *testing.T) {
input := `{
"type": "object",
"properties": {
@@ -488,11 +506,11 @@ func TestCleanJSONSchemaForGemini_TypeFlattening_Nullable_DotKey(t *testing.T) {
"required": ["other"]
}`
result := CleanJSONSchemaForGemini(input)
result := CleanJSONSchemaForAntigravity(input)
compareJSON(t, expected, result)
}
func TestCleanJSONSchemaForGemini_EnumHint(t *testing.T) {
func TestCleanJSONSchemaForAntigravity_EnumHint(t *testing.T) {
input := `{
"type": "object",
"properties": {
@@ -504,7 +522,7 @@ func TestCleanJSONSchemaForGemini_EnumHint(t *testing.T) {
}
}`
result := CleanJSONSchemaForGemini(input)
result := CleanJSONSchemaForAntigravity(input)
if !strings.Contains(result, "Allowed:") {
t.Errorf("Expected enum values hint, got: %s", result)
@@ -514,7 +532,7 @@ func TestCleanJSONSchemaForGemini_EnumHint(t *testing.T) {
}
}
func TestCleanJSONSchemaForGemini_AdditionalPropertiesHint(t *testing.T) {
func TestCleanJSONSchemaForAntigravity_AdditionalPropertiesHint(t *testing.T) {
input := `{
"type": "object",
"properties": {
@@ -523,14 +541,14 @@ func TestCleanJSONSchemaForGemini_AdditionalPropertiesHint(t *testing.T) {
"additionalProperties": false
}`
result := CleanJSONSchemaForGemini(input)
result := CleanJSONSchemaForAntigravity(input)
if !strings.Contains(result, "No extra properties allowed") {
t.Errorf("Expected additionalProperties hint, got: %s", result)
}
}
func TestCleanJSONSchemaForGemini_AnyOfFlattening_PreservesDescription(t *testing.T) {
func TestCleanJSONSchemaForAntigravity_AnyOfFlattening_PreservesDescription(t *testing.T) {
input := `{
"type": "object",
"properties": {
@@ -554,11 +572,11 @@ func TestCleanJSONSchemaForGemini_AnyOfFlattening_PreservesDescription(t *testin
}
}`
result := CleanJSONSchemaForGemini(input)
result := CleanJSONSchemaForAntigravity(input)
compareJSON(t, expected, result)
}
func TestCleanJSONSchemaForGemini_SingleEnumNoHint(t *testing.T) {
func TestCleanJSONSchemaForAntigravity_SingleEnumNoHint(t *testing.T) {
input := `{
"type": "object",
"properties": {
@@ -569,14 +587,14 @@ func TestCleanJSONSchemaForGemini_SingleEnumNoHint(t *testing.T) {
}
}`
result := CleanJSONSchemaForGemini(input)
result := CleanJSONSchemaForAntigravity(input)
if strings.Contains(result, "Allowed:") {
t.Errorf("Single value enum should not add Allowed hint, got: %s", result)
}
}
func TestCleanJSONSchemaForGemini_MultipleNonNullTypes(t *testing.T) {
func TestCleanJSONSchemaForAntigravity_MultipleNonNullTypes(t *testing.T) {
input := `{
"type": "object",
"properties": {
@@ -586,7 +604,7 @@ func TestCleanJSONSchemaForGemini_MultipleNonNullTypes(t *testing.T) {
}
}`
result := CleanJSONSchemaForGemini(input)
result := CleanJSONSchemaForAntigravity(input)
if !strings.Contains(result, "Accepts:") {
t.Errorf("Expected multiple types hint, got: %s", result)
@@ -676,3 +694,190 @@ func compareJSON(t *testing.T, expectedJSON, actualJSON string) {
t.Errorf("JSON mismatch:\nExpected:\n%s\n\nActual:\n%s", string(expBytes), string(actBytes))
}
}
// ============================================================================
// Empty Schema Placeholder Tests
// ============================================================================
func TestCleanJSONSchemaForAntigravity_EmptySchemaPlaceholder(t *testing.T) {
// Empty object schema with no properties should get a placeholder
input := `{
"type": "object"
}`
result := CleanJSONSchemaForAntigravity(input)
// Should have placeholder property added
if !strings.Contains(result, `"reason"`) {
t.Errorf("Empty schema should have 'reason' placeholder property, got: %s", result)
}
if !strings.Contains(result, `"required"`) {
t.Errorf("Empty schema should have 'required' with 'reason', got: %s", result)
}
}
func TestCleanJSONSchemaForAntigravity_EmptyPropertiesPlaceholder(t *testing.T) {
// Object with empty properties object
input := `{
"type": "object",
"properties": {}
}`
result := CleanJSONSchemaForAntigravity(input)
// Should have placeholder property added
if !strings.Contains(result, `"reason"`) {
t.Errorf("Empty properties should have 'reason' placeholder, got: %s", result)
}
}
func TestCleanJSONSchemaForAntigravity_NonEmptySchemaUnchanged(t *testing.T) {
// Schema with properties should NOT get placeholder
input := `{
"type": "object",
"properties": {
"name": {"type": "string"}
},
"required": ["name"]
}`
result := CleanJSONSchemaForAntigravity(input)
// Should NOT have placeholder property
if strings.Contains(result, `"reason"`) {
t.Errorf("Non-empty schema should NOT have 'reason' placeholder, got: %s", result)
}
// Original properties should be preserved
if !strings.Contains(result, `"name"`) {
t.Errorf("Original property 'name' should be preserved, got: %s", result)
}
}
func TestCleanJSONSchemaForAntigravity_NestedEmptySchema(t *testing.T) {
// Nested empty object in items should also get placeholder
input := `{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object"
}
}
}
}`
result := CleanJSONSchemaForAntigravity(input)
// Nested empty object should also get placeholder
// Check that the nested object has a reason property
parsed := gjson.Parse(result)
nestedProps := parsed.Get("properties.items.items.properties")
if !nestedProps.Exists() || !nestedProps.Get("reason").Exists() {
t.Errorf("Nested empty object should have 'reason' placeholder, got: %s", result)
}
}
func TestCleanJSONSchemaForAntigravity_EmptySchemaWithDescription(t *testing.T) {
// Empty schema with description should preserve description and add placeholder
input := `{
"type": "object",
"description": "An empty object"
}`
result := CleanJSONSchemaForAntigravity(input)
// Should have both description and placeholder
if !strings.Contains(result, `"An empty object"`) {
t.Errorf("Description should be preserved, got: %s", result)
}
if !strings.Contains(result, `"reason"`) {
t.Errorf("Empty schema should have 'reason' placeholder, got: %s", result)
}
}
// ============================================================================
// Format field handling (ad-hoc patch removal)
// ============================================================================
func TestCleanJSONSchemaForAntigravity_FormatFieldRemoval(t *testing.T) {
// format:"uri" should be removed and added as hint
input := `{
"type": "object",
"properties": {
"url": {
"type": "string",
"format": "uri",
"description": "A URL"
}
}
}`
result := CleanJSONSchemaForAntigravity(input)
// format should be removed
if strings.Contains(result, `"format"`) {
t.Errorf("format field should be removed, got: %s", result)
}
// hint should be added to description
if !strings.Contains(result, "format: uri") {
t.Errorf("format hint should be added to description, got: %s", result)
}
// original description should be preserved
if !strings.Contains(result, "A URL") {
t.Errorf("Original description should be preserved, got: %s", result)
}
}
func TestCleanJSONSchemaForAntigravity_FormatFieldNoDescription(t *testing.T) {
// format without description should create description with hint
input := `{
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email"
}
}
}`
result := CleanJSONSchemaForAntigravity(input)
// format should be removed
if strings.Contains(result, `"format"`) {
t.Errorf("format field should be removed, got: %s", result)
}
// hint should be added
if !strings.Contains(result, "format: email") {
t.Errorf("format hint should be added, got: %s", result)
}
}
func TestCleanJSONSchemaForAntigravity_MultipleFormats(t *testing.T) {
// Multiple format fields should all be handled
input := `{
"type": "object",
"properties": {
"url": {"type": "string", "format": "uri"},
"email": {"type": "string", "format": "email"},
"date": {"type": "string", "format": "date-time"}
}
}`
result := CleanJSONSchemaForAntigravity(input)
// All format fields should be removed
if strings.Contains(result, `"format"`) {
t.Errorf("All format fields should be removed, got: %s", result)
}
// All hints should be added
if !strings.Contains(result, "format: uri") {
t.Errorf("uri format hint should be added, got: %s", result)
}
if !strings.Contains(result, "format: email") {
t.Errorf("email format hint should be added, got: %s", result)
}
if !strings.Contains(result, "format: date-time") {
t.Errorf("date-time format hint should be added, got: %s", result)
}
}
+87
View File
@@ -0,0 +1,87 @@
package util
import (
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
// GetThinkingText extracts the thinking text from a content part.
// Handles various formats:
// - Simple string: { "thinking": "text" } or { "text": "text" }
// - Wrapped object: { "thinking": { "text": "text", "cache_control": {...} } }
// - Gemini-style: { "thought": true, "text": "text" }
// Returns the extracted text string.
func GetThinkingText(part gjson.Result) string {
// Try direct text field first (Gemini-style)
if text := part.Get("text"); text.Exists() && text.Type == gjson.String {
return text.String()
}
// Try thinking field
thinkingField := part.Get("thinking")
if !thinkingField.Exists() {
return ""
}
// thinking is a string
if thinkingField.Type == gjson.String {
return thinkingField.String()
}
// thinking is an object with inner text/thinking
if thinkingField.IsObject() {
if inner := thinkingField.Get("text"); inner.Exists() && inner.Type == gjson.String {
return inner.String()
}
if inner := thinkingField.Get("thinking"); inner.Exists() && inner.Type == gjson.String {
return inner.String()
}
}
return ""
}
// GetThinkingTextFromJSON extracts thinking text from a raw JSON string.
func GetThinkingTextFromJSON(jsonStr string) string {
return GetThinkingText(gjson.Parse(jsonStr))
}
// SanitizeThinkingPart normalizes a thinking part to a canonical form.
// Strips cache_control and other non-essential fields.
// Returns the sanitized part as JSON string.
func SanitizeThinkingPart(part gjson.Result) string {
// Gemini-style: { thought: true, text, thoughtSignature }
if part.Get("thought").Bool() {
result := `{"thought":true}`
if text := GetThinkingText(part); text != "" {
result, _ = sjson.Set(result, "text", text)
}
if sig := part.Get("thoughtSignature"); sig.Exists() && sig.Type == gjson.String {
result, _ = sjson.Set(result, "thoughtSignature", sig.String())
}
return result
}
// Anthropic-style: { type: "thinking", thinking, signature }
if part.Get("type").String() == "thinking" || part.Get("thinking").Exists() {
result := `{"type":"thinking"}`
if text := GetThinkingText(part); text != "" {
result, _ = sjson.Set(result, "thinking", text)
}
if sig := part.Get("signature"); sig.Exists() && sig.Type == gjson.String {
result, _ = sjson.Set(result, "signature", sig.String())
}
return result
}
// Not a thinking part, return as-is but strip cache_control
return StripCacheControl(part.Raw)
}
// StripCacheControl removes cache_control and providerOptions from a JSON object.
func StripCacheControl(jsonStr string) string {
result := jsonStr
result, _ = sjson.Delete(result, "cache_control")
result, _ = sjson.Delete(result, "providerOptions")
return result
}