From 6c2a942a887cfe9cab9a9664507d0f9b3fc1b69f Mon Sep 17 00:00:00 2001 From: chiyi Date: Thu, 9 Jul 2026 16:21:31 +0800 Subject: [PATCH] feat(providers): route only the generate submit through the proxy Extends the adobe split to leonardo, krea, imagine, runway (image+video) and chatgpt: reference-image upload, polling and result download egress on the local IP; only the generate/task-create submit uses proxy.url. chatgpt keeps its anti-bot-guarded submit+upload phase on the proxy and moves only the existing second (poll/resolve/download) session to local. custom is already direct. Build+vet pass; adobe verified live, the other providers are code-complete but not yet live-tested (no dev accounts). --- backend/internal/provider/chatgpt/client.go | 17 +++++++++++++-- backend/internal/provider/imagine/client.go | 17 ++++++++++++--- backend/internal/provider/imagine/image.go | 4 ++-- backend/internal/provider/krea/client.go | 17 ++++++++++++--- backend/internal/provider/krea/image.go | 16 +++++++++----- backend/internal/provider/leonardo/client.go | 23 +++++++++++++++----- backend/internal/provider/leonardo/image.go | 6 ++--- backend/internal/provider/runway/client.go | 10 +++++++-- backend/internal/provider/runway/image.go | 16 +++++++++----- backend/internal/provider/runway/video.go | 22 ++++++++++++------- 10 files changed, 109 insertions(+), 39 deletions(-) diff --git a/backend/internal/provider/chatgpt/client.go b/backend/internal/provider/chatgpt/client.go index 51458bd..c4cf337 100644 --- a/backend/internal/provider/chatgpt/client.go +++ b/backend/internal/provider/chatgpt/client.go @@ -110,7 +110,9 @@ func (c *Client) GenerateImage(ctx context.Context, accessToken, prompt, model, refIDs := uploadedRefIDSet(uploadedRefs) fileIDs = dropIDs(fileIDs, refIDs) sedimentIDs = dropIDs(sedimentIDs, refIDs) - session, err = c.newSession(accessToken) + // Poll / resolve / download run on the local IP (fresh direct session); + // only the submit phase above egressed via the proxy. + session, err = c.newDirectSession(accessToken) if err != nil { return nil, nil, err } @@ -219,6 +221,17 @@ type chatRequirements struct { } func (c *Client) newSession(accessToken string) (tlsclient.HttpClient, error) { + return c.newSessionP(accessToken, true) +} + +// newDirectSession egresses on the local IP (never the proxy). Used for the +// poll / resolve / download phase; only the anti-bot-guarded submit phase +// (bootstrap, chat-requirements, upload, conversation create) uses the proxy. +func (c *Client) newDirectSession(accessToken string) (tlsclient.HttpClient, error) { + return c.newSessionP(accessToken, false) +} + +func (c *Client) newSessionP(accessToken string, useProxy bool) (tlsclient.HttpClient, error) { options := []tlsclient.HttpClientOption{ tlsclient.WithTimeoutSeconds(600), // Match the Python reference (curl_cffi impersonate="chrome110"): the @@ -226,7 +239,7 @@ func (c *Client) newSession(accessToken string) (tlsclient.HttpClient, error) { tlsclient.WithClientProfile(profiles.Chrome_110), tlsclient.WithRandomTLSExtensionOrder(), } - if c.proxy != "" { + if useProxy && c.proxy != "" { options = append(options, tlsclient.WithProxyUrl(c.proxy)) } client, err := tlsclient.NewHttpClient(tlsclient.NewNoopLogger(), options...) diff --git a/backend/internal/provider/imagine/client.go b/backend/internal/provider/imagine/client.go index 7f95880..5c08c52 100644 --- a/backend/internal/provider/imagine/client.go +++ b/backend/internal/provider/imagine/client.go @@ -337,7 +337,12 @@ func (c *Client) FetchCreditsBalance(ctx context.Context, cred string) (map[stri // --------------------------------------------------------------------------- func (c *Client) apiGet(ctx context.Context, token, url string) ([]byte, int, error) { - client, err := c.newTLSClient() + return c.apiGetP(ctx, token, url, true) +} + +// apiGetP picks the egress: polling runs direct (local IP). +func (c *Client) apiGetP(ctx context.Context, token, url string, useProxy bool) ([]byte, int, error) { + client, err := c.newTLSClientP(useProxy) if err != nil { return nil, 0, err } @@ -365,12 +370,18 @@ func (c *Client) apiGet(ctx context.Context, token, url string) ([]byte, int, er return b, resp.StatusCode, err } -func (c *Client) newTLSClient() (tlsclient.HttpClient, error) { +func (c *Client) newTLSClient() (tlsclient.HttpClient, error) { return c.newTLSClientP(true) } + +// newDirectTLSClient egresses on the local IP (never the proxy). Used for +// polling and result download. +func (c *Client) newDirectTLSClient() (tlsclient.HttpClient, error) { return c.newTLSClientP(false) } + +func (c *Client) newTLSClientP(useProxy bool) (tlsclient.HttpClient, error) { options := []tlsclient.HttpClientOption{ tlsclient.WithTimeoutSeconds(60), tlsclient.WithClientProfile(profiles.Chrome_120), } - if c.proxy != "" { + if useProxy && c.proxy != "" { options = append(options, tlsclient.WithProxyUrl(c.proxy)) } return tlsclient.NewHttpClient(tlsclient.NewNoopLogger(), options...) diff --git a/backend/internal/provider/imagine/image.go b/backend/internal/provider/imagine/image.go index db5e364..125d616 100644 --- a/backend/internal/provider/imagine/image.go +++ b/backend/internal/provider/imagine/image.go @@ -101,7 +101,7 @@ func (c *Client) pollImage(ctx context.Context, token, userID, batchID string) ( url := teamsBase + "/v1/org/" + userID + "/objects?batch=true&limit=50&service=image,chat-image" for { - body, status, err := c.apiGet(ctx, token, url) + body, status, err := c.apiGetP(ctx, token, url, false) if err == nil && status == 200 { var resp struct { Data []struct { @@ -181,7 +181,7 @@ func firstImageURL(raw string) string { } func (c *Client) download(ctx context.Context, url string) ([]byte, error) { - client, err := c.newTLSClient() + client, err := c.newDirectTLSClient() if err != nil { return nil, err } diff --git a/backend/internal/provider/krea/client.go b/backend/internal/provider/krea/client.go index f4a845f..ad7cb7e 100644 --- a/backend/internal/provider/krea/client.go +++ b/backend/internal/provider/krea/client.go @@ -394,7 +394,12 @@ func accountKey(cookie string) string { // apiGet issues a GET to a krea.ai API path carrying the account cookie. func (c *Client) apiGet(ctx context.Context, cookie, path string) ([]byte, int, error) { - client, err := c.newTLSClient() + return c.apiGetP(ctx, cookie, path, true) +} + +// apiGetP picks the egress: polling / asset resolution run direct (local IP). +func (c *Client) apiGetP(ctx context.Context, cookie, path string, useProxy bool) ([]byte, int, error) { + client, err := c.newTLSClientP(useProxy) if err != nil { return nil, 0, err } @@ -426,12 +431,18 @@ func (c *Client) apiGet(ctx context.Context, cookie, path string) ([]byte, int, return b, resp.StatusCode, err } -func (c *Client) newTLSClient() (tlsclient.HttpClient, error) { +func (c *Client) newTLSClient() (tlsclient.HttpClient, error) { return c.newTLSClientP(true) } + +// newDirectTLSClient egresses on the local IP (never the proxy). Used for +// reference-image upload, polling and result download. +func (c *Client) newDirectTLSClient() (tlsclient.HttpClient, error) { return c.newTLSClientP(false) } + +func (c *Client) newTLSClientP(useProxy bool) (tlsclient.HttpClient, error) { options := []tlsclient.HttpClientOption{ tlsclient.WithTimeoutSeconds(60), tlsclient.WithClientProfile(profiles.Chrome_120), } - if c.proxy != "" { + if useProxy && c.proxy != "" { options = append(options, tlsclient.WithProxyUrl(c.proxy)) } return tlsclient.NewHttpClient(tlsclient.NewNoopLogger(), options...) diff --git a/backend/internal/provider/krea/image.go b/backend/internal/provider/krea/image.go index 714fc91..6fbef9b 100644 --- a/backend/internal/provider/krea/image.go +++ b/backend/internal/provider/krea/image.go @@ -67,7 +67,7 @@ func (c *Client) uploadImage(ctx context.Context, cookie string, img []byte) (st return "", err } _ = w.Close() - body, status, err := c.apiPost(ctx, cookie, "/api/upload?", w.FormDataContentType(), buf.Bytes()) + body, status, err := c.apiPostP(ctx, cookie, "/api/upload?", w.FormDataContentType(), buf.Bytes(), false) if err != nil { return "", fmt.Errorf("%w: upload: %s", ErrTemporaryUpstream, err.Error()) } @@ -184,7 +184,7 @@ func (c *Client) pollImage(ctx context.Context, cookie, jobID string) (string, e deadline := time.Now().Add(4 * time.Minute) for { - body, status, err := c.apiGet(ctx, cookie, "/api/job-status?id="+jobID) + body, status, err := c.apiGetP(ctx, cookie, "/api/job-status?id="+jobID, false) if err == nil && status == 200 { var js struct { Status string `json:"status"` @@ -215,7 +215,7 @@ func (c *Client) pollImage(ctx context.Context, cookie, jobID string) (string, e // assetForJob finds the generated asset produced by a job and returns its URL. func (c *Client) assetForJob(ctx context.Context, cookie, jobID string) (string, error) { - body, status, err := c.apiGet(ctx, cookie, "/api/assets?filter=generated&offset=0") + body, status, err := c.apiGetP(ctx, cookie, "/api/assets?filter=generated&offset=0", false) if err != nil || status != 200 { return "", fmt.Errorf("assets http %d", status) } @@ -237,7 +237,7 @@ func (c *Client) assetForJob(ctx context.Context, cookie, jobID string) (string, } func (c *Client) download(ctx context.Context, url string) ([]byte, error) { - client, err := c.newTLSClient() + client, err := c.newDirectTLSClient() if err != nil { return nil, err } @@ -268,7 +268,13 @@ func (c *Client) download(ctx context.Context, url string) ([]byte, error) { // apiPost issues a POST with a raw body + content-type, carrying the cookie. func (c *Client) apiPost(ctx context.Context, cookie, path, contentType string, body []byte) ([]byte, int, error) { - client, err := c.newTLSClient() + return c.apiPostP(ctx, cookie, path, contentType, body, true) +} + +// apiPostP picks the egress: reference-image upload runs direct (local IP), the +// generate submit uses the proxy. +func (c *Client) apiPostP(ctx context.Context, cookie, path, contentType string, body []byte, useProxy bool) ([]byte, int, error) { + client, err := c.newTLSClientP(useProxy) if err != nil { return nil, 0, err } diff --git a/backend/internal/provider/leonardo/client.go b/backend/internal/provider/leonardo/client.go index 83c3bff..a476fa8 100644 --- a/backend/internal/provider/leonardo/client.go +++ b/backend/internal/provider/leonardo/client.go @@ -248,10 +248,15 @@ func (c *Client) FetchCreditsBalance(ctx context.Context, cookie string) (map[st }, nil } -// graphql POSTs a GraphQL body to the Leonardo API with the bearer + schema header, -// returning the raw response body and status. +// graphql runs a GraphQL call through the proxy. graphqlP lets callers pick the +// egress: only the generate submit uses the proxy; reference-image upload and +// polling run direct (local IP). func (c *Client) graphql(ctx context.Context, accessToken string, payload []byte) ([]byte, int, error) { - client, err := c.newTLSClient() + return c.graphqlP(ctx, accessToken, payload, true) +} + +func (c *Client) graphqlP(ctx context.Context, accessToken string, payload []byte, useProxy bool) ([]byte, int, error) { + client, err := c.newTLSClientP(useProxy) if err != nil { return nil, 0, err } @@ -300,7 +305,13 @@ func unknownBalance(reason string) map[string]any { } } -func (c *Client) newTLSClient() (tlsclient.HttpClient, error) { +func (c *Client) newTLSClient() (tlsclient.HttpClient, error) { return c.newTLSClientP(true) } + +// newDirectTLSClient egresses on the local IP (never the proxy). Used for +// reference-image upload, polling and result download. +func (c *Client) newDirectTLSClient() (tlsclient.HttpClient, error) { return c.newTLSClientP(false) } + +func (c *Client) newTLSClientP(useProxy bool) (tlsclient.HttpClient, error) { // Match the fingerprint proven to work against Leonardo's Cloudflare edge: // Chrome_120, fixed extension order. A randomized JA3 (Chrome_133 + // WithRandomTLSExtensionOrder) gets flagged and 429'd at get-session. @@ -308,7 +319,7 @@ func (c *Client) newTLSClient() (tlsclient.HttpClient, error) { tlsclient.WithTimeoutSeconds(60), tlsclient.WithClientProfile(profiles.Chrome_120), } - if c.proxy != "" { + if useProxy && c.proxy != "" { options = append(options, tlsclient.WithProxyUrl(c.proxy)) } return tlsclient.NewHttpClient(tlsclient.NewNoopLogger(), options...) @@ -319,7 +330,7 @@ func (c *Client) downloadImage(ctx context.Context, imageURL string) ([]byte, er if _, err := url.Parse(imageURL); err != nil { return nil, err } - client, err := c.newTLSClient() + client, err := c.newDirectTLSClient() if err != nil { return nil, err } diff --git a/backend/internal/provider/leonardo/image.go b/backend/internal/provider/leonardo/image.go index 35f4bd9..33b8e91 100644 --- a/backend/internal/provider/leonardo/image.go +++ b/backend/internal/provider/leonardo/image.go @@ -57,7 +57,7 @@ func (c *Client) uploadInitImage(ctx context.Context, accessToken string, img [] "query": mUploadImage, "variables": map[string]any{"uploadImageInput": map[string]any{"uploadType": "INIT", "extension": "png"}}, }) - body, status, err := c.graphql(ctx, accessToken, payload) + body, status, err := c.graphqlP(ctx, accessToken, payload, false) if err != nil { return "", fmt.Errorf("%w: upload-init: %s", ErrTemporaryUpstream, err.Error()) } @@ -106,7 +106,7 @@ func (c *Client) uploadInitImage(ctx context.Context, accessToken string, img [] } _ = w.Close() - client, err := c.newTLSClient() + client, err := c.newDirectTLSClient() if err != nil { return "", err } @@ -256,7 +256,7 @@ func (c *Client) pollImage(ctx context.Context, accessToken, genID string) (stri deadline := time.Now().Add(5 * time.Minute) for { - body, status, err := c.graphql(ctx, accessToken, payload) + body, status, err := c.graphqlP(ctx, accessToken, payload, false) if err != nil { return "", fmt.Errorf("%w: poll: %s", ErrTemporaryUpstream, err.Error()) } diff --git a/backend/internal/provider/runway/client.go b/backend/internal/provider/runway/client.go index fa9667a..52c1716 100644 --- a/backend/internal/provider/runway/client.go +++ b/backend/internal/provider/runway/client.go @@ -180,13 +180,19 @@ func unknownBalance(reason string) map[string]any { } } -func (c *Client) newTLSClient() (tlsclient.HttpClient, error) { +func (c *Client) newTLSClient() (tlsclient.HttpClient, error) { return c.newTLSClientP(true) } + +// newDirectTLSClient egresses on the local IP (never the proxy). Used for +// reference-image upload, polling and result download. +func (c *Client) newDirectTLSClient() (tlsclient.HttpClient, error) { return c.newTLSClientP(false) } + +func (c *Client) newTLSClientP(useProxy bool) (tlsclient.HttpClient, error) { options := []tlsclient.HttpClientOption{ tlsclient.WithTimeoutSeconds(30), tlsclient.WithClientProfile(profiles.Chrome_133), tlsclient.WithRandomTLSExtensionOrder(), } - if c.proxy != "" { + if useProxy && c.proxy != "" { options = append(options, tlsclient.WithProxyUrl(c.proxy)) } return tlsclient.NewHttpClient(tlsclient.NewNoopLogger(), options...) diff --git a/backend/internal/provider/runway/image.go b/backend/internal/provider/runway/image.go index a2c380e..dde1c56 100644 --- a/backend/internal/provider/runway/image.go +++ b/backend/internal/provider/runway/image.go @@ -39,7 +39,13 @@ func (c *Client) GenerateImage(ctx context.Context, token, teamID, prompt, aspec imageSize = "1K" } - client, err := c.newTLSClient() + // Only the task-create (generate submit) egresses via the proxy; reference + // upload, polling and download run on the local IP. + submitClient, err := c.newTLSClient() + if err != nil { + return nil, nil, err + } + directClient, err := c.newDirectTLSClient() if err != nil { return nil, nil, err } @@ -50,7 +56,7 @@ func (c *Client) GenerateImage(ctx context.Context, token, teamID, prompt, aspec continue } filename := fmt.Sprintf("ref_%s_%d.png", time.Now().UTC().Format("20060102_150405"), i+1) - assetID, url, upErr := c.uploadReference(ctx, client, token, teamID, filename, raw) + assetID, url, upErr := c.uploadReference(ctx, directClient, token, teamID, filename, raw) if upErr != nil { return nil, nil, upErr } @@ -61,15 +67,15 @@ func (c *Client) GenerateImage(ctx context.Context, token, teamID, prompt, aspec }) } - taskID, err := c.createImageTask(ctx, client, token, teamID, prompt, aspectRatio, imageSize, refImages) + taskID, err := c.createImageTask(ctx, submitClient, token, teamID, prompt, aspectRatio, imageSize, refImages) if err != nil { return nil, nil, err } - artifactURL, err := c.pollTask(ctx, client, token, teamID, taskID) + artifactURL, err := c.pollTask(ctx, directClient, token, teamID, taskID) if err != nil { return nil, nil, err } - data, err := c.download(ctx, client, artifactURL) + data, err := c.download(ctx, directClient, artifactURL) if err != nil { return nil, nil, err } diff --git a/backend/internal/provider/runway/video.go b/backend/internal/provider/runway/video.go index 66e405c..463ceb7 100644 --- a/backend/internal/provider/runway/video.go +++ b/backend/internal/provider/runway/video.go @@ -70,33 +70,39 @@ func (c *Client) GenerateVideo(ctx context.Context, token, teamID, prompt, aspec return nil, nil, errors.New("runway: failed to decode first-frame image") } - client, err := c.newTLSClient() + // Only the task-create (generate submit) egresses via the proxy; first-frame + // upload, polling and download run on the local IP. + submitClient, err := c.newTLSClient() + if err != nil { + return nil, nil, err + } + directClient, err := c.newDirectTLSClient() if err != nil { return nil, nil, err } filename := "frame_" + time.Now().UTC().Format("20060102_150405") + ".png" - previewUploadID, _, err := c.uploadFile(ctx, client, token, teamID, filename, "DATASET_PREVIEW", frame) + previewUploadID, _, err := c.uploadFile(ctx, directClient, token, teamID, filename, "DATASET_PREVIEW", frame) if err != nil { return nil, nil, err } - datasetUploadID, _, err := c.uploadFile(ctx, client, token, teamID, filename, "DATASET", frame) + datasetUploadID, _, err := c.uploadFile(ctx, directClient, token, teamID, filename, "DATASET", frame) if err != nil { return nil, nil, err } - assetID, imageURL, err := c.createDataset(ctx, client, token, teamID, filename, datasetUploadID, previewUploadID, cfg.Width, cfg.Height) + assetID, imageURL, err := c.createDataset(ctx, directClient, token, teamID, filename, datasetUploadID, previewUploadID, cfg.Width, cfg.Height) if err != nil { return nil, nil, err } - assetGroupID, _ := c.assetGroupID(ctx, client, token, teamID) // best-effort + assetGroupID, _ := c.assetGroupID(ctx, directClient, token, teamID) // best-effort - taskID, err := c.createTask(ctx, client, token, teamID, prompt, imageURL, assetID, assetGroupID, aspectRatio, seconds) + taskID, err := c.createTask(ctx, submitClient, token, teamID, prompt, imageURL, assetID, assetGroupID, aspectRatio, seconds) if err != nil { return nil, nil, err } - artifactURL, err := c.pollTask(ctx, client, token, teamID, taskID) + artifactURL, err := c.pollTask(ctx, directClient, token, teamID, taskID) if err != nil { return nil, nil, err } @@ -109,7 +115,7 @@ func (c *Client) GenerateVideo(ctx context.Context, token, teamID, prompt, aspec if !downloadResult { return nil, meta, nil } - data, err := c.download(ctx, client, artifactURL) + data, err := c.download(ctx, directClient, artifactURL) if err != nil { return nil, nil, err }