Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

345 lines
10 KiB
Go

package agentruntime
import (
"encoding/json"
"regexp"
"strings"
)
var acpModelConsumptionDescriptionPattern = regexp.MustCompile(
`(?i)^\s*(?:x\s*([0-9]+(?:\.[0-9]+)?)|([0-9]+(?:\.[0-9]+)?)\s*x)\s*credits?\s*$`,
)
const StandardACPModelDescriptionMetadataFormatCreditConsumptionMultiplierV1 = "credit-consumption-multiplier-v1"
type acpModelInfo struct {
Description string `json:"description"`
ModelID string `json:"modelId"`
Name string `json:"name"`
SupportsReasoningEffort json.RawMessage `json:"supportsReasoningEffort"`
ReasoningEffort json.RawMessage `json:"reasoningEffort"`
ReasoningEfforts json.RawMessage `json:"reasoningEfforts"`
SupportsImageInput json.RawMessage `json:"supportsImageInput"`
Meta json.RawMessage `json:"_meta"`
}
func applyACPModelsResult(state *acpLiveState, raw json.RawMessage, descriptionMetadataFormat string) {
if state == nil || len(raw) == 0 {
return
}
var payload struct {
Models *struct {
AvailableModels []acpModelInfo `json:"availableModels"`
CurrentModelID string `json:"currentModelId"`
} `json:"models"`
}
if err := json.Unmarshal(raw, &payload); err != nil || payload.Models == nil {
return
}
state.modelsAPI = true
options := make([]any, 0, len(payload.Models.AvailableModels))
for _, model := range payload.Models.AvailableModels {
modelID := strings.TrimSpace(model.ModelID)
if modelID == "" {
continue
}
label := strings.TrimSpace(model.Name)
if label == "" {
label = modelID
}
option := map[string]any{"value": modelID, "label": label}
description, consumptionMultiplier := normalizeACPModelDescription(model.Description, descriptionMetadataFormat)
if description != "" {
option["description"] = description
}
if consumptionMultiplier != "" {
option["consumptionMultiplier"] = consumptionMultiplier
}
applyACPModelMetadata(option, model)
options = append(options, option)
}
if len(options) == 0 {
return
}
descriptor := map[string]any{
"id": "model",
"name": "Model",
"currentValue": strings.TrimSpace(payload.Models.CurrentModelID),
"options": options,
}
descriptors := cloneConfigOptionDescriptors(state.configOptionDescriptors)
replaced := false
for index := range descriptors {
if strings.TrimSpace(asString(descriptors[index]["id"])) == "model" {
descriptors[index] = descriptor
replaced = true
break
}
}
if !replaced {
descriptors = append(descriptors, descriptor)
}
applyACPConfigOptionDescriptors(state, descriptors)
}
// normalizeACPModelDescription converts the standalone credit multiplier into
// typed model metadata only when a verified Extension explicitly declares the
// closed format. Shared catalog and GUI layers must not infer this semantic
// from provider-owned presentation text.
func normalizeACPModelDescription(description string, metadataFormat string) (string, string) {
original := strings.TrimSpace(description)
if strings.TrimSpace(metadataFormat) != StandardACPModelDescriptionMetadataFormatCreditConsumptionMultiplierV1 {
return original, ""
}
parts := strings.FieldsFunc(description, func(value rune) bool {
return value == '·' || value == '•' || value == '\n' || value == '\r'
})
kept := make([]string, 0, len(parts))
consumptionMultiplier := ""
foundConsumption := false
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
if matches := acpModelConsumptionDescriptionPattern.FindStringSubmatch(part); len(matches) > 0 {
foundConsumption = true
if consumptionMultiplier == "" {
consumptionMultiplier = firstNonEmptyString(matches[1], matches[2])
}
continue
}
kept = append(kept, part)
}
if !foundConsumption {
return original, ""
}
return strings.Join(kept, " · "), consumptionMultiplier
}
func normalizeACPModelConfigOptionDescriptions(
descriptors []map[string]any,
modelConfigOptionID string,
metadataFormat string,
) {
modelConfigOptionID = strings.TrimSpace(modelConfigOptionID)
for _, descriptor := range descriptors {
options, ok := descriptor["options"].([]any)
if !ok {
continue
}
for _, rawOption := range options {
option, ok := rawOption.(map[string]any)
if !ok {
continue
}
// This field is an extension-owned semantic. Never allow a raw ACP
// provider payload to smuggle it through the shared runtime context;
// it may only be re-added below after the declared adapter format has
// parsed the description.
delete(option, "consumptionMultiplier")
if strings.TrimSpace(asString(descriptor["id"])) != modelConfigOptionID ||
strings.TrimSpace(metadataFormat) != StandardACPModelDescriptionMetadataFormatCreditConsumptionMultiplierV1 {
continue
}
description, consumptionMultiplier := normalizeACPModelDescription(
asString(option["description"]),
metadataFormat,
)
if description == "" {
delete(option, "description")
} else {
option["description"] = description
}
if consumptionMultiplier != "" {
option["consumptionMultiplier"] = consumptionMultiplier
}
}
}
}
func applyACPModelMetadata(option map[string]any, model acpModelInfo) {
metadata := rawJSONObject(model.Meta)
supportsReasoning := rawJSONBool(model.SupportsReasoningEffort)
if supportsReasoning == nil {
supportsReasoning = rawJSONBool(metadata["supportsReasoningEffort"])
}
if supportsReasoning != nil {
option["supportsReasoningEffort"] = *supportsReasoning
}
reasoningEffort := rawJSONString(model.ReasoningEffort)
if reasoningEffort == "" {
reasoningEffort = rawJSONString(metadata["reasoningEffort"])
}
if reasoningEffort != "" {
option["reasoningEffort"] = reasoningEffort
}
reasoningEfforts, advertised := normalizeACPReasoningEfforts(model.ReasoningEfforts)
if !advertised {
reasoningEfforts, advertised = normalizeACPReasoningEfforts(metadata["reasoningEfforts"])
}
if advertised {
option["reasoningEfforts"] = reasoningEfforts
}
supportsImage := rawJSONBool(model.SupportsImageInput)
if supportsImage == nil {
supportsImage = rawJSONBool(metadata["supportsImageInput"])
}
if supportsImage != nil {
option["supportsImageInput"] = *supportsImage
}
}
func normalizeACPReasoningEfforts(raw json.RawMessage) ([]any, bool) {
if len(raw) == 0 || string(raw) == "null" {
return nil, false
}
var values []any
if json.Unmarshal(raw, &values) != nil {
return nil, false
}
result := make([]any, 0, len(values))
seen := map[string]struct{}{}
for _, item := range values {
entry := map[string]any{}
value := ""
switch valueOrObject := item.(type) {
case string:
value = strings.TrimSpace(valueOrObject)
case map[string]any:
value = strings.TrimSpace(firstNonEmptyString(asString(valueOrObject["value"]), asString(valueOrObject["id"])))
if label := strings.TrimSpace(firstNonEmptyString(asString(valueOrObject["label"]), asString(valueOrObject["name"]))); label != "" {
entry["label"] = label
}
if description := strings.TrimSpace(asString(valueOrObject["description"])); description != "" {
entry["description"] = description
}
if isDefault, ok := valueOrObject["default"].(bool); ok {
entry["default"] = isDefault
}
}
if value == "" {
continue
}
if _, exists := seen[value]; exists {
continue
}
seen[value] = struct{}{}
entry["value"] = value
result = append(result, entry)
}
return result, true
}
func rawJSONObject(raw json.RawMessage) map[string]json.RawMessage {
if len(raw) == 0 {
return nil
}
var value map[string]json.RawMessage
if json.Unmarshal(raw, &value) != nil {
return nil
}
return value
}
func rawJSONBool(raw json.RawMessage) *bool {
if len(raw) == 0 {
return nil
}
var value bool
if json.Unmarshal(raw, &value) != nil {
return nil
}
return &value
}
func rawJSONString(raw json.RawMessage) string {
if len(raw) == 0 {
return ""
}
var value string
if json.Unmarshal(raw, &value) != nil {
return ""
}
return strings.TrimSpace(value)
}
func acpModelReasoningEffort(state acpLiveState, modelID string, requested string) (string, bool) {
modelID = strings.TrimSpace(modelID)
requested = strings.TrimSpace(requested)
for _, descriptor := range state.configOptionDescriptors {
if strings.TrimSpace(asString(descriptor["id"])) != "model" {
continue
}
for _, model := range configOptionEntries(descriptor["options"]) {
if strings.TrimSpace(asString(model["value"])) != modelID {
continue
}
supported, supportKnown := model["supportsReasoningEffort"].(bool)
_, effortsAdvertised := model["reasoningEfforts"]
defaultEffort := strings.TrimSpace(asString(model["reasoningEffort"]))
if supportKnown && !supported {
return "", true
}
if !supportKnown && !effortsAdvertised && defaultEffort == "" {
return "", false
}
first := ""
defaultSupported := false
for _, effort := range configOptionEntries(model["reasoningEfforts"]) {
value := strings.TrimSpace(firstNonEmptyString(asString(effort["value"]), asString(effort["id"])))
if value == "" {
continue
}
if first == "" {
first = value
}
if value == requested {
return requested, true
}
if defaultEffort == "" && effort["default"] == true {
defaultEffort = value
}
if value == defaultEffort {
defaultSupported = true
}
}
if defaultSupported {
return defaultEffort, true
}
return first, true
}
}
return "", false
}
func (a *standardACPAdapter) sessionModelReasoningEffort(
agentSessionID string,
modelID string,
requested string,
) (string, bool) {
if a == nil {
return "", false
}
a.mu.Lock()
defer a.mu.Unlock()
session := a.sessions[strings.TrimSpace(agentSessionID)]
if session == nil {
return "", false
}
return acpModelReasoningEffort(session.acpLiveState, modelID, requested)
}
func (a *standardACPAdapter) sessionCurrentModelID(agentSessionID string) string {
if a == nil {
return ""
}
a.mu.Lock()
defer a.mu.Unlock()
session := a.sessions[strings.TrimSpace(agentSessionID)]
if session == nil {
return ""
}
return strings.TrimSpace(asString(session.configOptions["model"]))
}