diff --git a/apps/api/go.mod b/apps/api/go.mod index c7a95f7..b51f472 100644 --- a/apps/api/go.mod +++ b/apps/api/go.mod @@ -36,7 +36,7 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect go.uber.org/goleak v1.3.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/text v0.37.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/text v0.39.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/apps/api/go.sum b/apps/api/go.sum index 006bff2..1229fec 100644 --- a/apps/api/go.sum +++ b/apps/api/go.sum @@ -73,10 +73,10 @@ golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/apps/api/internal/clients/clients_test.go b/apps/api/internal/clients/clients_test.go index 700f6d9..7f528fb 100644 --- a/apps/api/internal/clients/clients_test.go +++ b/apps/api/internal/clients/clients_test.go @@ -1306,6 +1306,25 @@ func TestGeminiClientImageGenerateBuildsNativeImageBody(t *testing.T) { } } +func TestGeminiGenerationConfigInitializesMissingImageConfig(t *testing.T) { + config := geminiGenerationConfig(map[string]any{ + "aspect_ratio": "16:9", + "resolution": "4K", + }, true) + + imageConfig, ok := config["imageConfig"].(map[string]any) + if !ok { + t.Fatalf("imageConfig should be initialized, got %+v", config) + } + if imageConfig["aspectRatio"] != "16:9" || imageConfig["imageSize"] != "4K" { + t.Fatalf("unexpected imageConfig: %+v", imageConfig) + } + modalities, ok := config["responseModalities"].([]any) + if !ok || len(modalities) != 1 || modalities[0] != "IMAGE" { + t.Fatalf("image response modality should be initialized, got %+v", config) + } +} + func TestGeminiClientImageEditPreservesNativeContentsAndFileData(t *testing.T) { var captured map[string]any server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/apps/api/internal/clients/gemini.go b/apps/api/internal/clients/gemini.go index 8cfe74f..6dc86bc 100644 --- a/apps/api/internal/clients/gemini.go +++ b/apps/api/internal/clients/gemini.go @@ -195,14 +195,23 @@ func geminiApplyRequestOptions(body map[string]any, request Request, imageRespon func geminiGenerationConfig(body map[string]any, imageResponse bool) map[string]any { source := mapFromAny(firstPresent(body["generationConfig"], body["generation_config"])) out := cloneMapAny(source) + if out == nil { + out = map[string]any{} + } if aspectRatio := firstNonEmptyString(body["aspect_ratio"], body["aspectRatio"]); aspectRatio != "" { imageConfig := cloneMapAny(mapFromAny(firstPresent(out["imageConfig"], out["image_config"]))) + if imageConfig == nil { + imageConfig = map[string]any{} + } imageConfig["aspectRatio"] = aspectRatio out["imageConfig"] = imageConfig delete(out, "image_config") } if imageSize := firstNonEmptyString(body["resolution"], body["imageSize"], body["image_size"], body["size"]); imageSize != "" { imageConfig := cloneMapAny(mapFromAny(firstPresent(out["imageConfig"], out["image_config"]))) + if imageConfig == nil { + imageConfig = map[string]any{} + } imageConfig["imageSize"] = imageSize out["imageConfig"] = imageConfig delete(out, "image_config") diff --git a/apps/api/internal/clients/universal.go b/apps/api/internal/clients/universal.go index f2a998a..4d1b4da 100644 --- a/apps/api/internal/clients/universal.go +++ b/apps/api/internal/clients/universal.go @@ -44,6 +44,8 @@ func (c UniversalClient) Run(ctx context.Context, request Request) (Response, er if err != nil { return Response{}, annotateResponseError(err, submitRequestID, startedAt, time.Now()) } + submitResult = universalEffectiveResult(submitResult) + submitRequestID = firstNonEmptyString(submitRequestID, requestIDFromResult(submitResult)) if isUniversalSuccess(submitResult) && submitResult["data"] != nil { return Response{ Result: normalizeUniversalResult(request, submitResult, ""), @@ -157,6 +159,7 @@ func (c UniversalClient) universalPollUntilDone(ctx context.Context, executor *s if err != nil { return nil, "", annotateResponseError(err, firstNonEmptyString(pollRequestID, requestID, upstreamTaskID), pollStarted, pollFinished) } + result = universalEffectiveResult(result) lastResult = result requestID = firstNonEmptyString(pollRequestID, requestID, requestIDFromResult(result), upstreamTaskID) if isUniversalSuccess(result) { @@ -239,6 +242,14 @@ func universalScriptContext(request Request, modelType string, payload map[strin return selectedBase + "/" + strings.TrimLeft(path, "/") } context["creatRequestURL"] = context["createRequestURL"] + processedParams := cloneBody(request.Body) + processedParams["model"] = upstreamModelName(request.Candidate) + context["processedParams"] = processedParams + context["preProcessParams"] = func(params map[string]any, _ ...string) map[string]any { + processed := cloneMapAny(params) + processed["model"] = upstreamModelName(request.Candidate) + return processed + } context["resolveGetTaskURL"] = func(taskID string) string { return resolveUniversalTaskURL(request.Candidate.PlatformConfig, taskID) } @@ -389,6 +400,20 @@ func universalStatus(result map[string]any) string { return strings.ToLower(strings.TrimSpace(firstNonEmptyString(result["status"], result["state"], result["task_status"]))) } +func universalEffectiveResult(result map[string]any) map[string]any { + nested, ok := result["result"].(map[string]any) + if !ok || nested == nil || isUniversalFailure(result) { + return result + } + out := cloneMapAny(nested) + for _, key := range []string{"status", "request_id", "requestId", "upstream_task_id", "task_id", "taskId", "id"} { + if out[key] == nil && result[key] != nil { + out[key] = result[key] + } + } + return out +} + func universalTaskID(result map[string]any) string { return firstNonEmptyString(result["upstream_task_id"], result["task_id"], result["taskId"], result["id"]) } diff --git a/apps/api/internal/clients/universal_test.go b/apps/api/internal/clients/universal_test.go index 325004d..42579cf 100644 --- a/apps/api/internal/clients/universal_test.go +++ b/apps/api/internal/clients/universal_test.go @@ -92,6 +92,47 @@ func TestUniversalClientDefaultSubmitAndPoll(t *testing.T) { } } +func TestUniversalClientSupportsServerMainScriptContextAndNestedResult(t *testing.T) { + request := Request{ + Kind: "images.generations", + ModelType: "image_generate", + Model: "custom-image", + Body: map[string]any{"model": "custom-image", "prompt": "hello"}, + Candidate: testUniversalCandidate(map[string]any{ + "customGetParamsScript": map[string]any{ + "image_generate": `async function getParams(params, context) { + const processed = await context.preProcessParams(params, context.type); + return { prompt: processed.prompt + "-" + processed.model + "-" + context.processedParams.model }; + }`, + }, + "customSubmitScript": map[string]any{ + "image_generate": `async function submitTask(payload) { + return { + status: "success", + result: { + status: "success", + data: [{ url: "https://cdn.example/" + payload.prompt + ".png" }] + } + }; + }`, + }, + }), + } + + response, err := (UniversalClient{}).Run(context.Background(), request) + if err != nil { + t.Fatalf("run failed: %v", err) + } + data, ok := response.Result["data"].([]any) + if !ok || len(data) != 1 { + t.Fatalf("unexpected nested result: %#v", response.Result) + } + image, ok := data[0].(map[string]any) + if !ok || image["url"] != "https://cdn.example/hello-provider-model-provider-model.png" { + t.Fatalf("unexpected image result: %#v", response.Result) + } +} + func TestUniversalClientResumeSkipsSubmit(t *testing.T) { request := Request{ Kind: "videos.generations", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 524987d..1314d12 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,17 +6,18 @@ settings: overrides: '@babel/core@<=7.29.0': 7.29.6 - '@nx/js>picomatch': 4.0.5 - '@nx/vite>picomatch': 4.0.5 - '@nx/workspace>picomatch': 4.0.5 axios@<1.18.0: 1.18.0 brace-expansion@>=2.0.0 <2.1.2: 2.1.2 dompurify@<=3.4.10: 3.4.11 esbuild@>=0.27.3 <0.28.1: 0.28.1 + fast-uri@>=3.0.0 <3.1.3: 3.1.3 form-data@>=4.0.0 <4.0.6: 4.0.6 js-yaml@<4.3.0: 4.3.0 mermaid@>=11.0.0-alpha.1 <=11.14.0: 11.15.0 minimatch@>=9.0.0 <9.0.7: 9.0.7 + '@nx/js>picomatch': 4.0.5 + '@nx/vite>picomatch': 4.0.5 + '@nx/workspace>picomatch': 4.0.5 tmp@<0.2.7: 0.2.7 importers: @@ -3034,8 +3035,8 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-uri@3.1.2: - resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + fast-uri@3.1.3: + resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} @@ -7181,7 +7182,7 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.2 + fast-uri: 3.1.3 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -7850,7 +7851,7 @@ snapshots: fast-deep-equal@3.1.3: {} - fast-uri@3.1.2: {} + fast-uri@3.1.3: {} fdir@6.5.0(picomatch@4.0.4): optionalDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 012fa01..f407d9b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -12,6 +12,7 @@ overrides: 'brace-expansion@>=2.0.0 <2.1.2': 2.1.2 'dompurify@<=3.4.10': 3.4.11 'esbuild@>=0.27.3 <0.28.1': 0.28.1 + 'fast-uri@>=3.0.0 <3.1.3': 3.1.3 'form-data@>=4.0.0 <4.0.6': 4.0.6 'js-yaml@<4.3.0': 4.3.0 'mermaid@>=11.0.0-alpha.1 <=11.14.0': 11.15.0