feat: publish v1.0.0-rc.36-custom.1
Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 9m20s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 55m7s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s
Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 9m20s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 55m7s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s
This commit is contained in:
commit
bb7968b612
2381 files changed
+551494
No files matched your search
@@ -0,0 +1,515 @@
|
||||
package toolconv
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/QuantumNous/new-api/relaykit/dto"
|
||||
kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil"
|
||||
"github.com/QuantumNous/new-api/relaykit/types"
|
||||
)
|
||||
|
||||
// InspectResponse reports protocol information that the current response
|
||||
// converters cannot faithfully express. It keeps loss handling centralized so
|
||||
// direct and multi-step routes behave consistently.
|
||||
func InspectResponse(from types.RelayFormat, to types.RelayFormat, response any) []types.ConversionDiagnostic {
|
||||
if from == to {
|
||||
return nil
|
||||
}
|
||||
var diagnostics []types.ConversionDiagnostic
|
||||
switch value := response.(type) {
|
||||
case *dto.ClaudeResponse:
|
||||
diagnostics = inspectClaudeResponse(value, to)
|
||||
case dto.ClaudeResponse:
|
||||
diagnostics = inspectClaudeResponse(&value, to)
|
||||
case *dto.OpenAIResponsesResponse:
|
||||
diagnostics = inspectOpenAIResponsesResponse(value, to)
|
||||
case dto.OpenAIResponsesResponse:
|
||||
diagnostics = inspectOpenAIResponsesResponse(&value, to)
|
||||
case *dto.ResponsesStreamResponse:
|
||||
diagnostics = inspectOpenAIResponsesStreamResponse(value)
|
||||
case dto.ResponsesStreamResponse:
|
||||
diagnostics = inspectOpenAIResponsesStreamResponse(&value)
|
||||
case *dto.GeminiChatResponse:
|
||||
diagnostics = inspectGeminiResponse(value, to)
|
||||
case dto.GeminiChatResponse:
|
||||
diagnostics = inspectGeminiResponse(&value, to)
|
||||
}
|
||||
for index := range diagnostics {
|
||||
diagnostics[index].From = from
|
||||
diagnostics[index].To = to
|
||||
}
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
// InspectStreamResponse avoids treating a single Gemini streaming chunk as a
|
||||
// complete grounding document. Gemini grounding chunk indexes and segment
|
||||
// offsets are cumulative across the stream; the stateful converter validates
|
||||
// and resolves them after accumulating prior chunks.
|
||||
func InspectStreamResponse(from types.RelayFormat, to types.RelayFormat, response any) []types.ConversionDiagnostic {
|
||||
if from != types.RelayFormatGemini {
|
||||
return InspectResponse(from, to, response)
|
||||
}
|
||||
var value *dto.GeminiChatResponse
|
||||
switch response := response.(type) {
|
||||
case *dto.GeminiChatResponse:
|
||||
value = response
|
||||
case dto.GeminiChatResponse:
|
||||
value = &response
|
||||
default:
|
||||
return InspectResponse(from, to, response)
|
||||
}
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
var diagnostics []types.ConversionDiagnostic
|
||||
for index := range value.Candidates {
|
||||
metadata := value.Candidates[index].GroundingMetadata
|
||||
if metadata == nil {
|
||||
continue
|
||||
}
|
||||
if len(metadata.WebSearchQueries) > 0 && to != types.RelayFormatOpenAIResponses {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(
|
||||
fmt.Sprintf("candidates[%d].groundingMetadata.webSearchQueries", index),
|
||||
"web_search_call_unrepresentable",
|
||||
"Gemini grounding confirms a hosted web search, but the target stream converter cannot produce an OpenAI Responses web_search_call lifecycle",
|
||||
))
|
||||
}
|
||||
if len(metadata.WebSearchQueries) == 0 && len(metadata.RetrievalQueries) == 0 && len(metadata.SearchEntryPoint) == 0 && len(metadata.RetrievalMetadata) == 0 && len(metadata.SourceFlaggingUris) == 0 && metadata.GoogleMapsWidgetContextToken == "" {
|
||||
continue
|
||||
}
|
||||
diagnostics = append(diagnostics, responsePresentationLoss(
|
||||
fmt.Sprintf("candidates[%d].groundingMetadata", index),
|
||||
"hosted_tool_metadata_reduced",
|
||||
"Gemini grounding citations are preserved across stream chunks, but provider-specific search metadata has no target-protocol equivalent",
|
||||
))
|
||||
}
|
||||
for index := range diagnostics {
|
||||
diagnostics[index].From = from
|
||||
diagnostics[index].To = to
|
||||
}
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
func inspectClaudeResponse(response *dto.ClaudeResponse, to types.RelayFormat) []types.ConversionDiagnostic {
|
||||
if response == nil {
|
||||
return nil
|
||||
}
|
||||
var diagnostics []types.ConversionDiagnostic
|
||||
if to != types.RelayFormatOpenAIResponses && (response.StopReason == "pause_turn" || response.Delta != nil && response.Delta.StopReason != nil && *response.Delta.StopReason == "pause_turn") {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(
|
||||
"stop_reason",
|
||||
"continuation_state_lost",
|
||||
"Claude pause_turn requires protocol-native continuation state that the target response cannot preserve",
|
||||
))
|
||||
}
|
||||
for index := range response.Content {
|
||||
diagnostics = append(diagnostics, inspectClaudeContentBlock(&response.Content[index], fmt.Sprintf("content[%d]", index), to, false)...)
|
||||
}
|
||||
if response.ContentBlock != nil {
|
||||
diagnostics = append(diagnostics, inspectClaudeContentBlock(response.ContentBlock, "content_block", to, true)...)
|
||||
}
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
func inspectClaudeContentBlock(block *dto.ClaudeMediaMessage, path string, to types.RelayFormat, stream bool) []types.ConversionDiagnostic {
|
||||
if block == nil {
|
||||
return nil
|
||||
}
|
||||
blockType := strings.TrimSpace(block.Type)
|
||||
var diagnostics []types.ConversionDiagnostic
|
||||
if isClaudeHostedToolBlock(blockType) {
|
||||
kind := KindNative
|
||||
if blockType == "server_tool_use" || blockType == "mcp_tool_use" {
|
||||
kind = hostedKindFromClaudeCall(blockType, block.Name)
|
||||
} else {
|
||||
kind = hostedKindFromClaudeResult(blockType)
|
||||
}
|
||||
if to != types.RelayFormatOpenAIResponses || kind != KindWebSearch && kind != KindMCP {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(
|
||||
path,
|
||||
"hosted_tool_unrepresentable",
|
||||
fmt.Sprintf("%s cannot losslessly represent Claude hosted-tool response block %q", to, blockType),
|
||||
))
|
||||
} else if blockType == "server_tool_use" || blockType == "mcp_tool_use" {
|
||||
if block.Id == "" {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(
|
||||
path+".id",
|
||||
"hosted_tool_id_missing",
|
||||
"Claude hosted-tool call has no id for pairing it with its result",
|
||||
))
|
||||
}
|
||||
if kind == KindMCP && (block.Name == "" || block.ServerName == "") {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(
|
||||
path,
|
||||
"mcp_identity_missing",
|
||||
"Claude MCP tool use must include both name and server_name for Responses MCP mapping",
|
||||
))
|
||||
}
|
||||
if rawJSONPresent(block.Caller) {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(
|
||||
path+".caller",
|
||||
"hosted_tool_caller_unrepresentable",
|
||||
"OpenAI Responses web_search_call and mcp_call items cannot preserve Claude's hosted-tool caller provenance",
|
||||
))
|
||||
}
|
||||
if !stream {
|
||||
input, err := kitutil.Marshal(block.Input)
|
||||
if err != nil {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(
|
||||
path+".input",
|
||||
"hosted_tool_input_invalid",
|
||||
err.Error(),
|
||||
))
|
||||
} else if kind == KindMCP {
|
||||
if _, err := responsesMCPArgumentsFromClaude(input); err != nil {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(
|
||||
path+".input",
|
||||
"mcp_arguments_unrepresentable",
|
||||
err.Error(),
|
||||
))
|
||||
}
|
||||
} else if kind == KindWebSearch {
|
||||
if _, err := dto.NormalizeResponsesWebSearchAction(input); err != nil {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(
|
||||
path+".input",
|
||||
"web_search_action_unrepresentable",
|
||||
err.Error(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if block.ToolUseId == "" {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(
|
||||
path+".tool_use_id",
|
||||
"hosted_tool_id_missing",
|
||||
"Claude hosted-tool result has no tool_use_id for pairing it with its call",
|
||||
))
|
||||
}
|
||||
if kind == KindMCP && blockType == "mcp_tool_result" {
|
||||
content, err := kitutil.Marshal(block.Content)
|
||||
if err != nil {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(path+".content", "mcp_result_unrepresentable", err.Error()))
|
||||
} else {
|
||||
failed, errorCode := claudeHostedResultFailure(blockType, content, block.IsError, block.ErrorCode)
|
||||
var normalized bool
|
||||
if failed {
|
||||
_, normalized, err = responsesMCPErrorFromClaudeContent(content, errorCode)
|
||||
} else {
|
||||
_, normalized, err = responsesMCPStringFromClaudeContent(content)
|
||||
}
|
||||
if err != nil {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(path+".content", "mcp_result_unrepresentable", err.Error()))
|
||||
} else if normalized {
|
||||
diagnostics = append(diagnostics, responsePresentationLoss(
|
||||
path+".content",
|
||||
"mcp_text_result_normalized",
|
||||
"Claude's single MCP text block is normalized to a Responses output string",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if blockType == "redacted_thinking" && block.Data != "" {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(
|
||||
path+".data",
|
||||
"continuation_state_lost",
|
||||
"Claude encrypted thinking state cannot be represented by the target response",
|
||||
))
|
||||
}
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
func isClaudeHostedToolBlock(blockType string) bool {
|
||||
if blockType == "server_tool_use" || blockType == "mcp_tool_use" || blockType == "mcp_tool_result" {
|
||||
return true
|
||||
}
|
||||
return strings.HasSuffix(blockType, "_tool_result")
|
||||
}
|
||||
|
||||
func inspectOpenAIResponsesResponse(response *dto.OpenAIResponsesResponse, to types.RelayFormat) []types.ConversionDiagnostic {
|
||||
if response == nil {
|
||||
return nil
|
||||
}
|
||||
var diagnostics []types.ConversionDiagnostic
|
||||
for index := range response.Output {
|
||||
output := &response.Output[index]
|
||||
if !isResponsesHostedOutput(output.Type) {
|
||||
continue
|
||||
}
|
||||
kind := hostedKindFromResponsesType(output.Type)
|
||||
if to != types.RelayFormatClaude || kind != KindWebSearch && kind != KindMCP {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(
|
||||
fmt.Sprintf("output[%d]", index),
|
||||
"hosted_tool_unrepresentable",
|
||||
fmt.Sprintf("%s cannot losslessly represent OpenAI Responses hosted-tool output %q", to, output.Type),
|
||||
))
|
||||
continue
|
||||
}
|
||||
if output.ID == "" {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(
|
||||
fmt.Sprintf("output[%d].id", index),
|
||||
"hosted_tool_id_missing",
|
||||
"hosted-tool output has no id for pairing the call with its result",
|
||||
))
|
||||
}
|
||||
if kind == KindMCP {
|
||||
if output.Name == "" || output.ServerLabel == "" {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(
|
||||
fmt.Sprintf("output[%d]", index),
|
||||
"mcp_identity_missing",
|
||||
"Responses MCP output must include both name and server_label for Claude MCP mapping",
|
||||
))
|
||||
}
|
||||
if output.ApprovalRequestID != "" {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(
|
||||
fmt.Sprintf("output[%d].approval_request_id", index),
|
||||
"mcp_approval_state_unrepresentable",
|
||||
"Claude MCP response blocks cannot preserve a Responses approval_request_id",
|
||||
))
|
||||
}
|
||||
if _, err := claudeMCPInputFromResponses(output.Arguments); err != nil {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(
|
||||
fmt.Sprintf("output[%d].arguments", index),
|
||||
"mcp_arguments_unrepresentable",
|
||||
err.Error(),
|
||||
))
|
||||
}
|
||||
if rawJSONPresent(output.Output) && rawJSONPresent(output.ItemError) {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(
|
||||
fmt.Sprintf("output[%d]", index),
|
||||
"mcp_result_ambiguous",
|
||||
"Responses MCP output contains both output and error",
|
||||
))
|
||||
}
|
||||
for _, field := range []struct {
|
||||
name string
|
||||
raw json.RawMessage
|
||||
}{{name: "output", raw: output.Output}, {name: "error", raw: output.ItemError}} {
|
||||
if !rawJSONPresent(field.raw) {
|
||||
continue
|
||||
}
|
||||
if _, err := claudeMCPContentFromResponsesString(field.raw); err != nil {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(
|
||||
fmt.Sprintf("output[%d].%s", index, field.name),
|
||||
"mcp_result_unrepresentable",
|
||||
err.Error(),
|
||||
))
|
||||
}
|
||||
}
|
||||
} else if kind == KindWebSearch {
|
||||
if _, err := claudeWebSearchInputFromResponses(output.Action); err != nil {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(
|
||||
fmt.Sprintf("output[%d].action", index),
|
||||
"web_search_action_unrepresentable",
|
||||
err.Error(),
|
||||
))
|
||||
}
|
||||
}
|
||||
if output.Status != "" && output.Status != "in_progress" && output.Status != "completed" && output.Status != "failed" {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(
|
||||
fmt.Sprintf("output[%d].status", index),
|
||||
"hosted_tool_status_unrepresentable",
|
||||
fmt.Sprintf("Claude cannot preserve hosted-tool status %q", output.Status),
|
||||
))
|
||||
}
|
||||
if output.Status == "failed" && !rawJSONPresent(output.ItemError) && !rawJSONPresent(output.Output) && !rawJSONPresent(output.Results) {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(
|
||||
fmt.Sprintf("output[%d].status", index),
|
||||
"hosted_tool_error_missing",
|
||||
"failed hosted-tool output has no error or output that Claude can preserve",
|
||||
))
|
||||
}
|
||||
}
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
func inspectOpenAIResponsesStreamResponse(response *dto.ResponsesStreamResponse) []types.ConversionDiagnostic {
|
||||
if response == nil {
|
||||
return nil
|
||||
}
|
||||
if response.Item != nil && isResponsesHostedOutput(response.Item.Type) {
|
||||
return []types.ConversionDiagnostic{responseSemanticLoss(
|
||||
"item",
|
||||
"hosted_tool_event_unrepresentable",
|
||||
fmt.Sprintf("OpenAI Responses hosted-tool output %q has no semantic target-protocol stream mapping", response.Item.Type),
|
||||
)}
|
||||
}
|
||||
eventType := strings.TrimSpace(response.Type)
|
||||
if strings.Contains(eventType, ".web_search_call.") ||
|
||||
strings.Contains(eventType, ".file_search_call.") ||
|
||||
strings.Contains(eventType, ".code_interpreter_call.") ||
|
||||
strings.Contains(eventType, ".computer_tool_call.") ||
|
||||
strings.Contains(eventType, ".image_generation_call.") ||
|
||||
strings.Contains(eventType, ".mcp_call.") {
|
||||
return []types.ConversionDiagnostic{responseSemanticLoss(
|
||||
"type",
|
||||
"hosted_tool_event_unrepresentable",
|
||||
fmt.Sprintf("OpenAI Responses hosted-tool stream event %q has no semantic target-protocol mapping", eventType),
|
||||
)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isResponsesHostedOutput(outputType string) bool {
|
||||
switch strings.TrimSpace(outputType) {
|
||||
case "", "message", "reasoning", "function_call", "custom_tool_call":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func inspectGeminiResponse(response *dto.GeminiChatResponse, to types.RelayFormat) []types.ConversionDiagnostic {
|
||||
if response == nil {
|
||||
return nil
|
||||
}
|
||||
var diagnostics []types.ConversionDiagnostic
|
||||
for index := range response.Candidates {
|
||||
metadata := response.Candidates[index].GroundingMetadata
|
||||
if metadata == nil {
|
||||
continue
|
||||
}
|
||||
path := fmt.Sprintf("candidates[%d].groundingMetadata", index)
|
||||
if len(metadata.WebSearchQueries) > 0 && to != types.RelayFormatOpenAIResponses {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(
|
||||
path+".webSearchQueries",
|
||||
"web_search_call_unrepresentable",
|
||||
"Gemini grounding confirms a hosted web search, but the target converter cannot produce an OpenAI Responses web_search_call item",
|
||||
))
|
||||
}
|
||||
diagnostics = append(diagnostics, inspectGeminiGroundingCitations(response.Candidates[index].Content, metadata, path)...)
|
||||
if len(metadata.WebSearchQueries) == 0 && len(metadata.RetrievalQueries) == 0 && len(metadata.SearchEntryPoint) == 0 && len(metadata.RetrievalMetadata) == 0 && len(metadata.SourceFlaggingUris) == 0 && metadata.GoogleMapsWidgetContextToken == "" {
|
||||
continue
|
||||
}
|
||||
diagnostics = append(diagnostics, responsePresentationLoss(
|
||||
path,
|
||||
"hosted_tool_metadata_reduced",
|
||||
"Gemini grounding citations are preserved, but provider-specific search metadata has no target-protocol equivalent",
|
||||
))
|
||||
}
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
type groundingSupportForInspection struct {
|
||||
Segment struct {
|
||||
PartIndex *int `json:"partIndex,omitempty"`
|
||||
StartIndex int `json:"startIndex,omitempty"`
|
||||
EndIndex int `json:"endIndex,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
} `json:"segment"`
|
||||
GroundingChunkIndices []int `json:"groundingChunkIndices"`
|
||||
}
|
||||
|
||||
type groundingChunkForInspection struct {
|
||||
Web *groundingSourceForInspection `json:"web,omitempty"`
|
||||
RetrievedContext *groundingSourceForInspection `json:"retrievedContext,omitempty"`
|
||||
}
|
||||
|
||||
type groundingSourceForInspection struct {
|
||||
URI string `json:"uri,omitempty"`
|
||||
}
|
||||
|
||||
func inspectGeminiGroundingCitations(content dto.GeminiChatContent, metadata *dto.GeminiGroundingMetadata, path string) []types.ConversionDiagnostic {
|
||||
if len(metadata.GroundingSupports) == 0 {
|
||||
return nil
|
||||
}
|
||||
var chunks []groundingChunkForInspection
|
||||
if len(metadata.GroundingChunks) == 0 || kitutil.Unmarshal(metadata.GroundingChunks, &chunks) != nil {
|
||||
return []types.ConversionDiagnostic{responseSemanticLoss(
|
||||
path+".groundingChunks",
|
||||
"grounding_source_invalid",
|
||||
"Gemini grounding chunks are missing or cannot be decoded",
|
||||
)}
|
||||
}
|
||||
var supports []groundingSupportForInspection
|
||||
if err := kitutil.Unmarshal(metadata.GroundingSupports, &supports); err != nil {
|
||||
return []types.ConversionDiagnostic{responseSemanticLoss(
|
||||
path+".groundingSupports",
|
||||
"grounding_citation_invalid",
|
||||
fmt.Sprintf("Gemini grounding supports cannot be decoded: %v", err),
|
||||
)}
|
||||
}
|
||||
textPartCount := 0
|
||||
soleTextPart := -1
|
||||
for index := range content.Parts {
|
||||
if content.Parts[index].Text == "" || content.Parts[index].Thought {
|
||||
continue
|
||||
}
|
||||
textPartCount++
|
||||
soleTextPart = index
|
||||
}
|
||||
var diagnostics []types.ConversionDiagnostic
|
||||
for index, support := range supports {
|
||||
segmentPath := fmt.Sprintf("%s.groundingSupports[%d].segment", path, index)
|
||||
hasSource := false
|
||||
for _, chunkIndex := range support.GroundingChunkIndices {
|
||||
if chunkIndex < 0 || chunkIndex >= len(chunks) {
|
||||
continue
|
||||
}
|
||||
source := chunks[chunkIndex].Web
|
||||
if source == nil {
|
||||
source = chunks[chunkIndex].RetrievedContext
|
||||
}
|
||||
if source != nil && source.URI != "" {
|
||||
hasSource = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasSource {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(
|
||||
fmt.Sprintf("%s.groundingSupports[%d].groundingChunkIndices", path, index),
|
||||
"grounding_source_invalid",
|
||||
"Gemini grounding support does not reference a valid source URI",
|
||||
))
|
||||
continue
|
||||
}
|
||||
partIndex := soleTextPart
|
||||
if support.Segment.PartIndex != nil {
|
||||
partIndex = *support.Segment.PartIndex
|
||||
} else if textPartCount != 1 {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(
|
||||
segmentPath+".partIndex",
|
||||
"grounding_part_ambiguous",
|
||||
"Gemini grounding omitted partIndex while multiple text parts are present, so citation placement is ambiguous",
|
||||
))
|
||||
continue
|
||||
}
|
||||
if partIndex < 0 || partIndex >= len(content.Parts) || content.Parts[partIndex].Text == "" || content.Parts[partIndex].Thought {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(
|
||||
segmentPath+".partIndex",
|
||||
"grounding_part_invalid",
|
||||
fmt.Sprintf("Gemini grounding references non-text part %d", partIndex),
|
||||
))
|
||||
continue
|
||||
}
|
||||
partText := content.Parts[partIndex].Text
|
||||
start, end := support.Segment.StartIndex, support.Segment.EndIndex
|
||||
if start < 0 || end <= start || end > len(partText) || !utf8.ValidString(partText[:start]) || !utf8.ValidString(partText[:end]) {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(
|
||||
segmentPath,
|
||||
"grounding_offset_invalid",
|
||||
"Gemini grounding byte offsets do not identify valid UTF-8 boundaries in the referenced part",
|
||||
))
|
||||
continue
|
||||
}
|
||||
if support.Segment.Text != "" && partText[start:end] != support.Segment.Text {
|
||||
diagnostics = append(diagnostics, responseSemanticLoss(
|
||||
segmentPath+".text",
|
||||
"grounding_text_mismatch",
|
||||
"Gemini grounding segment text does not match the referenced part range",
|
||||
))
|
||||
}
|
||||
}
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
func responseSemanticLoss(path string, code string, message string) types.ConversionDiagnostic {
|
||||
return types.ConversionDiagnostic{Code: code, Path: path, Message: message, Severity: types.ConversionDiagnosticError}
|
||||
}
|
||||
|
||||
func responsePresentationLoss(path string, code string, message string) types.ConversionDiagnostic {
|
||||
return types.ConversionDiagnostic{Code: code, Path: path, Message: message, Severity: types.ConversionDiagnosticWarning}
|
||||
}
|
||||
Reference in new issue
Block a user