package runner import ( "context" "fmt" "reflect" "strings" scriptengine "github.com/easyai/easyai-ai-gateway/apps/api/internal/script" "github.com/easyai/easyai-ai-gateway/apps/api/internal/store" ) func (s *Service) preprocessRequestWithScripts(ctx context.Context, kind string, body map[string]any, candidate store.RuntimeModelCandidate) parameterPreprocessResult { if platformConfigBool(candidate.PlatformConfig, "skipParamNormalization", "skip_param_normalization") { modelType := strings.TrimSpace(candidate.ModelType) if modelType == "" { modelType = modelTypeFromKind(kind, body) } input := cloneMap(body) return parameterPreprocessResult{ Body: cloneMap(body), Log: parameterPreprocessingLog{ ModelType: modelType, Input: input, Output: cloneMap(body), Changed: false, Changes: []parameterPreprocessChange{}, Model: preprocessingModelSnapshot(candidate), }, } } result := preprocessRequestWithLog(kind, body, candidate) if result.Err != nil { return result } scriptText := platformConfigString(candidate.PlatformConfig, "customPreprocessScript", "custom_preprocess_script") if strings.TrimSpace(scriptText) == "" || s.scriptExecutor == nil { return result } before := cloneMap(result.Body) scriptContext := s.scriptContext(candidate, result.Log.ModelType, nil, map[string]any{ "modelCapability": effectiveModelCapability(candidate), "platformModel": result.Log.Model, "platform": candidate.PlatformConfig, }) out, err := s.scriptExecutor.Execute(ctx, scriptengine.Options{ Script: scriptText, Args: []any{cloneMap(result.Body), result.Log.ModelType, scriptContext}, ContextData: scriptContext, ScriptName: "custom_preprocess_script:" + result.Log.ModelType, PreferredEntryNames: []string{"preprocessParams", "preprocess", "main", "handler"}, Timeout: scriptengine.PreprocessTimeout, }) if err != nil { result.Log.recordScriptChange("CustomPreprocessScript", "error", "$", before, result.Body, err.Error()) result.Log.Output = cloneMap(result.Body) result.Log.Changed = len(result.Log.Changes) > 0 result.Err = err return result } rewritten, ok := out.(map[string]any) if !ok || rewritten == nil { result.Log.Output = cloneMap(result.Body) result.Log.Changed = len(result.Log.Changes) > 0 return result } merged := cloneMap(result.Body) for key, value := range rewritten { merged[key] = value } if !mapsEqual(before, merged) { result.Log.recordScriptChange("CustomPreprocessScript", "rewrite", "$", before, merged, "platform custom preprocess script returned parameter updates") } result.Body = merged result.Log.Output = cloneMap(merged) result.Log.Changed = len(result.Log.Changes) > 0 return result } func (s *Service) scriptContext(candidate store.RuntimeModelCandidate, modelType string, payload map[string]any, extra map[string]any) map[string]any { getTaskURL := platformConfigString(candidate.PlatformConfig, "getTaskURL", "get_task_url") baseURL := strings.TrimRight(strings.TrimSpace(candidate.BaseURL), "/") env := cloneMap(candidate.PlatformConfig) context := map[string]any{ "__easyaiScriptContext": true, "baseURL": baseURL, "getTaskURL": getTaskURL, "authValues": cloneMap(candidate.Credentials), "headers": map[string]any{}, "payload": cloneMap(payload), "type": modelType, "options": map[string]any{ "model": candidate.ModelName, "providerModelName": candidate.ProviderModelName, "platformId": candidate.PlatformID, "platformModelId": candidate.PlatformModelID, "canonicalModelKey": candidate.CanonicalModelKey, "sourceProviderCode": candidate.Provider, }, "env": env, "candidate": preprocessingModelSnapshot(candidate), } context["createRequestURL"] = func(path string, base ...string) string { selectedBase := baseURL if len(base) > 0 && strings.TrimSpace(base[0]) != "" { selectedBase = strings.TrimRight(strings.TrimSpace(base[0]), "/") } if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") { return path } return selectedBase + "/" + strings.TrimLeft(path, "/") } context["creatRequestURL"] = context["createRequestURL"] context["resolveGetTaskURL"] = func(taskID string) string { return resolveTaskURLTemplate(getTaskURL, taskID, "") } for key, value := range extra { context[key] = value } return context } func preprocessingModelSnapshot(candidate store.RuntimeModelCandidate) map[string]any { return map[string]any{ "modelName": candidate.ModelName, "modelAlias": candidate.ModelAlias, "providerModelName": candidate.ProviderModelName, "provider": candidate.Provider, "platformId": candidate.PlatformID, "platformModelId": candidate.PlatformModelID, "capabilities": cloneMap(candidate.Capabilities), } } func (log *parameterPreprocessingLog) recordScriptChange(processor string, action string, path string, before any, after any, reason string) { if log == nil { return } log.Changes = append(log.Changes, parameterPreprocessChange{ Processor: processor, Action: action, Path: path, Before: cloneAny(before), After: cloneAny(after), Reason: reason, }) } func platformConfigString(config map[string]any, keys ...string) string { for _, key := range keys { if value := strings.TrimSpace(fmt.Sprint(config[key])); value != "" && value != "" { return value } } return "" } func platformConfigBool(config map[string]any, keys ...string) bool { for _, key := range keys { switch value := config[key].(type) { case bool: return value case string: return strings.EqualFold(strings.TrimSpace(value), "true") } } return false } func resolveTaskURLTemplate(template string, upstreamTaskID string, taskID string) string { out := strings.TrimSpace(template) replacements := [][2]string{ {"${upstream_task_id}", upstreamTaskID}, {"{{upstream_task_id}}", upstreamTaskID}, {"{upstream_task_id}", upstreamTaskID}, {"${task_id}", taskID}, {"{{task_id}}", taskID}, {"{task_id}", taskID}, {"${taskId}", upstreamTaskID}, {"${taskID}", upstreamTaskID}, {"{{taskId}}", upstreamTaskID}, {"{{taskID}}", upstreamTaskID}, {"{taskId}", upstreamTaskID}, {"{taskID}", upstreamTaskID}, } for _, replacement := range replacements { out = strings.ReplaceAll(out, replacement[0], replacement[1]) } return out } func mapsEqual(left map[string]any, right map[string]any) bool { return reflect.DeepEqual(left, right) }