feat(metadata): enhance metadata extraction with reasoning content support
This commit is contained in:
@@ -92,11 +92,18 @@ type chatMessage struct {
|
||||
|
||||
type chatCompletionsResponse struct {
|
||||
Choices []struct {
|
||||
Message chatMessage `json:"message"`
|
||||
Message responseChatMessage `json:"message"`
|
||||
Text string `json:"text,omitempty"`
|
||||
} `json:"choices"`
|
||||
Error *providerError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type responseChatMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content json.RawMessage `json:"content"`
|
||||
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||
}
|
||||
|
||||
type providerError struct {
|
||||
Message string `json:"message"`
|
||||
Type string `json:"type,omitempty"`
|
||||
@@ -200,7 +207,7 @@ func (c *Client) extractMetadataWithModel(ctx context.Context, input, model stri
|
||||
return thoughttypes.ThoughtMetadata{}, fmt.Errorf("%s metadata: no choices returned", c.name)
|
||||
}
|
||||
|
||||
rawResponse := resp.Choices[0].Message.Content
|
||||
rawResponse := extractChoiceText(resp.Choices[0].Message, resp.Choices[0].Text)
|
||||
|
||||
if c.logConversations && c.log != nil {
|
||||
c.log.Info("metadata conversation response",
|
||||
@@ -247,7 +254,7 @@ func (c *Client) Summarize(ctx context.Context, systemPrompt, userPrompt string)
|
||||
return "", fmt.Errorf("%s summarize: no choices returned", c.name)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(resp.Choices[0].Message.Content), nil
|
||||
return extractChoiceText(resp.Choices[0].Message, resp.Choices[0].Text), nil
|
||||
}
|
||||
|
||||
func (c *Client) Name() string {
|
||||
@@ -335,12 +342,45 @@ func (c *Client) doJSON(ctx context.Context, path string, requestBody any, dest
|
||||
// extractJSONObject finds the first complete {...} block in s.
|
||||
// It handles models that prepend prose to a JSON response despite json_object mode.
|
||||
func extractJSONObject(s string) string {
|
||||
start := strings.Index(s, "{")
|
||||
end := strings.LastIndex(s, "}")
|
||||
if start == -1 || end == -1 || end <= start {
|
||||
return ""
|
||||
for start := 0; start < len(s); start++ {
|
||||
if s[start] != '{' {
|
||||
continue
|
||||
}
|
||||
depth := 0
|
||||
inString := false
|
||||
escaped := false
|
||||
for end := start; end < len(s); end++ {
|
||||
ch := s[end]
|
||||
if escaped {
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
if ch == '\\' && inString {
|
||||
escaped = true
|
||||
continue
|
||||
}
|
||||
if ch == '"' {
|
||||
inString = !inString
|
||||
continue
|
||||
}
|
||||
if inString {
|
||||
continue
|
||||
}
|
||||
switch ch {
|
||||
case '{':
|
||||
depth++
|
||||
case '}':
|
||||
depth--
|
||||
if depth == 0 {
|
||||
candidate := s[start : end+1]
|
||||
if json.Valid([]byte(candidate)) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return s[start : end+1]
|
||||
return ""
|
||||
}
|
||||
|
||||
// stripThinkingBlocks removes <think>...</think> and <thinking>...</thinking>
|
||||
@@ -379,6 +419,71 @@ func stripCodeFence(value string) string {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
|
||||
func extractChoiceText(message responseChatMessage, fallbackText string) string {
|
||||
if text := strings.TrimSpace(extractMessageContent(message.Content)); text != "" {
|
||||
return text
|
||||
}
|
||||
if text := strings.TrimSpace(message.ReasoningContent); text != "" {
|
||||
return text
|
||||
}
|
||||
return strings.TrimSpace(fallbackText)
|
||||
}
|
||||
|
||||
func extractMessageContent(raw json.RawMessage) string {
|
||||
raw = bytes.TrimSpace(raw)
|
||||
if len(raw) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var contentString string
|
||||
if err := json.Unmarshal(raw, &contentString); err == nil {
|
||||
return contentString
|
||||
}
|
||||
|
||||
var contentArray []any
|
||||
if err := json.Unmarshal(raw, &contentArray); err == nil {
|
||||
parts := make([]string, 0, len(contentArray))
|
||||
for _, item := range contentArray {
|
||||
if text := strings.TrimSpace(extractTextFromAny(item)); text != "" {
|
||||
parts = append(parts, text)
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, "\n")
|
||||
}
|
||||
|
||||
var contentObject map[string]any
|
||||
if err := json.Unmarshal(raw, &contentObject); err == nil {
|
||||
return extractTextFromAny(contentObject)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(raw))
|
||||
}
|
||||
|
||||
func extractTextFromAny(value any) string {
|
||||
switch typed := value.(type) {
|
||||
case string:
|
||||
return typed
|
||||
case []any:
|
||||
parts := make([]string, 0, len(typed))
|
||||
for _, item := range typed {
|
||||
if text := strings.TrimSpace(extractTextFromAny(item)); text != "" {
|
||||
parts = append(parts, text)
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, "\n")
|
||||
case map[string]any:
|
||||
// Common provider shapes for chat content parts.
|
||||
for _, key := range []string{"text", "output_text", "content", "value"} {
|
||||
if nested, ok := typed[key]; ok {
|
||||
if text := strings.TrimSpace(extractTextFromAny(nested)); text != "" {
|
||||
return text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isRetryableStatus(status int) bool {
|
||||
switch status {
|
||||
case http.StatusTooManyRequests, http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout:
|
||||
|
||||
Reference in New Issue
Block a user