* fix(execution): 补齐原生消息透传并保留对话结构 * fix(execution): 保护转换请求的指令和执行约束 * fix(execution): 补齐无角色历史后的系统指令保护
416 lines
11 KiB
Go
416 lines
11 KiB
Go
package dialect
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"gpt-load/internal/execution"
|
|
"gpt-load/internal/protocol"
|
|
)
|
|
|
|
func chatExecutionMetadata(
|
|
clientProtocol protocol.Protocol,
|
|
body []byte,
|
|
) (execution.Operation, execution.RouteRequirement) {
|
|
nativeResource := chatRequiresNativeRoute(clientProtocol, body)
|
|
return execution.OperationChatCompletion, routeRequirement(nativeResource)
|
|
}
|
|
|
|
func responsesExecutionMetadata(
|
|
request *ParsedRequest,
|
|
) (
|
|
execution.Operation,
|
|
execution.RouteRequirement,
|
|
execution.ResponsesStorePreference,
|
|
) {
|
|
operation := responsesOperation(request)
|
|
nativeResource := operation == execution.OperationResponsesRetrieve ||
|
|
operation == execution.OperationResponsesDelete ||
|
|
operation == execution.OperationResponsesCancel ||
|
|
operation == execution.OperationResponsesInputItems ||
|
|
operation == execution.OperationResponsesPassthrough
|
|
if operation == execution.OperationResponsesCreate {
|
|
routeRequirement, storePreference := responsesCreateRequirements(request.Body)
|
|
return operation, routeRequirement, storePreference
|
|
}
|
|
return operation, routeRequirement(nativeResource), execution.ResponsesStorePreferenceNone
|
|
}
|
|
|
|
func routeRequirement(native bool) execution.RouteRequirement {
|
|
if native {
|
|
return execution.RouteRequirementNative
|
|
}
|
|
return execution.RouteRequirementAny
|
|
}
|
|
|
|
func responsesOperation(request *ParsedRequest) execution.Operation {
|
|
if request == nil {
|
|
return execution.OperationResponsesPassthrough
|
|
}
|
|
switch {
|
|
case request.Method == http.MethodPost && request.Path == openAIResponsesPath:
|
|
return execution.OperationResponsesCreate
|
|
case request.Method == http.MethodPost && request.Path == openAIResponsesCompactPath:
|
|
return execution.OperationResponsesCompact
|
|
case request.Method == http.MethodPost && request.Path == openAIResponsesPath+"/input_tokens":
|
|
return execution.OperationResponsesInputTokens
|
|
}
|
|
|
|
resourcePath := strings.TrimPrefix(request.Path, openAIResponsesPath+"/")
|
|
segments := strings.Split(resourcePath, "/")
|
|
if resourcePath == request.Path || len(segments) == 0 || segments[0] == "" {
|
|
return execution.OperationResponsesPassthrough
|
|
}
|
|
switch {
|
|
case len(segments) == 1 && request.Method == http.MethodGet:
|
|
return execution.OperationResponsesRetrieve
|
|
case len(segments) == 1 && request.Method == http.MethodDelete:
|
|
return execution.OperationResponsesDelete
|
|
case len(segments) == 2 && segments[1] == "cancel" && request.Method == http.MethodPost:
|
|
return execution.OperationResponsesCancel
|
|
case len(segments) == 2 && segments[1] == "input_items" && request.Method == http.MethodGet:
|
|
return execution.OperationResponsesInputItems
|
|
default:
|
|
return execution.OperationResponsesPassthrough
|
|
}
|
|
}
|
|
|
|
func decodeExecutionFeatureObject(body []byte) (map[string]any, bool) {
|
|
if len(bytes.TrimSpace(body)) == 0 {
|
|
return nil, false
|
|
}
|
|
decoder := json.NewDecoder(bytes.NewReader(body))
|
|
decoder.UseNumber()
|
|
var object map[string]any
|
|
if err := decoder.Decode(&object); err != nil || object == nil {
|
|
return nil, false
|
|
}
|
|
return object, true
|
|
}
|
|
|
|
func hasMeaningfulField(object map[string]any, field string) bool {
|
|
value, ok := object[field]
|
|
if !ok || value == nil {
|
|
return false
|
|
}
|
|
switch typed := value.(type) {
|
|
case []any:
|
|
return len(typed) > 0
|
|
case map[string]any:
|
|
return len(typed) > 0
|
|
case string:
|
|
return typed != ""
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
// CountMidConversationSystemMessages 只观察真实角色,不把用户文本中的提示标签当作系统消息。
|
|
func CountMidConversationSystemMessages(clientProtocol protocol.Protocol, body []byte) int {
|
|
root, ok := decodeExecutionFeatureObject(body)
|
|
if !ok {
|
|
return 0
|
|
}
|
|
field := "messages"
|
|
switch clientProtocol {
|
|
case protocol.OpenAICompletions, protocol.Anthropic:
|
|
case protocol.OpenAIResponses:
|
|
field = "input"
|
|
default:
|
|
return 0
|
|
}
|
|
messages, _ := root[field].([]any)
|
|
seenConversation, count := false, 0
|
|
for _, raw := range messages {
|
|
message, ok := raw.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
role, _ := message["role"].(string)
|
|
if role == "system" || role == "developer" {
|
|
if seenConversation && hasMeaningfulField(message, "content") {
|
|
count++
|
|
}
|
|
continue
|
|
}
|
|
switch role {
|
|
case "user", "assistant", "tool":
|
|
seenConversation = true
|
|
}
|
|
itemType, _ := message["type"].(string)
|
|
if clientProtocol == protocol.OpenAIResponses && itemType != "" {
|
|
// 非指令项目也属于有序历史,不按部分工具类型枚举会话起点。
|
|
seenConversation = true
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func responsesCreateRequirements(
|
|
body []byte,
|
|
) (execution.RouteRequirement, execution.ResponsesStorePreference) {
|
|
root, ok := decodeExecutionFeatureObject(body)
|
|
if !ok {
|
|
return execution.RouteRequirementNative, execution.ResponsesStorePreferenceNone
|
|
}
|
|
if hasMeaningfulField(root, "conversation") ||
|
|
responsesPromptReferencesProviderResource(root["prompt"]) {
|
|
return execution.RouteRequirementNative, execution.ResponsesStorePreferenceNone
|
|
}
|
|
if background, ok := root["background"].(bool); ok && background {
|
|
return execution.RouteRequirementNative, execution.ResponsesStorePreferenceNone
|
|
}
|
|
if responsesInputReferencesProviderResource(root["input"]) ||
|
|
responsesToolsReferenceProviderResource(root["tools"]) {
|
|
return execution.RouteRequirementNative, execution.ResponsesStorePreferenceNone
|
|
}
|
|
value, exists := root["store"]
|
|
store, ok := value.(bool)
|
|
if exists && !ok {
|
|
return execution.RouteRequirementNative, execution.ResponsesStorePreferenceNone
|
|
}
|
|
// 其他资源与参数约束优先;仅纯 ID 续接使用上游存储要求。
|
|
if hasMeaningfulField(root, "previous_response_id") {
|
|
return execution.RouteRequirementNative, execution.ResponsesStorePreferenceRequireStored
|
|
}
|
|
if !exists || store {
|
|
return execution.RouteRequirementAny, execution.ResponsesStorePreferencePreferStored
|
|
}
|
|
return execution.RouteRequirementAny, execution.ResponsesStorePreferenceNone
|
|
}
|
|
|
|
func responsesPromptReferencesProviderResource(value any) bool {
|
|
prompt, ok := value.(map[string]any)
|
|
return ok && hasMeaningfulField(prompt, "id")
|
|
}
|
|
|
|
func chatRequiresNativeRoute(clientProtocol protocol.Protocol, body []byte) bool {
|
|
root, ok := decodeExecutionFeatureObject(body)
|
|
if !ok {
|
|
return false
|
|
}
|
|
switch clientProtocol {
|
|
case protocol.OpenAICompletions:
|
|
return openAIChatMessagesReferenceProviderResource(root["messages"])
|
|
case protocol.Gemini:
|
|
return hasMeaningfulField(root, "cachedContent") ||
|
|
hasMeaningfulField(root, "cached_content") ||
|
|
geminiContentsReferenceProviderResource(root["contents"])
|
|
case protocol.Anthropic:
|
|
return hasMeaningfulField(root, "container") ||
|
|
anthropicMessagesReferenceProviderResource(root["messages"])
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func openAIChatMessagesReferenceProviderResource(value any) bool {
|
|
messages, ok := value.([]any)
|
|
if !ok {
|
|
return false
|
|
}
|
|
for _, value := range messages {
|
|
message, ok := value.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
blocks, _ := message["content"].([]any)
|
|
for _, value := range blocks {
|
|
block, ok := value.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
blockType, _ := block["type"].(string)
|
|
file, _ := block["file"].(map[string]any)
|
|
if normalizeFeatureName(blockType) == "file" && hasMeaningfulField(file, "file_id") {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func geminiContentsReferenceProviderResource(value any) bool {
|
|
contents, ok := value.([]any)
|
|
if !ok {
|
|
return false
|
|
}
|
|
for _, value := range contents {
|
|
content, ok := value.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
parts, _ := content["parts"].([]any)
|
|
for _, value := range parts {
|
|
part, ok := value.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
fileData := objectField(part, "fileData", "file_data")
|
|
fileURI, _ := stringField(fileData, "fileUri", "file_uri")
|
|
if geminiProviderResourceURI(fileURI) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func objectField(object map[string]any, names ...string) map[string]any {
|
|
for _, name := range names {
|
|
if value, ok := object[name].(map[string]any); ok {
|
|
return value
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func stringField(object map[string]any, names ...string) (string, bool) {
|
|
for _, name := range names {
|
|
if value, ok := object[name].(string); ok {
|
|
return value, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func geminiProviderResourceURI(value string) bool {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return false
|
|
}
|
|
parsed, err := url.Parse(value)
|
|
if err == nil {
|
|
if strings.EqualFold(parsed.Scheme, "gs") {
|
|
return true
|
|
}
|
|
if strings.EqualFold(parsed.Hostname(), "generativelanguage.googleapis.com") &&
|
|
strings.Contains(strings.ToLower(parsed.Path), "/files/") {
|
|
return true
|
|
}
|
|
}
|
|
relative := strings.TrimPrefix(strings.TrimSpace(value), "/")
|
|
return strings.HasPrefix(strings.ToLower(relative), "files/") ||
|
|
strings.HasPrefix(strings.ToLower(relative), "v1beta/files/")
|
|
}
|
|
|
|
func responsesInputReferencesProviderResource(value any) bool {
|
|
items, ok := value.([]any)
|
|
if !ok {
|
|
return false
|
|
}
|
|
for _, value := range items {
|
|
item, ok := value.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
itemType, _ := item["type"].(string)
|
|
switch normalizeFeatureName(itemType) {
|
|
case "itemreference":
|
|
if hasMeaningfulField(item, "id") {
|
|
return true
|
|
}
|
|
case "codeinterpretercall", "computer_call", "computercall", "computercalloutput":
|
|
if hasMeaningfulField(item, "container_id") {
|
|
return true
|
|
}
|
|
}
|
|
if responsesContentReferencesProviderResource(item["content"]) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func responsesContentReferencesProviderResource(value any) bool {
|
|
blocks, ok := value.([]any)
|
|
if !ok {
|
|
return false
|
|
}
|
|
for _, value := range blocks {
|
|
block, ok := value.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
blockType, _ := block["type"].(string)
|
|
switch normalizeFeatureName(blockType) {
|
|
case "inputfile", "inputimage":
|
|
if hasMeaningfulField(block, "file_id") {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func responsesToolsReferenceProviderResource(value any) bool {
|
|
tools, ok := value.([]any)
|
|
if !ok {
|
|
return false
|
|
}
|
|
for _, value := range tools {
|
|
tool, ok := value.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
toolType, _ := tool["type"].(string)
|
|
switch normalizeFeatureName(toolType) {
|
|
case "filesearch":
|
|
if hasMeaningfulField(tool, "vector_store_ids") {
|
|
return true
|
|
}
|
|
case "codeinterpreter":
|
|
if hasMeaningfulField(tool, "container") || hasMeaningfulField(tool, "container_id") {
|
|
return true
|
|
}
|
|
case "imagegeneration":
|
|
mask, _ := tool["input_image_mask"].(map[string]any)
|
|
if hasMeaningfulField(mask, "file_id") {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func anthropicMessagesReferenceProviderResource(value any) bool {
|
|
messages, ok := value.([]any)
|
|
if !ok {
|
|
return false
|
|
}
|
|
for _, value := range messages {
|
|
message, ok := value.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
blocks, _ := message["content"].([]any)
|
|
for _, value := range blocks {
|
|
block, ok := value.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
blockType, _ := block["type"].(string)
|
|
if normalizeFeatureName(blockType) == "containerupload" &&
|
|
hasMeaningfulField(block, "file_id") {
|
|
return true
|
|
}
|
|
source, _ := block["source"].(map[string]any)
|
|
sourceType, _ := source["type"].(string)
|
|
if normalizeFeatureName(sourceType) == "file" &&
|
|
hasMeaningfulField(source, "file_id") {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func normalizeFeatureName(value string) string {
|
|
value = strings.ToLower(value)
|
|
value = strings.ReplaceAll(value, "_", "")
|
|
return strings.ReplaceAll(value, "-", "")
|
|
}
|