Files
ttstd-infinite-canvas/backend/internal/app/cloud_agent_model_selection.go

135 lines
5.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"encoding/json"
"strings"
)
// The tool schema advertises the alternative selection shapes, while this
// validator remains the authoritative boundary for null, whitespace and
// decoded-value checks that JSON Schema cannot reliably express across all
// provider adapters.
const cloudAgentModelSelectionDescription = "模型选择:复制 model_list 的 selection 到顶层。媒体生成必须显式提供 logicalModelId,或同时提供 channelId 与 channelModelKey,二者互斥。未使用字段省略或传空字符串,不得传 null/空白;混用或不完整均拒绝,不会使用项目默认模型或随机选模。"
// A model can occasionally omit the copied selection even after reading
// model_list. We may repair that omission only when the exact same query has a
// single candidate. This is deterministic materialization of an explicit
// model_list result, not a project-default or random model fallback.
func cloudAgentApplyUniqueModelListSelection(state *cloudAgentRuntime, raw string, a *cloudAgentMediaArgs) bool {
if state == nil || a == nil || cloudAgentModelSelectionProvided(raw) {
return false
}
for i := len(state.Events) - 1; i >= 0; i-- {
event := state.Events[i]
if event.Type != "tool_completed" || stringValue(event.Payload["toolName"]) != "model_list" {
continue
}
var query struct {
Mode string `json:"mode"`
ReferenceNodeIDs []string `json:"referenceNodeIds"`
}
if err := json.Unmarshal([]byte(stringValue(event.Payload["arguments"])), &query); err != nil || query.Mode != a.Mode || !sameStringSlice(query.ReferenceNodeIDs, a.ReferenceNodeIDs) {
continue
}
result, ok := event.Payload["result"].(map[string]any)
if !ok {
return false
}
models, ok := result["models"].([]any)
if !ok {
return false
}
var selection map[string]string
for _, item := range models {
modelItem, ok := item.(map[string]any)
if !ok || normalizeCapability(stringValue(modelItem["capability"])) != normalizeCapability(a.Mode) {
continue
}
candidate, ok := modelItem["selection"].(map[string]any)
if !ok {
continue
}
candidateSelection := map[string]string{}
if logical := strings.TrimSpace(stringValue(candidate["logicalModelId"])); logical != "" {
candidateSelection["logicalModelId"] = logical
} else if channelID := strings.TrimSpace(stringValue(candidate["channelId"])); channelID != "" {
if modelKey := strings.TrimSpace(stringValue(candidate["channelModelKey"])); modelKey != "" {
candidateSelection["channelId"], candidateSelection["channelModelKey"] = channelID, modelKey
}
}
if len(candidateSelection) == 0 {
continue
}
if selection != nil {
return false
}
selection = candidateSelection
}
if len(selection) == 0 {
return false
}
a.LogicalModelID, a.ChannelID, a.ChannelModelKey = selection["logicalModelId"], selection["channelId"], selection["channelModelKey"]
return true
}
return false
}
func cloudAgentModelSelectionProvided(raw string) bool {
var fields map[string]json.RawMessage
if err := json.Unmarshal([]byte(raw), &fields); err != nil || fields == nil {
return true
}
for _, field := range []string{"logicalModelId", "channelId", "channelModelKey"} {
if _, ok := fields[field]; ok {
return true
}
}
return false
}
func sameStringSlice(left, right []string) bool {
if len(left) != len(right) {
return false
}
for i := range left {
if left[i] != right[i] {
return false
}
}
return true
}
func validateCloudAgentModelSelection(raw string, a cloudAgentMediaArgs) error {
// Go's JSON decoder accepts null for string fields; the tool contract does
// not. Check presence/type before interpreting the decoded selection.
var fields map[string]json.RawMessage
if err := json.Unmarshal([]byte(raw), &fields); err != nil {
return cloudAgentJSONArgumentError(err)
}
for _, field := range []string{"logicalModelId", "channelId", "channelModelKey"} {
if value, exists := fields[field]; exists {
var text *string
if err := json.Unmarshal(value, &text); err != nil || text == nil {
return cloudAgentFieldError(field, "type_mismatch", "模型选择字段必须是字符串,不能为 null")
}
if *text != "" && strings.TrimSpace(*text) == "" {
return cloudAgentFieldError(field, "invalid_value", "模型选择字段不能仅包含空白字符")
}
}
}
switch {
case a.LogicalModelID != "" && (a.ChannelID != "" || a.ChannelModelKey != ""):
return cloudAgentFieldError("logicalModelId", "mutually_exclusive", "模型选择冲突:logicalModelId 与 channelId/channelModelKey 不得混用,请复制 model_list 的一种 selection")
case a.LogicalModelID != "":
return nil
case a.ChannelID == "" && a.ChannelModelKey == "":
return cloudAgentFieldError("logicalModelId", "required", "缺少模型选择:请复制 model_list 的 logicalModelId 或完整的 channelId/channelModelKey;媒体生成必须显式指定模型,不会使用项目默认模型或随机选模")
case a.ChannelID == "":
return cloudAgentFieldError("channelId", "required", "系统渠道模型选择不完整:缺少 channelId,请复制 model_list 的完整 selection")
case a.ChannelModelKey == "":
return cloudAgentFieldError("channelModelKey", "required", "系统渠道模型选择不完整:缺少 channelModelKey,请复制 model_list 的完整 selection")
default:
return nil
}
}