Files
cursor-byok/internal/client/model_adapter_benchmark.go
T
2026-07-10 11:38:45 +08:00

772 lines
26 KiB
Go

package client
import (
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"hash/fnv"
"io"
"math"
"net/http"
"sort"
"strconv"
"strings"
"sync"
"time"
"unicode/utf8"
modeladapter "cursor/internal/backend/agent/model"
serverconfig "cursor/internal/backend/server/config"
"cursor/internal/modelchannel"
"github.com/wailsapp/wails/v3/pkg/application"
)
const (
modelAdapterTestUpdatedEvent = "model-adapter-test:updated"
modelAdapterTestPrompt = "Output the numbers 1 through 120 separated by a single space. No commas, no newlines, no explanation."
modelAdapterTestTimeout = 45 * time.Second
modelAdapterTestDefaultMaxTokens = 65_536
modelAdapterTestEmptyTextError = "未收到文本输出,无法计算测速结果"
modelAdapterTestMaxErrorBodyBytes = 8192
)
type ModelAdapterTestStatus string
const (
ModelAdapterTestStatusIdle ModelAdapterTestStatus = "idle"
ModelAdapterTestStatusRunning ModelAdapterTestStatus = "running"
ModelAdapterTestStatusSuccess ModelAdapterTestStatus = "success"
ModelAdapterTestStatusError ModelAdapterTestStatus = "error"
)
// ModelAdapterTestResult 表示一次模型测速结果。
type ModelAdapterTestResult struct {
AdapterID string `json:"adapterID"`
RequestHash string `json:"requestHash"`
Status string `json:"status"`
TokensPerSecond float64 `json:"tokensPerSecond"`
FirstTextTokenMS int64 `json:"firstTextTokenMS"`
TotalDurationMS int64 `json:"totalDurationMS"`
OutputTokens int64 `json:"outputTokens"`
TokensEstimated bool `json:"tokensEstimated"`
SummaryText string `json:"summaryText"`
Error string `json:"error"`
RawResponse string `json:"rawResponse"`
TestedAt string `json:"testedAt"`
}
// ModelAdapterTestResultsPayload 用于向前端广播当前测速结果快照。
type ModelAdapterTestResultsPayload struct {
Results []ModelAdapterTestResult `json:"results"`
}
type modelAdapterTestMetrics struct {
firstTextTokenAt time.Time
finishedAt time.Time
outputTokens int64
outputProvided bool
text strings.Builder
rawResponse string
}
type modelAdapterTestArtifactObserver struct {
mu sync.Mutex
response strings.Builder
}
func (observer *modelAdapterTestArtifactObserver) RecordLLMRequest(string, string, string, map[string]any) (string, error) {
return "", nil
}
func (observer *modelAdapterTestArtifactObserver) AppendLLMResponseChunk(_ string, _ string, _ string, chunk string) (string, error) {
if observer == nil {
return "", nil
}
observer.mu.Lock()
defer observer.mu.Unlock()
_, _ = observer.response.WriteString(chunk)
return "", nil
}
func (observer *modelAdapterTestArtifactObserver) RecordLLMSummary(string, string, string, map[string]any) (string, error) {
return "", nil
}
func (observer *modelAdapterTestArtifactObserver) RawResponse() string {
if observer == nil {
return ""
}
observer.mu.Lock()
defer observer.mu.Unlock()
return strings.TrimSpace(observer.response.String())
}
func (s *ProxyService) GetModelAdapterTestResults() []ModelAdapterTestResult {
return s.snapshotModelAdapterTestResults()
}
func (s *ProxyService) TestModelAdapter(adapter serverconfig.ModelAdapterConfig) (ModelAdapterTestResult, error) {
requestHash := buildModelAdapterTestRequestHash(adapter)
adapterID := buildModelAdapterTestCacheKey(adapter, requestHash)
if cached, ok := s.getRunningModelAdapterTestResult(adapterID, requestHash); ok {
return cached, nil
}
normalized, err := normalizeSingleModelAdapterConfig(adapter)
if err != nil {
result := ModelAdapterTestResult{
AdapterID: adapterID,
RequestHash: requestHash,
Status: string(ModelAdapterTestStatusError),
SummaryText: buildModelAdapterTestErrorSummary(err),
Error: buildModelAdapterTestErrorSummary(err),
RawResponse: strings.TrimSpace(modelAdapterTestErrorMessage(err)),
TestedAt: time.Now().UTC().Format(time.RFC3339Nano),
}
s.storeAndEmitModelAdapterTestResult(result)
return result, err
}
running := ModelAdapterTestResult{
AdapterID: normalized.ID,
RequestHash: requestHash,
Status: string(ModelAdapterTestStatusRunning),
SummaryText: "测试中...",
TestedAt: time.Now().UTC().Format(time.RFC3339Nano),
}
s.storeAndEmitModelAdapterTestResult(running)
result, testErr := s.runModelAdapterTest(normalized, requestHash)
s.storeAndEmitModelAdapterTestResult(result)
if testErr != nil {
return result, testErr
}
return result, nil
}
func normalizeSingleModelAdapterConfig(adapter serverconfig.ModelAdapterConfig) (serverconfig.ModelAdapterConfig, error) {
normalized, err := serverconfig.NormalizeModelAdapterConfigs([]serverconfig.ModelAdapterConfig{adapter})
if err != nil {
return serverconfig.ModelAdapterConfig{}, err
}
if len(normalized) == 0 {
return serverconfig.ModelAdapterConfig{}, errors.New("模型配置不能为空")
}
return normalized[0], nil
}
func (s *ProxyService) runModelAdapterTest(adapter serverconfig.ModelAdapterConfig, requestHash string) (ModelAdapterTestResult, error) {
ctx, cancel := context.WithTimeout(context.Background(), modelAdapterTestTimeout)
defer cancel()
startedAt := time.Now().UTC()
metrics, requestErr := s.executeModelAdapterNonStreamingTest(ctx, adapter)
if requestErr != nil {
result := buildErroredModelAdapterTestResult(adapter.ID, requestHash, requestErr)
return result, requestErr
}
if metrics.finishedAt.IsZero() {
metrics.finishedAt = time.Now().UTC()
}
if metrics.firstTextTokenAt.IsZero() {
emptyTextErr := errors.New(modelAdapterTestEmptyTextError)
result := buildErroredModelAdapterTestResult(adapter.ID, requestHash, emptyTextErr)
return result, emptyTextErr
}
outputTokens := metrics.outputTokens
tokensEstimated := false
if !metrics.outputProvided || outputTokens <= 0 {
outputTokens = estimateBenchmarkTextTokens(metrics.text.String())
tokensEstimated = true
}
firstTextTokenMS := metrics.firstTextTokenAt.Sub(startedAt).Milliseconds()
if firstTextTokenMS < 0 {
firstTextTokenMS = 0
}
totalDurationMS := metrics.finishedAt.Sub(startedAt).Milliseconds()
if totalDurationMS < 0 {
totalDurationMS = 0
}
tokensPerSecond := 0.0
totalDuration := metrics.finishedAt.Sub(startedAt)
if outputTokens > 0 && totalDuration > 0 {
tokensPerSecond = float64(outputTokens) / totalDuration.Seconds()
}
result := ModelAdapterTestResult{
AdapterID: adapter.ID,
RequestHash: requestHash,
Status: string(ModelAdapterTestStatusSuccess),
TokensPerSecond: tokensPerSecond,
FirstTextTokenMS: firstTextTokenMS,
TotalDurationMS: totalDurationMS,
OutputTokens: outputTokens,
TokensEstimated: tokensEstimated,
TestedAt: time.Now().UTC().Format(time.RFC3339Nano),
RawResponse: strings.TrimSpace(metrics.rawResponse),
}
result.SummaryText = buildModelAdapterTestSummaryText(result)
return result, nil
}
func (s *ProxyService) executeModelAdapterNonStreamingTest(ctx context.Context, adapter serverconfig.ModelAdapterConfig) (*modelAdapterTestMetrics, error) {
switch strings.TrimSpace(adapter.Type) {
case "openai":
return s.executeOpenAIStreamingTest(ctx, adapter)
case "anthropic":
return s.executeAnthropicStreamingTest(ctx, adapter)
default:
return nil, fmt.Errorf("unsupported provider %q", strings.TrimSpace(adapter.Type))
}
}
func (s *ProxyService) executeOpenAIStreamingTest(ctx context.Context, adapter serverconfig.ModelAdapterConfig) (*modelAdapterTestMetrics, error) {
_ = s
metrics := &modelAdapterTestMetrics{}
observer := &modelAdapterTestArtifactObserver{}
maxTokens := modelAdapterTestConfiguredOpenAIMaxTokens(adapter)
requestID := "model-adapter-test-" + buildModelAdapterTestRequestHash(adapter)
req := modeladapter.StreamRequest{
RequestID: requestID,
RunID: requestID,
ModelCallID: requestID,
ModelID: strings.TrimSpace(adapter.ID),
Provider: "openai",
BaseURL: strings.TrimSpace(adapter.BaseURL),
APIKey: strings.TrimSpace(adapter.APIKey),
ProviderModelID: strings.TrimSpace(adapter.ModelID),
ResolvedChannelID: strings.TrimSpace(adapter.ID),
ResolvedChannelName: strings.TrimSpace(adapter.DisplayName),
ResolvedContextWindowTokens: adapter.ContextWindowTokens,
ReasoningEffort: strings.TrimSpace(adapter.ReasoningEffort),
OpenAIEndpoint: strings.TrimSpace(adapter.OpenAIEndpoint),
OpenAIExtraParamsEnabled: adapter.OpenAIExtraParamsEnabled,
OpenAIExtraParamsJSON: strings.TrimSpace(adapter.OpenAIExtraParamsJSON),
CustomHeadersEnabled: adapter.CustomHeadersEnabled,
CustomHeadersJSON: strings.TrimSpace(adapter.CustomHeadersJSON),
Messages: []modeladapter.Message{{Role: "user", Content: modelAdapterTestPrompt}},
MaxTokens: maxTokens,
Stream: true,
RequestKnobs: map[string]any{"stream": true, "max_tokens": maxTokens},
Observer: observer,
ProviderStreamIdleTimeout: modelAdapterTestTimeout,
}
err := modeladapter.NewOpenAIAdapter().Stream(ctx, req, func(event modeladapter.ModelEvent) error {
now := time.Now().UTC()
switch event.Kind {
case modeladapter.ModelEventKindTextDelta:
if strings.TrimSpace(event.Text) != "" && metrics.firstTextTokenAt.IsZero() {
metrics.firstTextTokenAt = now
}
_, _ = metrics.text.WriteString(event.Text)
case modeladapter.ModelEventKindTurnFinished:
metrics.finishedAt = now
if event.OutputTokens > 0 {
metrics.outputTokens = event.OutputTokens
metrics.outputProvided = true
}
case modeladapter.ModelEventKindProviderError:
if event.Err != nil {
return event.Err
}
return errors.New("provider error")
}
return nil
})
if err != nil {
return nil, err
}
if metrics.finishedAt.IsZero() {
metrics.finishedAt = time.Now().UTC()
}
metrics.rawResponse = observer.RawResponse()
if strings.TrimSpace(metrics.rawResponse) == "" {
metrics.rawResponse = strings.TrimSpace(metrics.text.String())
}
return metrics, nil
}
func (s *ProxyService) executeAnthropicStreamingTest(ctx context.Context, adapter serverconfig.ModelAdapterConfig) (*modelAdapterTestMetrics, error) {
_ = s
metrics := &modelAdapterTestMetrics{}
observer := &modelAdapterTestArtifactObserver{}
maxTokens := modelAdapterTestConfiguredAnthropicMaxTokens(adapter)
thinkingEffort := normalizeModelAdapterTestAnthropicThinkingEffort(adapter.AnthropicThinkingEffort)
requestID := "model-adapter-test-" + buildModelAdapterTestRequestHash(adapter)
req := modeladapter.StreamRequest{
RequestID: requestID,
RunID: requestID,
ModelCallID: requestID,
ModelID: strings.TrimSpace(adapter.ID),
Provider: "anthropic",
BaseURL: strings.TrimSpace(adapter.BaseURL),
APIKey: strings.TrimSpace(adapter.APIKey),
ProviderModelID: strings.TrimSpace(adapter.ModelID),
ResolvedChannelID: strings.TrimSpace(adapter.ID),
ResolvedChannelName: strings.TrimSpace(adapter.DisplayName),
ResolvedContextWindowTokens: adapter.ContextWindowTokens,
ThinkingEffort: thinkingEffort,
AnthropicMaxTokens: maxTokens,
AnthropicThinkingEffort: thinkingEffort,
CustomHeadersEnabled: adapter.CustomHeadersEnabled,
CustomHeadersJSON: strings.TrimSpace(adapter.CustomHeadersJSON),
AnthropicExtraParamsEnabled: adapter.AnthropicExtraParamsEnabled,
AnthropicExtraParamsJSON: strings.TrimSpace(adapter.AnthropicExtraParamsJSON),
ThinkingBudgetTokens: adapter.ThinkingBudgetTokens,
Messages: []modeladapter.Message{{Role: "user", Content: modelAdapterTestPrompt}},
MaxTokens: maxTokens,
Stream: true,
RequestKnobs: map[string]any{"stream": true, "anthropic_max_tokens": maxTokens, "max_tokens": maxTokens},
Observer: observer,
ProviderStreamIdleTimeout: modelAdapterTestTimeout,
}
err := modeladapter.NewAnthropicAdapter().Stream(ctx, req, func(event modeladapter.ModelEvent) error {
now := time.Now().UTC()
switch event.Kind {
case modeladapter.ModelEventKindTextDelta:
if strings.TrimSpace(event.Text) != "" && metrics.firstTextTokenAt.IsZero() {
metrics.firstTextTokenAt = now
}
_, _ = metrics.text.WriteString(event.Text)
case modeladapter.ModelEventKindTurnFinished:
metrics.finishedAt = now
if event.OutputTokens > 0 {
metrics.outputTokens = event.OutputTokens
metrics.outputProvided = true
}
case modeladapter.ModelEventKindProviderError:
if event.Err != nil {
return event.Err
}
return errors.New("provider error")
}
return nil
})
if err != nil {
return nil, err
}
if metrics.finishedAt.IsZero() {
metrics.finishedAt = time.Now().UTC()
}
metrics.rawResponse = observer.RawResponse()
if strings.TrimSpace(metrics.rawResponse) == "" {
metrics.rawResponse = strings.TrimSpace(metrics.text.String())
}
return metrics, nil
}
func (s *ProxyService) getRunningModelAdapterTestResult(adapterID string, requestHash string) (ModelAdapterTestResult, bool) {
s.modelTestMu.RLock()
defer s.modelTestMu.RUnlock()
if s.modelTestResults == nil {
return ModelAdapterTestResult{}, false
}
result, ok := s.modelTestResults[adapterID]
if !ok {
return ModelAdapterTestResult{}, false
}
if strings.TrimSpace(result.Status) != string(ModelAdapterTestStatusRunning) {
return ModelAdapterTestResult{}, false
}
if strings.TrimSpace(result.RequestHash) != strings.TrimSpace(requestHash) {
return ModelAdapterTestResult{}, false
}
return result, true
}
func (s *ProxyService) storeAndEmitModelAdapterTestResult(result ModelAdapterTestResult) {
if strings.TrimSpace(result.AdapterID) == "" {
return
}
s.modelTestMu.Lock()
if s.modelTestResults == nil {
s.modelTestResults = make(map[string]ModelAdapterTestResult)
}
s.modelTestResults[result.AdapterID] = result
snapshot := snapshotModelAdapterTestResultsLocked(s.modelTestResults)
s.modelTestMu.Unlock()
s.emitModelAdapterTestResults(snapshot)
}
func (s *ProxyService) snapshotModelAdapterTestResults() []ModelAdapterTestResult {
s.modelTestMu.RLock()
defer s.modelTestMu.RUnlock()
return snapshotModelAdapterTestResultsLocked(s.modelTestResults)
}
func snapshotModelAdapterTestResultsLocked(items map[string]ModelAdapterTestResult) []ModelAdapterTestResult {
if len(items) == 0 {
return []ModelAdapterTestResult{}
}
results := make([]ModelAdapterTestResult, 0, len(items))
for _, item := range items {
results = append(results, item)
}
sort.Slice(results, func(i int, j int) bool {
if results[i].TestedAt == results[j].TestedAt {
return results[i].AdapterID < results[j].AdapterID
}
return results[i].TestedAt > results[j].TestedAt
})
return results
}
func (s *ProxyService) emitModelAdapterTestResults(results []ModelAdapterTestResult) {
app := application.Get()
if app == nil {
return
}
app.Event.Emit(modelAdapterTestUpdatedEvent, ModelAdapterTestResultsPayload{
Results: results,
})
}
func buildErroredModelAdapterTestResult(adapterID string, requestHash string, err error) ModelAdapterTestResult {
message := strings.TrimSpace(modelAdapterTestErrorMessage(err))
summary := buildModelAdapterTestErrorSummary(err)
return ModelAdapterTestResult{
AdapterID: strings.TrimSpace(adapterID),
RequestHash: strings.TrimSpace(requestHash),
Status: string(ModelAdapterTestStatusError),
SummaryText: summary,
Error: summary,
RawResponse: message,
TestedAt: time.Now().UTC().Format(time.RFC3339Nano),
}
}
func buildModelAdapterTestSummaryText(result ModelAdapterTestResult) string {
if strings.TrimSpace(result.Status) != string(ModelAdapterTestStatusSuccess) {
return firstNonEmptyTrimmed(result.SummaryText, "测试失败")
}
return fmt.Sprintf("%d t/s | 首字 %s", int(math.Round(maxFloat64(result.TokensPerSecond, 0))), formatModelAdapterTestDuration(result.FirstTextTokenMS))
}
func buildModelAdapterHTTPStatusError(prefix string, resp *http.Response) error {
if resp == nil {
return fmt.Errorf("%s response is nil", strings.TrimSpace(prefix))
}
limitedBody, err := io.ReadAll(io.LimitReader(resp.Body, modelAdapterTestMaxErrorBodyBytes))
if err != nil {
if retrySummary := modeladapter.ProviderRetryAttemptSummary(resp); retrySummary != "" {
return fmt.Errorf("%s status=%d %s body_read_error=%v", strings.TrimSpace(prefix), resp.StatusCode, retrySummary, err)
}
return fmt.Errorf("%s status=%d body_read_error=%v", strings.TrimSpace(prefix), resp.StatusCode, err)
}
retrySummary := modeladapter.ProviderRetryAttemptSummary(resp)
bodyText := strings.TrimSpace(string(limitedBody))
if bodyText == "" {
if retrySummary != "" {
return fmt.Errorf("%s status=%d %s", strings.TrimSpace(prefix), resp.StatusCode, retrySummary)
}
return fmt.Errorf("%s status=%d", strings.TrimSpace(prefix), resp.StatusCode)
}
if retrySummary != "" {
return fmt.Errorf("%s status=%d %s body=%s", strings.TrimSpace(prefix), resp.StatusCode, retrySummary, bodyText)
}
return fmt.Errorf("%s status=%d body=%s", strings.TrimSpace(prefix), resp.StatusCode, bodyText)
}
func buildModelAdapterProviderBodyError(prefix string, body []byte) error {
var payload map[string]any
if err := json.Unmarshal(body, &payload); err != nil {
return nil
}
errorValue, ok := payload["error"]
if !ok || errorValue == nil {
return nil
}
message := ""
details := make([]string, 0, 2)
switch value := errorValue.(type) {
case string:
message = strings.TrimSpace(value)
case map[string]any:
message = strings.TrimSpace(fmt.Sprint(value["message"]))
if errorType := strings.TrimSpace(fmt.Sprint(value["type"])); errorType != "" && errorType != "<nil>" {
details = append(details, "type="+errorType)
}
if code := strings.TrimSpace(fmt.Sprint(value["code"])); code != "" && code != "<nil>" {
details = append(details, "code="+code)
}
default:
message = strings.TrimSpace(fmt.Sprint(value))
}
if message == "" || message == "<nil>" {
message = "provider returned error response"
}
summary := strings.TrimSpace(prefix)
if summary == "" {
summary = "model adapter"
}
if len(details) > 0 {
return fmt.Errorf("%s provider error %s: %s", summary, strings.Join(details, " "), message)
}
return fmt.Errorf("%s provider error: %s", summary, message)
}
func modelAdapterTestErrorMessage(err error) string {
if err == nil {
return ""
}
message := strings.TrimSpace(err.Error())
if message == "" {
return "模型测试失败"
}
if errors.Is(err, context.DeadlineExceeded) {
return "模型测试超时,请稍后重试"
}
return message
}
func buildModelAdapterTestErrorSummary(err error) string {
if err == nil {
return "测试失败"
}
if errors.Is(err, context.DeadlineExceeded) {
return "测试超时"
}
message := strings.TrimSpace(err.Error())
switch {
case strings.Contains(message, modelAdapterTestEmptyTextError):
return "无正文返回"
case strings.Contains(strings.ToLower(message), "context canceled"):
return "测试已停止"
default:
return "测试失败"
}
}
func formatModelAdapterTestDuration(durationMS int64) string {
if durationMS < 1000 {
if durationMS < 0 {
durationMS = 0
}
return fmt.Sprintf("%d ms", durationMS)
}
seconds := float64(durationMS) / 1000
return fmt.Sprintf("%.1f s", seconds)
}
func estimateBenchmarkTextTokens(text string) int64 {
trimmed := strings.TrimSpace(text)
if trimmed == "" {
return 0
}
runeCount := utf8.RuneCountInString(trimmed)
if runeCount <= 0 {
return 0
}
estimated := int64((runeCount + 3) / 4)
estimated += int64(strings.Count(trimmed, "\n"))
if estimated < 1 {
return 1
}
return estimated
}
func buildModelAdapterTestCacheKey(adapter serverconfig.ModelAdapterConfig, requestHash string) string {
baseURL, baseURLErr := modelchannel.NormalizeBaseURL(adapter.BaseURL)
if baseURLErr == nil &&
strings.TrimSpace(adapter.DisplayName) != "" &&
strings.TrimSpace(adapter.ModelID) != "" &&
strings.TrimSpace(adapter.APIKey) != "" {
return modelchannel.BuildChannelID(baseURL, adapter.ModelID, adapter.APIKey, adapter.DisplayName, modelchannel.NormalizeOpenAIEndpoint(adapter.Type, adapter.OpenAIEndpoint))
}
return "invalid:" + strings.TrimSpace(requestHash)
}
func buildModelAdapterTestRequestHash(adapter serverconfig.ModelAdapterConfig) string {
source := normalizeModelAdapterTestHashSource(adapter)
payload := strings.Join([]string{
source.Type,
source.BaseURL,
source.APIKey,
source.ModelID,
source.ReasoningEffort,
source.OpenAIEndpoint,
strconv.Itoa(source.OpenAIExtraParamsEnabled),
source.OpenAIExtraParamsJSON,
strconv.Itoa(source.CustomHeadersEnabled),
source.CustomHeadersJSON,
strconv.Itoa(source.AnthropicExtraParamsEnabled),
source.AnthropicExtraParamsJSON,
strconv.Itoa(source.ContextWindowTokens),
strconv.Itoa(source.MaxCompletionTokens),
strconv.Itoa(source.AnthropicMaxTokens),
source.AnthropicThinkingEffort,
}, "\n")
hasher := fnv.New32a()
_, _ = hasher.Write([]byte(payload))
sum := hasher.Sum(nil)
return hex.EncodeToString(sum)
}
type modelAdapterTestHashSource struct {
Type string
BaseURL string
APIKey string
ModelID string
ReasoningEffort string
OpenAIEndpoint string
OpenAIExtraParamsEnabled int
OpenAIExtraParamsJSON string
CustomHeadersEnabled int
CustomHeadersJSON string
AnthropicExtraParamsEnabled int
AnthropicExtraParamsJSON string
ContextWindowTokens int
MaxCompletionTokens int
AnthropicMaxTokens int
AnthropicThinkingEffort string
}
func normalizeModelAdapterTestHashSource(adapter serverconfig.ModelAdapterConfig) modelAdapterTestHashSource {
baseURL := strings.TrimSpace(adapter.BaseURL)
if normalizedBaseURL, err := modelchannel.NormalizeBaseURL(adapter.BaseURL); err == nil {
baseURL = normalizedBaseURL
}
return modelAdapterTestHashSource{
Type: normalizeModelAdapterTestType(adapter.Type),
BaseURL: baseURL,
APIKey: strings.TrimSpace(adapter.APIKey),
ModelID: strings.TrimSpace(adapter.ModelID),
ReasoningEffort: normalizeModelAdapterTestProviderReasoning(adapter),
OpenAIEndpoint: modelchannel.NormalizeOpenAIEndpoint(adapter.Type, adapter.OpenAIEndpoint),
OpenAIExtraParamsEnabled: normalizeModelAdapterTestBool(adapter.Type == "openai" && adapter.OpenAIExtraParamsEnabled),
OpenAIExtraParamsJSON: normalizeModelAdapterTestOpenAIExtraParamsJSON(adapter),
CustomHeadersEnabled: normalizeModelAdapterTestBool(adapter.CustomHeadersEnabled),
CustomHeadersJSON: normalizeModelAdapterTestCustomHeadersJSON(adapter),
AnthropicExtraParamsEnabled: normalizeModelAdapterTestBool(adapter.Type == "anthropic" && adapter.AnthropicExtraParamsEnabled),
AnthropicExtraParamsJSON: normalizeModelAdapterTestAnthropicExtraParamsJSON(adapter),
ContextWindowTokens: normalizeModelAdapterTestInt(adapter.ContextWindowTokens),
MaxCompletionTokens: normalizeModelAdapterTestInt(adapter.MaxCompletionTokens),
AnthropicMaxTokens: normalizeModelAdapterTestInt(adapter.AnthropicMaxTokens),
AnthropicThinkingEffort: normalizeModelAdapterTestProviderAnthropicThinkingEffort(adapter),
}
}
func normalizeModelAdapterTestType(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "anthropic":
return "anthropic"
case "openai":
return "openai"
default:
return strings.ToLower(strings.TrimSpace(value))
}
}
func normalizeModelAdapterTestReasoning(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "low", "medium", "high", "xhigh", "max":
return strings.ToLower(strings.TrimSpace(value))
default:
return "medium"
}
}
func normalizeModelAdapterTestProviderReasoning(adapter serverconfig.ModelAdapterConfig) string {
if normalizeModelAdapterTestType(adapter.Type) != "openai" {
return ""
}
return normalizeModelAdapterTestReasoning(adapter.ReasoningEffort)
}
func normalizeModelAdapterTestAnthropicThinkingEffort(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "low", "medium", "high", "xhigh", "max":
return strings.ToLower(strings.TrimSpace(value))
default:
return "xhigh"
}
}
func normalizeModelAdapterTestProviderAnthropicThinkingEffort(adapter serverconfig.ModelAdapterConfig) string {
if normalizeModelAdapterTestType(adapter.Type) != "anthropic" {
return ""
}
return normalizeModelAdapterTestAnthropicThinkingEffort(adapter.AnthropicThinkingEffort)
}
func modelAdapterTestConfiguredAnthropicMaxTokens(adapter serverconfig.ModelAdapterConfig) int {
if adapter.AnthropicMaxTokens > 0 {
return adapter.AnthropicMaxTokens
}
if adapter.MaxCompletionTokens > 0 {
return adapter.MaxCompletionTokens
}
return modelAdapterTestDefaultMaxTokens
}
func modelAdapterTestConfiguredOpenAIMaxTokens(adapter serverconfig.ModelAdapterConfig) int {
if adapter.MaxCompletionTokens > 0 {
return adapter.MaxCompletionTokens
}
if adapter.AnthropicMaxTokens > 0 {
return adapter.AnthropicMaxTokens
}
return modelAdapterTestDefaultMaxTokens
}
func normalizeModelAdapterTestBool(value bool) int {
if value {
return 1
}
return 0
}
func normalizeModelAdapterTestOpenAIExtraParamsJSON(adapter serverconfig.ModelAdapterConfig) string {
if normalizeModelAdapterTestType(adapter.Type) != "openai" || !adapter.OpenAIExtraParamsEnabled {
return ""
}
return strings.TrimSpace(adapter.OpenAIExtraParamsJSON)
}
func normalizeModelAdapterTestCustomHeadersJSON(adapter serverconfig.ModelAdapterConfig) string {
if !adapter.CustomHeadersEnabled {
return ""
}
return strings.TrimSpace(adapter.CustomHeadersJSON)
}
func normalizeModelAdapterTestAnthropicExtraParamsJSON(adapter serverconfig.ModelAdapterConfig) string {
if normalizeModelAdapterTestType(adapter.Type) != "anthropic" || !adapter.AnthropicExtraParamsEnabled {
return ""
}
return strings.TrimSpace(adapter.AnthropicExtraParamsJSON)
}
func normalizeModelAdapterTestInt(value int) int {
if value <= 0 {
return 0
}
return value
}
func maxFloat64(value float64, fallback float64) float64 {
if value < fallback {
return fallback
}
return value
}
func firstNonEmptyTrimmed(values ...string) string {
for _, value := range values {
trimmed := strings.TrimSpace(value)
if trimmed != "" {
return trimmed
}
}
return ""
}