File size: 2,780 Bytes
f606b10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
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
}