feat: publish sanitized custom.97 source

This commit is contained in:
cneicy committed 2026-10-27 20:43:39 +08:00
1 parent 8e6ed0317b
commit 86c0276156
1022 files changed
+69903 -19207

No files matched your search

+374 -142
View File
@@ -2,9 +2,10 @@ package jsplugin
import (
"bytes"
"cmp"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"maps"
@@ -14,6 +15,7 @@ import (
"net/http"
"net/textproto"
"net/url"
"reflect"
"regexp"
"slices"
"strconv"
@@ -46,6 +48,26 @@ type requestDescriptor struct {
RewriteModel string `json:"rewriteModel"`
BodyType string `json:"bodyType"`
Parts []requestPart `json:"parts"`
// BodyText is the body as JSON.stringify wrote it, for a plugin that
// preserves JSON order: the upstream gets the members in that order.
BodyText json.RawMessage `json:"-"`
}
// orderedRequestDescriptor is requestDescriptor as a plugin that preserves
// JSON order returns it: decoded from the JSON.stringify text of the result,
// with the body kept as that text.
type orderedRequestDescriptor struct {
ResponseType string `json:"responseType"`
URL string `json:"url"`
Method string `json:"method"`
Headers map[string]string `json:"headers"`
Body json.RawMessage `json:"body"`
Credentialless bool `json:"credentialless"`
Action string `json:"action"`
Model string `json:"model"`
RewriteModel string `json:"rewriteModel"`
BodyType string `json:"bodyType"`
Parts []requestPart `json:"parts"`
}
type requestPart struct {
@@ -88,6 +110,12 @@ type TaskAdaptor struct {
routeRequest *pluginruntime.RouteRequestContext
requestHeaders map[string]string
files []map[string]any
// hookRequestBody is hookRequestSource as hooks receive it.
hookRequestSource map[string]any
hookRequestBody any
// requestBodyText is the decoded requestBody as JSON text, which hooks
// of a plugin that preserves JSON order receive instead.
requestBodyText json.RawMessage
}
func New(plugin *pluginruntime.LoadedPlugin) *TaskAdaptor { return &TaskAdaptor{plugin: plugin} }
@@ -98,7 +126,10 @@ func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycom
if pinned, ok := pinnedValue.(pluginruntime.PinnedEndpoint); ok && pinned.Plugin == a.plugin {
if protocolValue, present := c.Get(pluginruntime.ContextKeyProtocolRequest); present {
if protocolContext, valid := protocolValue.(pluginruntime.ProtocolRequestContext); valid {
resolvedValue, callErr := a.plugin.Engine.CallPath(context.WithoutCancel(c.Request.Context()), "protocols", []string{pinned.Protocol, "decodeRequest"}, protocolContext.JSValue())
resolvedValue, requestBodyText, callErr := a.plugin.Engine.CallPathWithMemberJSON(
context.WithoutCancel(c.Request.Context()), 0, a.plugin.Meta.JSONTextMember("requestBody"),
"protocols", []string{pinned.Protocol, "decodeRequest"}, protocolContext.JSValueFor(a.plugin.Meta),
)
resolved, resolvedOK := resolvedValue.(map[string]any)
resolvedModel, modelOK := resolved["model"].(string)
if callErr != nil || !resolvedOK || !modelOK || resolvedModel != pinned.Model {
@@ -110,6 +141,9 @@ func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycom
if body, present := resolved["requestBody"]; present {
c.Set("task_request", body)
}
if requestBodyText != nil {
c.Set(pluginruntime.ContextKeyRequestBodyText, requestBodyText)
}
if action, valid := resolved["action"].(string); valid && strings.TrimSpace(action) != "" {
c.Set("task_action", action)
info.Action = action
@@ -126,18 +160,23 @@ func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycom
request, hasRequest := c.Get("task_request")
hasUsageProfiles := len(a.plugin.Meta.UsageProfiles) > 0
if hasRequest && !hasUsageProfiles {
if err := a.validateResolvedUsageRequest(request, ""); err != nil {
if err := a.validateResolvedUsageRequest(request); err != nil {
return service.TaskErrorWrapperLocal(err, "plugin_usage_invalid", http.StatusBadRequest)
}
}
if _, err := a.buildSubmit(c, info); err != nil {
return service.TaskErrorWrapperLocal(err, "plugin_request_invalid", http.StatusBadRequest)
// utils.fetch failures in the build hook name the host fetched: the
// channel base URL or one of the plugin's allowed hosts, which an
// installed plugin may point at private hosts.
taskErr := service.TaskErrorWrapper(err, "plugin_request_invalid", http.StatusBadRequest, append(common.UpstreamAddresses(c), a.plugin.Meta.AllowedHosts...)...)
taskErr.LocalError = true
return taskErr
}
// The descriptor may rewrite the model. Validate profiled requests against
// that final model before any quota calculation or upstream submission.
// that final model before any quota calculation or upstream submission; an
// upstream endpoint ID without a profile keeps the client model's profile.
if hasRequest && hasUsageProfiles {
usageModel := cmp.Or(info.UpstreamModelName, info.OriginModelName)
if err := a.validateResolvedUsageRequest(request, usageModel); err != nil {
if err := a.validateResolvedUsageRequest(request, info.UpstreamModelName, info.OriginModelName); err != nil {
return service.TaskErrorWrapperLocal(err, "plugin_usage_invalid", http.StatusBadRequest)
}
}
@@ -156,7 +195,7 @@ func (a *TaskAdaptor) EstimateBilling(c *gin.Context, info *relaycommon.RelayInf
func (a *TaskAdaptor) EstimateBillingValidated(c *gin.Context, info *relaycommon.RelayInfo) (map[string]float64, error) {
usageContext := a.submitContext(c, info)
usageContext["usagePurpose"] = "billing_ratios"
return a.usageRatios(c.Request.Context(), cmp.Or(info.UpstreamModelName, info.OriginModelName), "extractUsage", usageContext)
return a.usageRatios(c.Request.Context(), []string{info.UpstreamModelName, info.OriginModelName}, "extractUsage", usageContext)
}
func (a *TaskAdaptor) ExtractUsageFacts(c *gin.Context, info *relaycommon.RelayInfo) map[string]any {
@@ -185,10 +224,11 @@ func (a *TaskAdaptor) ExtractUsageFactsValidated(c *gin.Context, info *relaycomm
if !ok {
return nil, fmt.Errorf("plugin usage hook must return an object")
}
if _, err = a.validatedUsageRatios(facts, cmp.Or(info.UpstreamModelName, info.OriginModelName)); err != nil {
validated, _, err := a.validatedUsageRatios(facts, info.UpstreamModelName, info.OriginModelName)
if err != nil {
return nil, err
}
return facts, nil
return validated, nil
}
func (a *TaskAdaptor) AdjustBillingOnSubmit(info *relaycommon.RelayInfo, taskData []byte) map[string]float64 {
@@ -196,7 +236,7 @@ func (a *TaskAdaptor) AdjustBillingOnSubmit(info *relaycommon.RelayInfo, taskDat
if err := common.Unmarshal(taskData, &data); err != nil {
data = string(taskData)
}
ratios, err := a.usageRatios(context.Background(), cmp.Or(info.UpstreamModelName, info.OriginModelName), "extractUsageOnSubmit", a.submitContext(nil, info), data)
ratios, err := a.usageRatios(context.Background(), []string{info.UpstreamModelName, info.OriginModelName}, "extractUsageOnSubmit", a.submitContext(nil, info), data)
if err != nil {
a.logRejectedUsage("extractUsageOnSubmit", err)
return nil
@@ -212,11 +252,11 @@ func (a *TaskAdaptor) AdjustBillingOnComplete(task *model.Task, result *relaycom
if err != nil {
return 0
}
usageModel := ""
if task != nil {
usageModel = cmp.Or(task.Properties.UpstreamModelName, task.Properties.OriginModelName)
if task == nil {
a.applyCompletionUsageFacts(result, value)
return 0
}
a.applyCompletionUsageFacts(result, value, usageModel)
a.applyCompletionUsageFacts(result, value, task.Properties.UpstreamModelName, task.Properties.OriginModelName)
return 0
}
@@ -227,13 +267,18 @@ func (a *TaskAdaptor) BuildRequestURL(info *relaycommon.RelayInfo) (string, erro
return a.submit.URL, pluginruntime.ValidateRequestURL(a.submit.URL, info.ChannelBaseUrl, a.plugin.Meta.AllowedHosts)
}
func (a *TaskAdaptor) BuildRequestHeader(_ *gin.Context, req *http.Request, _ *relaycommon.RelayInfo) error {
func (a *TaskAdaptor) BuildRequestHeader(c *gin.Context, req *http.Request, _ *relaycommon.RelayInfo) error {
if a.submit == nil {
return fmt.Errorf("plugin submit request was not built")
}
for name, value := range a.submit.Headers {
req.Header.Set(name, value)
}
// BuildRequestBody rebuilt the multipart body with a new boundary and put its
// Content-Type on the client request; a plugin's own Content-Type cannot match it.
if a.submit.BodyType == "multipart" {
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
}
return nil
}
@@ -267,18 +312,18 @@ func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, info *relaycommon.RelayIn
}
continue
}
field := strings.TrimPrefix(part.FileRef, "request_file:")
field, index, validRef := pluginruntime.ParseFileReference(part.FileRef)
files := form.File[field]
if len(files) == 0 {
if !validRef || index >= len(files) {
return nil, fmt.Errorf("unknown file reference %q", part.FileRef)
}
file, openErr := files[0].Open()
file, openErr := files[index].Open()
if openErr != nil {
return nil, openErr
}
filename := part.Filename
if filename == "" {
filename = files[0].Filename
filename = files[index].Filename
}
header := make(textproto.MIMEHeader)
disposition := mime.FormatMediaType("form-data", map[string]string{"name": part.Name, "filename": filename})
@@ -287,7 +332,7 @@ func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, info *relaycommon.RelayIn
return nil, fmt.Errorf("invalid multipart name or filename")
}
header.Set("Content-Disposition", disposition)
header.Set("Content-Type", files[0].Header.Get("Content-Type"))
header.Set("Content-Type", files[index].Header.Get("Content-Type"))
destination, copyErr := writer.CreatePart(header)
if copyErr == nil {
_, copyErr = io.Copy(destination, file)
@@ -309,7 +354,18 @@ func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, info *relaycommon.RelayIn
if text, ok := descriptor.Body.(string); ok {
return strings.NewReader(text), nil
}
inlined, err := inlineJSONFilePlaceholders(c, descriptor.Body)
value := descriptor.Body
if descriptor.BodyText != nil {
if !bytes.Contains(descriptor.BodyText, []byte(`"__fileRef":`)) {
return bytes.NewReader(descriptor.BodyText), nil
}
// File placeholders resolve only in the decoded value, which writes
// the members sorted; uploads carry no client JSON order to keep.
if err := common.Unmarshal(descriptor.BodyText, &value); err != nil {
return nil, err
}
}
inlined, err := inlineJSONFilePlaceholders(c, value)
if err != nil {
return nil, err
}
@@ -391,12 +447,12 @@ func encodeFilePlaceholder(placeholder map[string]any, form *multipart.Form, lim
if form == nil {
return "", fmt.Errorf("unknown file reference %q", ref)
}
field := strings.TrimPrefix(ref, "request_file:")
field, index, validRef := pluginruntime.ParseFileReference(ref)
files := form.File[field]
if len(files) == 0 {
if !validRef || index >= len(files) {
return "", fmt.Errorf("unknown file reference %q", ref)
}
header := files[0]
header := files[index]
maxBytes := limit
if raw, exists := placeholder["maxBytes"]; exists {
n, ok := usageNumber(raw, false)
@@ -484,7 +540,7 @@ func (a *TaskAdaptor) ParseResponse(c *gin.Context, resp *http.Response, info *r
}
}
if err != nil {
failure := service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusBadGateway)
failure := service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusBadGateway, common.UpstreamAddresses(c)...)
failure.NoRetry = streaming
return nil, failure
}
@@ -499,7 +555,7 @@ func (a *TaskAdaptor) ParseResponse(c *gin.Context, resp *http.Response, info *r
resp.StatusCode,
time.Since(started).Milliseconds(),
)
return nil, service.TaskErrorWrapper(err, "plugin_submit_response_failed", http.StatusBadGateway)
return nil, service.TaskErrorWrapper(err, "plugin_submit_response_failed", http.StatusBadGateway, common.UpstreamAddresses(c)...)
}
if object, ok := value.(map[string]any); ok {
if _, forbidden := object["clientResponse"]; forbidden {
@@ -518,7 +574,7 @@ func (a *TaskAdaptor) ParseResponse(c *gin.Context, resp *http.Response, info *r
resp.StatusCode,
time.Since(started).Milliseconds(),
)
return nil, service.TaskErrorWrapper(err, "plugin_submit_response_invalid", http.StatusBadGateway)
return nil, service.TaskErrorWrapper(err, "plugin_submit_response_invalid", http.StatusBadGateway, common.UpstreamAddresses(c)...)
}
var taskData []byte
if parsed.TaskData != nil {
@@ -527,12 +583,12 @@ func (a *TaskAdaptor) ParseResponse(c *gin.Context, resp *http.Response, info *r
if err == nil {
err = fmt.Errorf("task data exceeds size limit")
}
return nil, service.TaskErrorWrapper(err, "plugin_submit_response_invalid", http.StatusBadGateway)
return nil, service.TaskErrorWrapper(err, "plugin_submit_response_invalid", http.StatusBadGateway, common.UpstreamAddresses(c)...)
}
}
pluginState, _ := encodeReturnedPluginState(value)
if len(pluginState) > maxTaskPluginPersistedJSONBytes {
logger.LogWarn(c, fmt.Sprintf("task plugin %s rejected oversized submit state (%d bytes)", a.plugin.Meta.Key, len(pluginState)))
logger.LogWarn(c, common.LogText("task plugin %s rejected oversized submit state (%d bytes)", a.plugin.Meta.Key, len(pluginState)))
pluginState = nil
}
response := &channel.TaskSubmitResponse{
@@ -562,16 +618,16 @@ func (a *TaskAdaptor) ParseResponse(c *gin.Context, resp *http.Response, info *r
task.PrivateData.PluginState = pluginState
ctx, err := a.queryContext(task, info.ApiKey, info.ChannelBaseUrl, info.ChannelSetting.Proxy)
if err != nil {
logger.LogWarn(c, fmt.Sprintf("task plugin %s completion context failed; retaining reserved quota: %v", a.plugin.Meta.Key, err))
logger.LogWarn(c, common.LogText("task plugin %s completion context failed; retaining reserved quota: %v", a.plugin.Meta.Key, err))
return response, nil
}
facts, err := a.plugin.Engine.Call(c.Request.Context(), "extractUsageOnComplete", ctx, jsonValue(immediate), parsed.TaskData)
if err != nil {
logger.LogWarn(c, fmt.Sprintf("task plugin %s completion usage failed; retaining reserved quota: %v", a.plugin.Meta.Key, err))
logger.LogWarn(c, common.LogText("task plugin %s completion usage failed; retaining reserved quota: %v", a.plugin.Meta.Key, err))
return response, nil
}
if err := a.applyCompletionUsageFacts(immediate, facts, cmp.Or(info.UpstreamModelName, info.OriginModelName)); err != nil {
logger.LogWarn(c, fmt.Sprintf("task plugin %s completion usage rejected; retaining reserved quota: %v", a.plugin.Meta.Key, err))
if err := a.applyCompletionUsageFacts(immediate, facts, info.UpstreamModelName, info.OriginModelName); err != nil {
logger.LogWarn(c, common.LogText("task plugin %s completion usage rejected; retaining reserved quota: %v", a.plugin.Meta.Key, err))
}
return response, nil
}
@@ -593,11 +649,11 @@ func (a *TaskAdaptor) FetchBatchTasks(baseURL, key string, tasks []*model.Task,
if err != nil {
return nil, err
}
value, err := a.plugin.Engine.Call(context.Background(), "buildBatchQueryRequest", ctx, taskContexts)
descriptor, err := a.requestDescriptor(pluginruntime.WithFetcher(context.Background(), a.hookFetcher(baseURL, proxy)), "buildBatchQueryRequest", ctx, taskContexts)
if err != nil {
return nil, err
}
return a.doFetchDescriptor(baseURL, proxy, value)
return a.doFetchDescriptor(baseURL, proxy, descriptor)
}
func (a *TaskAdaptor) FetchTask(baseURL, key string, task *model.Task, proxy string) (*http.Response, error) {
@@ -605,18 +661,60 @@ func (a *TaskAdaptor) FetchTask(baseURL, key string, task *model.Task, proxy str
if err != nil {
return nil, err
}
value, err := a.plugin.Engine.Call(context.Background(), "buildQueryRequest", ctx)
descriptor, err := a.requestDescriptor(pluginruntime.WithFetcher(context.Background(), a.hookFetcher(baseURL, proxy)), "buildQueryRequest", ctx)
if err != nil {
return nil, err
}
return a.doFetchDescriptor(baseURL, proxy, value)
return a.doFetchDescriptor(baseURL, proxy, descriptor)
}
func (a *TaskAdaptor) doFetchDescriptor(baseURL, proxy string, value any) (*http.Response, error) {
var descriptor requestDescriptor
if err := convert(value, &descriptor); err != nil {
return nil, err
// hookFetcher sends a driver hook's utils.fetch requests to the hosts its
// request descriptors may use, through the channel's proxy like poll requests.
func (a *TaskAdaptor) hookFetcher(baseURL, proxy string) pluginruntime.Fetcher {
return func(ctx context.Context, request pluginruntime.FetchRequest) (pluginruntime.FetchResponse, error) {
if err := pluginruntime.ValidateRequestURL(request.URL, baseURL, a.plugin.Meta.AllowedHosts); err != nil {
return pluginruntime.FetchResponse{}, err
}
client, err := service.GetHttpClientWithProxySettings(proxy, a.channelSetting())
if err != nil {
return pluginruntime.FetchResponse{}, err
}
// A redirect reaches the plugin as it is; fetching its location is a
// new request, checked and counted again.
noRedirects := *client
noRedirects.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }
req, err := http.NewRequestWithContext(ctx, request.Method, request.URL, nil)
if err != nil {
return pluginruntime.FetchResponse{}, err
}
for name, value := range request.Headers {
req.Header.Set(name, value)
}
started := time.Now()
resp, err := noRedirects.Do(req)
if err != nil {
logger.LogDebug(ctx, "task_plugin subsystem=fetch event=failed plugin=%q hook=%q method=%q host=%q reason=transport_error elapsed_ms=%d",
a.plugin.Meta.Key, request.Hook, request.Method, req.URL.Host, time.Since(started).Milliseconds())
return pluginruntime.FetchResponse{}, err
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, pluginruntime.MaxFetchBodyBytes+1))
if err != nil {
return pluginruntime.FetchResponse{}, err
}
headers := make(map[string]string, len(resp.Header))
for name, values := range resp.Header {
if len(values) > 0 {
headers[name] = values[0]
}
}
logger.LogDebug(ctx, "task_plugin subsystem=fetch event=done plugin=%q hook=%q method=%q host=%q status=%d bytes=%d elapsed_ms=%d",
a.plugin.Meta.Key, request.Hook, request.Method, req.URL.Host, resp.StatusCode, len(body), time.Since(started).Milliseconds())
return pluginruntime.FetchResponse{Status: resp.StatusCode, Headers: headers, Body: body}, nil
}
}
func (a *TaskAdaptor) doFetchDescriptor(baseURL, proxy string, descriptor requestDescriptor) (*http.Response, error) {
if err := pluginruntime.ValidateRequestURL(descriptor.URL, baseURL, a.plugin.Meta.AllowedHosts); err != nil {
return nil, err
}
@@ -624,6 +722,8 @@ func (a *TaskAdaptor) doFetchDescriptor(baseURL, proxy string, value any) (*http
if descriptor.Body != nil {
if bodyText, ok := descriptor.Body.(string); ok {
requestBody = strings.NewReader(bodyText)
} else if descriptor.BodyText != nil {
requestBody = bytes.NewReader(descriptor.BodyText)
} else {
encoded, marshalErr := common.Marshal(descriptor.Body)
if marshalErr != nil {
@@ -643,7 +743,7 @@ func (a *TaskAdaptor) doFetchDescriptor(baseURL, proxy string, value any) (*http
for name, value := range descriptor.Headers {
req.Header.Set(name, value)
}
client, err := service.GetHttpClientWithProxy(proxy)
client, err := service.GetHttpClientWithProxySettings(proxy, a.channelSetting())
if err != nil {
return nil, err
}
@@ -690,11 +790,6 @@ func (a *TaskAdaptor) ParseBatchResult(tasks []*model.Task, resp *http.Response,
if err != nil {
return nil, err
}
value, err := a.plugin.Engine.Call(context.Background(), "parseBatchResult", ctx, input, hookHTTPResponse(resp))
if err != nil {
logger.LogDebug(context.Background(), "task_plugin subsystem=adaptor event=parse_batch_failed plugin=%q reason=hook_failed body_bytes=%d elapsed_ms=%d", a.plugin.Meta.Key, len(body), time.Since(started).Milliseconds())
return nil, err
}
var parsed []struct {
TaskID string `json:"taskId"`
Action string `json:"action"`
@@ -708,8 +803,13 @@ func (a *TaskAdaptor) ParseBatchResult(tasks []*model.Task, resp *http.Response,
Data any `json:"data"`
State any `json:"state"`
}
if err = convert(value, &parsed); err != nil {
logger.LogDebug(context.Background(), "task_plugin subsystem=adaptor event=parse_batch_failed plugin=%q reason=invalid_result body_bytes=%d elapsed_ms=%d", a.plugin.Meta.Key, len(body), time.Since(started).Milliseconds())
if err = a.plugin.Engine.CallInto(pluginruntime.WithFetcher(context.Background(), a.hookFetcher(baseURL, proxy)), &parsed, "parseBatchResult", ctx, input, hookHTTPResponse(resp)); err != nil {
reason := "hook_failed"
var invalid *pluginruntime.ResultError
if errors.As(err, &invalid) {
reason = "invalid_result"
}
logger.LogDebug(context.Background(), "task_plugin subsystem=adaptor event=parse_batch_failed plugin=%q reason=%s body_bytes=%d elapsed_ms=%d", a.plugin.Meta.Key, reason, len(body), time.Since(started).Milliseconds())
return nil, err
}
results := make(map[string]*service.BatchTaskResult, len(parsed))
@@ -722,7 +822,7 @@ func (a *TaskAdaptor) ParseBatchResult(tasks []*model.Task, resp *http.Response,
if item.State != nil {
pluginState, marshalErr := common.Marshal(item.State)
if marshalErr != nil || len(pluginState) > maxTaskPluginPersistedJSONBytes {
logger.LogWarn(context.Background(), fmt.Sprintf("task plugin %s rejected invalid or oversized poll state", a.plugin.Meta.Key))
logger.LogWarn(context.Background(), common.LogText("task plugin %s rejected invalid or oversized poll state", a.plugin.Meta.Key))
} else {
info.PluginState = pluginState
}
@@ -741,8 +841,9 @@ func (a *TaskAdaptor) ParseBatchResult(tasks []*model.Task, resp *http.Response,
}
facts, hookErr := a.plugin.Engine.Call(context.Background(), "extractUsageOnComplete", itemCtx, jsonValue(&info), usageBody)
if hookErr == nil {
usageModel, _ := itemCtx["upstreamModel"].(string)
a.applyCompletionUsageFacts(&info, facts, usageModel)
upstreamModel, _ := itemCtx["upstreamModel"].(string)
originModel, _ := itemCtx["model"].(string)
a.applyCompletionUsageFacts(&info, facts, upstreamModel, originModel)
}
}
results[item.TaskID] = &service.BatchTaskResult{TaskInfo: info, Action: item.Action, SubmitTime: item.SubmitTime, StartTime: item.StartTime, FinishTime: item.FinishTime, Data: item.Data}
@@ -774,7 +875,7 @@ func (a *TaskAdaptor) ParseTaskResult(task *model.Task, resp *http.Response, bod
if err != nil {
return nil, err
}
value, err := a.plugin.Engine.Call(context.Background(), "parseTaskResult", ctx, input, hookHTTPResponse(resp))
value, err := a.plugin.Engine.Call(pluginruntime.WithFetcher(context.Background(), a.hookFetcher(baseURL, proxy)), "parseTaskResult", ctx, input, hookHTTPResponse(resp))
if err != nil {
logger.LogDebug(context.Background(), "task_plugin subsystem=adaptor event=parse_task_failed plugin=%q reason=hook_failed body_bytes=%d elapsed_ms=%d", a.plugin.Meta.Key, len(body), time.Since(started).Milliseconds())
return nil, err
@@ -797,7 +898,7 @@ func (a *TaskAdaptor) ParseTaskResult(task *model.Task, resp *http.Response, bod
}
if pluginState, present := encodeReturnedPluginState(value); present {
if len(pluginState) > maxTaskPluginPersistedJSONBytes {
logger.LogWarn(context.Background(), fmt.Sprintf("task plugin %s rejected oversized poll state (%d bytes)", a.plugin.Meta.Key, len(pluginState)))
logger.LogWarn(context.Background(), common.LogText("task plugin %s rejected oversized poll state (%d bytes)", a.plugin.Meta.Key, len(pluginState)))
} else {
result.PluginState = pluginState
}
@@ -807,8 +908,9 @@ func (a *TaskAdaptor) ParseTaskResult(task *model.Task, resp *http.Response, bod
if a.hasHook(context.Background(), "extractUsageOnComplete") {
facts, hookErr := a.plugin.Engine.Call(context.Background(), "extractUsageOnComplete", ctx, jsonValue(result), input)
if hookErr == nil {
usageModel, _ := ctx["upstreamModel"].(string)
a.applyCompletionUsageFacts(result, facts, usageModel)
upstreamModel, _ := ctx["upstreamModel"].(string)
originModel, _ := ctx["model"].(string)
a.applyCompletionUsageFacts(result, facts, upstreamModel, originModel)
}
}
taskStatus := model.TaskStatus(result.Status)
@@ -823,8 +925,8 @@ func (a *TaskAdaptor) ParseTaskResult(task *model.Task, resp *http.Response, bod
return result, nil
}
func (a *TaskAdaptor) applyCompletionUsageFacts(result *relaycommon.TaskInfo, facts any, modelName string) error {
values, err := a.validatedCompletionUsageFacts(facts, modelName)
func (a *TaskAdaptor) applyCompletionUsageFacts(result *relaycommon.TaskInfo, facts any, models ...string) error {
values, err := a.validatedCompletionUsageFacts(facts, models...)
if err != nil {
a.logRejectedUsage("extractUsageOnComplete", err)
return err
@@ -861,16 +963,19 @@ func (a *TaskAdaptor) ConvertToOpenAIVideo(task *model.Task) ([]byte, error) {
if err != nil {
return nil, err
}
value, err := a.plugin.Engine.CallPath(context.Background(), "protocols", []string{"openai_video", "render"}, map[string]any{"protocol": "openai_video", "operation": "retrieve"}, jsonValue(view))
encodedView, err := common.Marshal(view)
if err != nil {
return nil, err
}
encoded, err := common.Marshal(value)
// Decoding into any leaves only encoding failures to CallPathInto, so they
// keep the codec's message.
var value any
err = a.plugin.Engine.CallPathInto(context.Background(), &value, "protocols", []string{"openai_video", "render"}, map[string]any{"protocol": "openai_video", "operation": "retrieve"}, pluginruntime.RawJSON(encodedView))
if err != nil {
return nil, err
}
var rendered map[string]any
if err = common.Unmarshal(encoded, &rendered); err != nil || rendered == nil {
rendered, _ := value.(map[string]any)
if rendered == nil {
return nil, fmt.Errorf("plugin returned an invalid OpenAI video object")
}
// Keep provider extensions intact while the host owns the public task's
@@ -906,7 +1011,7 @@ func (a *TaskAdaptor) ListArtifacts(task *model.Task) ([]channel.TaskArtifact, e
return validateTaskArtifacts(value)
}
func (a *TaskAdaptor) BuildContentRequest(task *model.Task, artifactKey string, clientRequest channel.TaskArtifactClientRequest) (*channel.TaskContentRequest, error) {
func (a *TaskAdaptor) BuildContentRequest(ctx context.Context, task *model.Task, artifactKey string, clientRequest channel.TaskArtifactClientRequest) (*channel.TaskContentRequest, error) {
if !a.hasHook(context.Background(), "buildContentRequest") {
return nil, nil
}
@@ -916,32 +1021,22 @@ func (a *TaskAdaptor) BuildContentRequest(task *model.Task, artifactKey string,
if !taskArtifactKeyPattern.MatchString(artifactKey) {
return nil, fmt.Errorf("invalid artifact key")
}
ctx, err := taskArtifactContext(task)
hookContext, err := taskArtifactContext(task)
if err != nil {
return nil, err
}
ctx["upstreamTaskId"] = task.GetUpstreamTaskID()
ctx["artifactKey"] = artifactKey
ctx["baseUrl"] = a.info.ChannelBaseUrl
ctx["clientRequest"] = jsonValue(clientRequest)
proxy := a.info.ChannelSetting.Proxy
auth, err := resolveAuth(a.plugin.Meta.Auth, a.info.ApiKey, proxy)
if err != nil {
hookContext["upstreamTaskId"] = task.GetUpstreamTaskID()
hookContext["artifactKey"] = artifactKey
hookContext["baseUrl"] = a.info.ChannelBaseUrl
hookContext["clientRequest"] = jsonValue(clientRequest)
if err = a.applyUpstreamCredentials(hookContext, a.info.ChannelType, a.info.ApiKey, a.info.ChannelSetting.Proxy); err != nil {
return nil, err
}
ctx["auth"] = auth
ctx["authHeader"] = auth["authHeader"]
if a.plugin.Meta.Auth.Type == "" || a.plugin.Meta.Auth.Type == "none" || a.plugin.Meta.Auth.Type == "api_key" {
ctx["apiKey"] = a.info.ApiKey
}
value, err := a.plugin.Engine.Call(context.Background(), "buildContentRequest", ctx)
fetchContext := pluginruntime.WithFetcher(ctx, a.hookFetcher(a.info.ChannelBaseUrl, a.info.ChannelSetting.Proxy))
descriptor, err := a.requestDescriptor(fetchContext, "buildContentRequest", hookContext)
if err != nil {
return nil, err
}
var descriptor requestDescriptor
if err = convert(value, &descriptor); err != nil {
return nil, err
}
method := strings.ToUpper(strings.TrimSpace(descriptor.Method))
if method == "" {
method = strings.ToUpper(strings.TrimSpace(clientRequest.Method))
@@ -970,6 +1065,8 @@ func (a *TaskAdaptor) BuildContentRequest(task *model.Task, artifactKey string,
if descriptor.Body != nil {
if text, ok := descriptor.Body.(string); ok {
body = []byte(text)
} else if descriptor.BodyText != nil {
body = descriptor.BodyText
} else {
body, err = common.Marshal(descriptor.Body)
if err != nil {
@@ -1056,29 +1153,17 @@ func (a *TaskAdaptor) queryContext(task *model.Task, key, baseURL, proxy string)
key = task.PrivateData.Key
}
}
auth, err := resolveAuth(a.plugin.Meta.Auth, key, proxy)
if err != nil {
if err := a.applyUpstreamCredentials(ctx, a.channelType(), key, proxy); err != nil {
return nil, err
}
ctx["auth"] = auth
ctx["authHeader"] = auth["authHeader"]
if a.plugin.Meta.Auth.Type == "" || a.plugin.Meta.Auth.Type == "none" || a.plugin.Meta.Auth.Type == "api_key" {
ctx["apiKey"] = key
}
return ctx, nil
}
func (a *TaskAdaptor) batchQueryContext(key, baseURL, proxy string, tasks []map[string]any) (map[string]any, error) {
ctx := map[string]any{"baseUrl": baseURL, "tasks": tasks}
auth, err := resolveAuth(a.plugin.Meta.Auth, key, proxy)
if err != nil {
if err := a.applyUpstreamCredentials(ctx, a.channelType(), key, proxy); err != nil {
return nil, err
}
ctx["auth"] = auth
ctx["authHeader"] = auth["authHeader"]
if a.plugin.Meta.Auth.Type == "" || a.plugin.Meta.Auth.Type == "none" || a.plugin.Meta.Auth.Type == "api_key" {
ctx["apiKey"] = key
}
return ctx, nil
}
@@ -1089,6 +1174,50 @@ func (a *TaskAdaptor) queryCredentials() (key, baseURL, proxy string) {
return a.info.ApiKey, a.info.ChannelBaseUrl, a.info.ChannelSetting.Proxy
}
// channelType is the executing channel's type when polling or content
// fetches run with channel metadata; zero otherwise, which is a vendor upstream.
func (a *TaskAdaptor) channelType() int {
if a.info == nil || !a.info.HasChannelMeta() {
return 0
}
return a.info.ChannelType
}
// channelSetting is the executing channel's settings when the adaptor runs
// with channel metadata; zero otherwise.
func (a *TaskAdaptor) channelSetting() kitdto.ChannelSettings {
if a.info == nil || !a.info.HasChannelMeta() {
return kitdto.ChannelSettings{}
}
return a.info.ChannelSetting
}
// applyUpstreamCredentials sets ctx.upstream together with the credentials
// every driver hook reads. A New API channel (type 60) is another gateway:
// the plugin must address its own prefixed native routes there, and the
// channel key is that gateway's token, so it is sent as a Bearer header for
// every auth type instead of running the plugin's vendor auth scheme.
func (a *TaskAdaptor) applyUpstreamCredentials(ctx map[string]any, channelType int, key, proxy string) error {
if channelType == constant.ChannelTypeNewAPI {
ctx["upstream"] = map[string]any{"kind": pluginruntime.UpstreamKindNewAPI}
ctx["auth"] = map[string]any{"authHeader": "Bearer " + key}
ctx["authHeader"] = "Bearer " + key
ctx["apiKey"] = key
return nil
}
ctx["upstream"] = map[string]any{"kind": pluginruntime.UpstreamKindVendor}
auth, err := resolveAuth(a.plugin.Meta.Auth, key, proxy)
if err != nil {
return err
}
ctx["auth"] = auth
ctx["authHeader"] = auth["authHeader"]
if a.plugin.Meta.Auth.Type == "" || a.plugin.Meta.Auth.Type == "none" || a.plugin.Meta.Auth.Type == "api_key" {
ctx["apiKey"] = key
}
return nil
}
func hookHTTPResponse(resp *http.Response) map[string]any {
headers := map[string]string{}
status := 0
@@ -1176,27 +1305,62 @@ func validateTaskArtifacts(value any) ([]channel.TaskArtifact, error) {
return artifacts, nil
}
// requestDescriptor runs a request-building hook. A plugin that preserves JSON
// order has its descriptor decoded from the JSON.stringify text of the result,
// with the body kept as that text (BodyText) for the upstream.
func (a *TaskAdaptor) requestDescriptor(ctx context.Context, hook string, args ...any) (requestDescriptor, error) {
if !a.plugin.Meta.PreservesJSONOrder() {
var descriptor requestDescriptor
err := a.plugin.Engine.CallInto(ctx, &descriptor, hook, args...)
return descriptor, err
}
var ordered orderedRequestDescriptor
if err := a.plugin.Engine.CallJSONInto(ctx, &ordered, hook, args...); err != nil {
return requestDescriptor{}, err
}
descriptor := requestDescriptor{
ResponseType: ordered.ResponseType, URL: ordered.URL, Method: ordered.Method, Headers: ordered.Headers,
Credentialless: ordered.Credentialless, Action: ordered.Action, Model: ordered.Model,
RewriteModel: ordered.RewriteModel, BodyType: ordered.BodyType, Parts: ordered.Parts,
}
switch {
case len(ordered.Body) == 0 || string(ordered.Body) == "null":
case ordered.Body[0] == '"':
// A string body is the plugin's own text, sent as it is.
var text string
if err := common.Unmarshal(ordered.Body, &text); err != nil {
return requestDescriptor{}, err
}
descriptor.Body = text
default:
descriptor.Body, descriptor.BodyText = ordered.Body, ordered.Body
}
return descriptor, nil
}
func (a *TaskAdaptor) buildSubmit(c *gin.Context, info *relaycommon.RelayInfo) (*requestDescriptor, error) {
if a.submit != nil {
return a.submit, nil
}
started := time.Now()
value, err := a.plugin.Engine.Call(c.Request.Context(), "buildSubmitRequest", a.submitContext(c, info))
if err != nil {
logger.LogDebug(
c,
"task_plugin subsystem=adaptor event=build_submit_failed plugin=%q stage=build_submit_request reason=hook_failed elapsed_ms=%d",
a.plugin.Meta.Key,
time.Since(started).Milliseconds(),
)
return nil, err
// buildSubmitRequest runs before quota is reserved, so a failed fetch
// costs nothing; requests that start vendor work stay the descriptor.
fetchContext := c.Request.Context()
if info.HasChannelMeta() {
fetchContext = pluginruntime.WithFetcher(fetchContext, a.hookFetcher(info.ChannelBaseUrl, info.ChannelSetting.Proxy))
}
var descriptor requestDescriptor
if err = convert(value, &descriptor); err != nil {
descriptor, err := a.requestDescriptor(fetchContext, "buildSubmitRequest", a.submitContext(c, info))
if err != nil {
reason := "hook_failed"
var invalid *pluginruntime.ResultError
if errors.As(err, &invalid) {
reason = "invalid_descriptor"
}
logger.LogDebug(
c,
"task_plugin subsystem=adaptor event=build_submit_failed plugin=%q stage=build_submit_request reason=invalid_descriptor elapsed_ms=%d",
"task_plugin subsystem=adaptor event=build_submit_failed plugin=%q stage=build_submit_request reason=%s elapsed_ms=%d",
a.plugin.Meta.Key,
reason,
time.Since(started).Milliseconds(),
)
return nil, err
@@ -1282,7 +1446,11 @@ func (a *TaskAdaptor) submitContext(c *gin.Context, info *relaycommon.RelayInfo)
}
}
if taskRequest, exists := c.Get("task_request"); exists {
routeRequest.RequestBody = jsonValue(taskRequest)
routeRequest.RequestBody = taskRequest
}
a.requestBodyText = nil
if text, exists := c.Get(pluginruntime.ContextKeyRequestBodyText); exists && a.plugin.Meta.PreservesJSONOrder() {
a.requestBodyText, _ = text.(json.RawMessage)
}
if c.Request != nil {
if routeRequest.Path == "" {
@@ -1309,8 +1477,8 @@ func (a *TaskAdaptor) submitContext(c *gin.Context, info *relaycommon.RelayInfo)
if form, err := common.ParseMultipartFormReusable(c); err == nil {
defer form.RemoveAll()
for field, headers := range form.File {
for _, header := range headers {
files = append(files, map[string]any{"ref": "request_file:" + field, "field": field, "filename": header.Filename, "mimeType": header.Header.Get("Content-Type"), "size": header.Size})
for index, header := range headers {
files = append(files, map[string]any{"ref": pluginruntime.FileReference(field, index), "field": field, "filename": header.Filename, "mimeType": header.Header.Get("Content-Type"), "size": header.Size})
}
}
}
@@ -1323,7 +1491,25 @@ func (a *TaskAdaptor) submitContext(c *gin.Context, info *relaycommon.RelayInfo)
a.files = append(a.files[:0], files...)
}
ctx := routeRequest.JSValue()
ctx["requestBody"] = jsonValue(routeRequest.RequestBody)
// Hooks never write into the request body, so the same object normalizes
// to the same value on every hook call of this request.
requestBody := routeRequest.RequestBody
source, isObject := requestBody.(map[string]any)
if a.requestBodyText != nil {
// Driver hooks read the request body, so the engine parses the text
// in place for each of them, members in order.
requestBody = pluginruntime.RawJSON(a.requestBodyText)
} else if isObject && a.hookRequestSource != nil && reflect.ValueOf(source).UnsafePointer() == reflect.ValueOf(a.hookRequestSource).UnsafePointer() {
requestBody = a.hookRequestBody
} else {
if !isPlainJSONValue(requestBody, 0) {
requestBody = jsonValue(requestBody)
}
if isObject {
a.hookRequestSource, a.hookRequestBody = source, requestBody
}
}
ctx["requestBody"] = requestBody
ctx["requestHeaders"] = requestHeaders
ctx["files"] = files
ctx["action"] = info.Action
@@ -1352,21 +1538,13 @@ func (a *TaskAdaptor) submitContext(c *gin.Context, info *relaycommon.RelayInfo)
ctx["upstreamModel"] = info.UpstreamModelName
ctx["baseUrl"] = info.ChannelBaseUrl
ctx["userSetting"] = info.UserSetting
proxy := ""
proxy = info.ChannelSetting.Proxy
if auth, err := resolveAuth(a.plugin.Meta.Auth, info.ApiKey, proxy); err == nil {
ctx["auth"] = auth
ctx["authHeader"] = auth["authHeader"]
if a.plugin.Meta.Auth.Type == "" || a.plugin.Meta.Auth.Type == "none" || a.plugin.Meta.Auth.Type == "api_key" {
ctx["apiKey"] = info.ApiKey
}
} else {
if err := a.applyUpstreamCredentials(ctx, info.ChannelType, info.ApiKey, info.ChannelSetting.Proxy); err != nil {
ctx["authError"] = err.Error()
}
return ctx
}
func (a *TaskAdaptor) usageRatios(ctx context.Context, modelName, hook string, args ...any) (map[string]float64, error) {
func (a *TaskAdaptor) usageRatios(ctx context.Context, models []string, hook string, args ...any) (map[string]float64, error) {
if !a.hasHook(ctx, hook) {
return nil, nil
}
@@ -1384,7 +1562,7 @@ func (a *TaskAdaptor) usageRatios(ctx context.Context, modelName, hook string, a
logger.LogDebug(ctx, "task_plugin subsystem=adaptor event=usage_hook_failed plugin=%q hook=%q reason=result_not_object elapsed_ms=%d", a.plugin.Meta.Key, hook, time.Since(started).Milliseconds())
return nil, fmt.Errorf("plugin usage hook must return an object")
}
ratios, err := a.validatedUsageRatios(facts, modelName)
_, ratios, err := a.validatedUsageRatios(facts, models...)
if err != nil {
logger.LogDebug(ctx, "task_plugin subsystem=adaptor event=usage_hook_failed plugin=%q hook=%q reason=invalid_usage elapsed_ms=%d", a.plugin.Meta.Key, hook, time.Since(started).Milliseconds())
return nil, err
@@ -1401,8 +1579,8 @@ func (a *TaskAdaptor) usageRatios(ctx context.Context, modelName, hook string, a
return ratios, nil
}
func (a *TaskAdaptor) validateResolvedUsageRequest(request any, modelName string) error {
schema, _ := a.plugin.Meta.UsageForModel(modelName)
func (a *TaskAdaptor) validateResolvedUsageRequest(request any, models ...string) error {
schema, _ := a.plugin.Meta.UsageForModels(models...)
return a.validateResolvedUsageValue(jsonValue(request), schema)
}
@@ -1410,11 +1588,21 @@ func (a *TaskAdaptor) validateResolvedUsageValue(value any, usageSchema map[stri
switch typed := value.(type) {
case map[string]any:
for key, item := range typed {
limit, canonical := canonicalUsageLimit(key)
// A duration of -1 asks the vendor to pick the length. Plugins that
// declare duration-auto@1 reserve the longest duration for it and
// settle on the usage the vendor reports; usage facts stay
// non-negative.
if canonical && limit == relaycommon.MaxTaskDurationSeconds && a.plugin.Meta.AcceptsAutoDuration() {
if number, _ := usageNumber(item, true); number == -1 {
continue
}
}
if schema, declared := usageSchema[key]; declared {
if _, err := validateUsageValue(item, schema, true); err != nil {
return err
}
} else if limit, canonical := canonicalUsageLimit(key); canonical {
} else if canonical {
if err := validateUsageLimit(item, limit, true); err != nil {
return err
}
@@ -1433,17 +1621,22 @@ func (a *TaskAdaptor) validateResolvedUsageValue(value any, usageSchema map[stri
return nil
}
func (a *TaskAdaptor) validatedUsageRatios(facts map[string]any, modelName string) (map[string]float64, error) {
usageSchema, _ := a.plugin.Meta.UsageForModel(modelName)
// validatedUsageRatios returns the normalized facts in a new map because the
// hook result may be a host value, such as the request body, that must stay
// unchanged.
func (a *TaskAdaptor) validatedUsageRatios(facts map[string]any, models ...string) (map[string]any, map[string]float64, error) {
usageSchema, _ := a.plugin.Meta.UsageForModels(models...)
validated := make(map[string]any, len(facts))
ratios := make(map[string]float64)
for key, value := range facts {
validated[key] = value
if schema, declared := usageSchema[key]; declared {
number, err := validateUsageValue(value, schema, false)
if err != nil {
return nil, err
return nil, nil, err
}
if schema.Type == "number" {
facts[key] = number
validated[key] = number
if number > 0 {
ratios[key] = number
}
@@ -1462,20 +1655,20 @@ func (a *TaskAdaptor) validatedUsageRatios(facts map[string]any, modelName strin
limit = relaycommon.MaxTaskDurationSeconds
}
if err := validateUsageNumberLimit(number, limit); err != nil {
return nil, err
return nil, nil, err
}
// Keep numeric facts usable by existing expressions even when the
// selected profile no longer declares that legacy extension field.
facts[key] = number
validated[key] = number
if number > 0 {
ratios[key] = number
}
}
return ratios, nil
return validated, ratios, nil
}
func (a *TaskAdaptor) validatedCompletionUsageFacts(facts any, modelName string) (map[string]any, error) {
usageSchema, _ := a.plugin.Meta.UsageForModel(modelName)
func (a *TaskAdaptor) validatedCompletionUsageFacts(facts any, models ...string) (map[string]any, error) {
usageSchema, _ := a.plugin.Meta.UsageForModels(models...)
if facts == nil {
return nil, nil
}
@@ -1602,8 +1795,10 @@ func usageNumber(value any, allowNumericString bool) (float64, bool) {
}
}
var usageKeySeparators = strings.NewReplacer("_", "", "-", "")
func canonicalUsageLimit(key string) (int, bool) {
normalized := strings.NewReplacer("_", "", "-", "").Replace(strings.ToLower(key))
normalized := usageKeySeparators.Replace(strings.ToLower(key))
switch normalized {
case "duration", "durationseconds", "second", "seconds":
return relaycommon.MaxTaskDurationSeconds, true
@@ -1615,13 +1810,13 @@ func canonicalUsageLimit(key string) (int, bool) {
}
func (a *TaskAdaptor) logRejectedUsage(hook string, _ error) {
common.SysError(fmt.Sprintf("task plugin %s rejected invalid %s billing facts", a.plugin.Meta.Key, hook))
common.SysError(common.LogText("task plugin %s rejected invalid %s billing facts", a.plugin.Meta.Key, hook))
}
func (a *TaskAdaptor) hasHook(ctx context.Context, hook string) bool {
has, err := a.plugin.Engine.HasExport(ctx, hook)
return err == nil && has
func (a *TaskAdaptor) hasHook(_ context.Context, hook string) bool {
return a.plugin.Engine.HasExport(hook)
}
func convert(value any, target any) error {
data, err := common.Marshal(value)
if err != nil {
@@ -1645,8 +1840,45 @@ func jsonValue(value any) any {
return normalized
}
// Already-parsed JSON and Sobek's plain exported values do not need another
// encode/decode pass. Copy containers to isolate hooks, preserving the codec's
// isPlainJSONValue reports whether hooks can take value as is: it reaches
// JavaScript exactly as the codec's decoding of it would, and the engine never
// writes back into it. Integers count, since hooks see the same numbers; a nil
// slice does not, since it would reach hooks as [] instead of null.
func isPlainJSONValue(value any, depth int) bool {
if depth > 64 {
return false
}
switch typed := value.(type) {
case nil, bool, int64, int:
return true
case string:
return utf8.ValidString(typed)
case float64:
return !math.IsNaN(typed) && !math.IsInf(typed, 0)
case map[string]any:
for key, item := range typed {
if !utf8.ValidString(key) || !isPlainJSONValue(item, depth+1) {
return false
}
}
return true
case []any:
if typed == nil {
return false
}
for _, item := range typed {
if !isPlainJSONValue(item, depth+1) {
return false
}
}
return true
default:
return false
}
}
// Already-parsed JSON and the engine's plain exported values do not need another
// encode/decode pass. Copy containers for host-side edits, preserving the codec's
// float64 numbers and nulls. Other Go types still use the configured codec.
func cloneJSONValue(value any, depth int) (any, bool) {
if depth > 64 {