vrex-flow-engine 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/PKG-INFO +7 -2
  2. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/PYPI_README.md +6 -1
  3. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/bridge/flow_client.py +9 -2
  4. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/bridge/flow_sdk.py +3 -2
  5. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/cli/app.py +21 -6
  6. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/cli/config.py +14 -0
  7. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/cli/supervisor.py +18 -5
  8. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/pyproject.toml +1 -1
  9. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/vrex_flow_engine.egg-info/PKG-INFO +7 -2
  10. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/vrex_flow_engine.egg-info/SOURCES.txt +0 -1
  11. vrex_flow_engine-0.2.0/README.md +0 -259
  12. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/__init__.py +0 -0
  13. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/bridge/__init__.py +0 -0
  14. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/bridge/ws_server.py +0 -0
  15. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/catalog.py +0 -0
  16. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/cli/__init__.py +0 -0
  17. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/cli/dashboard.py +0 -0
  18. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/cli/health.py +0 -0
  19. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/cli/processes.py +0 -0
  20. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/config.py +0 -0
  21. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/ingest.py +0 -0
  22. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/job_store.py +0 -0
  23. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/jobs.py +0 -0
  24. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/main.py +0 -0
  25. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/media.py +0 -0
  26. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/media_store.py +0 -0
  27. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/openai/__init__.py +0 -0
  28. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/openai/_util.py +0 -0
  29. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/openai/images.py +0 -0
  30. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/openai/models.py +0 -0
  31. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/openai/uploads.py +0 -0
  32. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/openai/videos.py +0 -0
  33. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/pool.py +0 -0
  34. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/posthog_client.py +0 -0
  35. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/session.py +0 -0
  36. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/video_context.py +0 -0
  37. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/flow_engine/video_context_store.py +0 -0
  38. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/setup.cfg +0 -0
  39. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/vrex_flow_engine.egg-info/dependency_links.txt +0 -0
  40. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/vrex_flow_engine.egg-info/entry_points.txt +0 -0
  41. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/vrex_flow_engine.egg-info/requires.txt +0 -0
  42. {vrex_flow_engine-0.2.0 → vrex_flow_engine-0.2.2}/vrex_flow_engine.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vrex-flow-engine
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Launcher + supervisor and OpenAI-compatible image/video generation service (Vrex Flow Engine).
5
5
  Author: Vrex
6
6
  License: Proprietary
@@ -37,6 +37,11 @@ crash-restart), and renders a live status dashboard.
37
37
  uvx vrex-flow-engine start
38
38
  ```
39
39
 
40
+ `uvx` ships with [uv](https://docs.astral.sh/uv/). If you get
41
+ `uvx: command not found`, install uv (`curl -LsSf https://astral.sh/uv/install.sh | sh`,
42
+ or `brew install uv`) and re-run. No uv? `pipx run vrex-flow-engine start`, or
43
+ `pip install --user vrex-flow-engine && flow-engine start`.
44
+
40
45
  First run prompts for the engine API key and the Cloudflare tunnel token (saved
41
46
  to `~/.vrex-flow/config.json`, chmod 600), then brings up the engine + tunnel.
42
47
  Ctrl-C stops both cleanly.
@@ -45,7 +50,7 @@ Ctrl-C stops both cleanly.
45
50
 
46
51
  | Command | What it does |
47
52
  |---|---|
48
- | `vrex-flow-engine start` | Run engine + tunnel with a live dashboard. `--no-tunnel` runs the engine only. |
53
+ | `vrex-flow-engine start` | Run engine + tunnel with a live dashboard. `--no-tunnel` runs the engine only; `--tunnel-config <path>` drives an existing cloudflared config.yml (named tunnel) instead of a token. |
49
54
  | `vrex-flow-engine setup` | Store the engine key + tunnel token. |
50
55
  | `vrex-flow-engine doctor` | Check prerequisites and probe a running engine. |
51
56
  | `vrex-flow-engine serve` | Run just the engine in the foreground. |
@@ -13,6 +13,11 @@ crash-restart), and renders a live status dashboard.
13
13
  uvx vrex-flow-engine start
14
14
  ```
15
15
 
16
+ `uvx` ships with [uv](https://docs.astral.sh/uv/). If you get
17
+ `uvx: command not found`, install uv (`curl -LsSf https://astral.sh/uv/install.sh | sh`,
18
+ or `brew install uv`) and re-run. No uv? `pipx run vrex-flow-engine start`, or
19
+ `pip install --user vrex-flow-engine && flow-engine start`.
20
+
16
21
  First run prompts for the engine API key and the Cloudflare tunnel token (saved
17
22
  to `~/.vrex-flow/config.json`, chmod 600), then brings up the engine + tunnel.
18
23
  Ctrl-C stops both cleanly.
@@ -21,7 +26,7 @@ Ctrl-C stops both cleanly.
21
26
 
22
27
  | Command | What it does |
23
28
  |---|---|
24
- | `vrex-flow-engine start` | Run engine + tunnel with a live dashboard. `--no-tunnel` runs the engine only. |
29
+ | `vrex-flow-engine start` | Run engine + tunnel with a live dashboard. `--no-tunnel` runs the engine only; `--tunnel-config <path>` drives an existing cloudflared config.yml (named tunnel) instead of a token. |
25
30
  | `vrex-flow-engine setup` | Store the engine key + tunnel token. |
26
31
  | `vrex-flow-engine doctor` | Check prerequisites and probe a running engine. |
27
32
  | `vrex-flow-engine serve` | Run just the engine in the foreground. |
@@ -35,6 +35,13 @@ logger = logging.getLogger(__name__)
35
35
  # need to plumb it through from the extension on every call.
36
36
  _FLOW_API_KEY = "AIzaSyBtrm0o5ab1c-Ec8ZuLcGt3oJAA5VWt3pY"
37
37
  _FLOW_CREDITS_URL = "https://aisandbox-pa.googleapis.com/v1/credits"
38
+
39
+ # Concrete paygate tiers Google returns that we accept as "resolved". Google
40
+ # periodically introduces tiers (ONE=Pro, TWO=Ultra, TIER1P5="Tier 1.5"); an
41
+ # unknown or `PAYGATE_TIER_UNSPECIFIED` value means the tier isn't resolved.
42
+ # Single source of truth — flow_sdk imports this for its dispatch-time guard so
43
+ # the two sites can never drift (a drift is what silently blocked TIER1P5).
44
+ VALID_PAYGATE_TIERS = ("PAYGATE_TIER_ONE", "PAYGATE_TIER_TWO", "PAYGATE_TIER_TIER1P5")
38
45
  # Minimum gap between paygate-tier refreshes when the same Bearer token
39
46
  # is re-delivered. Tier rarely changes; 60 s is fine for AccountPanel
40
47
  # freshness and tames the credits-fetch storm an old extension can
@@ -182,9 +189,9 @@ class FlowClient:
182
189
  logger.warning("fetch_paygate_tier: response was not JSON")
183
190
  return False
184
191
  tier = data.get("userPaygateTier")
185
- if tier not in ("PAYGATE_TIER_ONE", "PAYGATE_TIER_TWO"):
192
+ if tier not in VALID_PAYGATE_TIERS:
186
193
  logger.warning(
187
- "fetch_paygate_tier: response missing userPaygateTier (got %r)",
194
+ "fetch_paygate_tier: unsupported userPaygateTier (got %r)",
188
195
  tier,
189
196
  )
190
197
  return False
@@ -17,7 +17,7 @@ import time
17
17
  import uuid
18
18
  from typing import Any, Optional
19
19
 
20
- from flow_engine.bridge.flow_client import FlowClient
20
+ from flow_engine.bridge.flow_client import FlowClient, VALID_PAYGATE_TIERS
21
21
 
22
22
  logger = logging.getLogger(__name__)
23
23
 
@@ -1465,7 +1465,8 @@ def _extract_project_id(resp: Any) -> Optional[str]:
1465
1465
  return None
1466
1466
 
1467
1467
 
1468
- _VALID_TIERS = {"PAYGATE_TIER_ONE", "PAYGATE_TIER_TWO"}
1468
+ # Dispatch-time guard shares flow_client's single tier list (no drift).
1469
+ _VALID_TIERS = set(VALID_PAYGATE_TIERS)
1469
1470
 
1470
1471
 
1471
1472
  def _extract_uploaded_media_id(resp: Any) -> Optional[str]:
@@ -12,6 +12,7 @@ monorepo, `uvx --from 'git+ssh://…/multimodal.git#subdirectory=flow-engine' fl
12
12
  from __future__ import annotations
13
13
 
14
14
  import argparse
15
+ import os
15
16
  import shutil
16
17
  import sys
17
18
 
@@ -39,10 +40,17 @@ def _run_wizard(cfg: FlowConfig) -> FlowConfig:
39
40
  "Engine API key" + (f" [dim]({mask(cfg.engine_api_key)})[/]" if cfg.has_engine_key else ""),
40
41
  password=True, default=cfg.engine_api_key, show_default=False,
41
42
  )
42
- token = Prompt.ask(
43
- "Cloudflare tunnel token" + (f" [dim]({mask(cfg.tunnel_token)})[/]" if cfg.has_tunnel_token else ""),
44
- password=True, default=cfg.tunnel_token, show_default=False,
45
- )
43
+ # Skip the token prompt when a tunnel config file is already configured — the
44
+ # config file carries its own credentials, so no token is needed.
45
+ if cfg.has_tunnel_config:
46
+ console.print(f"[dim]tunnel: using config {cfg.tunnel_config_path}[/]")
47
+ token = cfg.tunnel_token
48
+ else:
49
+ token = Prompt.ask(
50
+ "Cloudflare tunnel token" + (f" [dim]({mask(cfg.tunnel_token)})[/]" if cfg.has_tunnel_token else "")
51
+ + " [dim](blank if using --tunnel-config)[/]",
52
+ password=True, default=cfg.tunnel_token, show_default=False,
53
+ )
46
54
  port = Prompt.ask("HTTP port", default=str(cfg.http_port or DEFAULT_PORT))
47
55
  base = Prompt.ask("Public base URL", default=cfg.public_base_url or DEFAULT_PUBLIC_BASE_URL)
48
56
  cfg.engine_api_key = key.strip()
@@ -68,6 +76,8 @@ def _cmd_start(args: argparse.Namespace) -> int:
68
76
  cfg.engine_api_key = args.engine_key
69
77
  if args.tunnel_token:
70
78
  cfg.tunnel_token = args.tunnel_token
79
+ if args.tunnel_config:
80
+ cfg.tunnel_config_path = os.path.abspath(os.path.expanduser(args.tunnel_config))
71
81
  if args.port:
72
82
  cfg.http_port = args.port
73
83
  if args.public_url:
@@ -75,7 +85,8 @@ def _cmd_start(args: argparse.Namespace) -> int:
75
85
 
76
86
  with_tunnel = not args.no_tunnel
77
87
  # Fill gaps interactively unless told not to; then persist for next time.
78
- needs = not cfg.has_engine_key or (with_tunnel and not cfg.has_tunnel_token)
88
+ # A tunnel config file satisfies the tunnel requirement in place of a token.
89
+ needs = not cfg.has_engine_key or (with_tunnel and not cfg.has_tunnel_auth)
79
90
  if needs and not args.non_interactive:
80
91
  cfg = _run_wizard(cfg)
81
92
  save_config(cfg)
@@ -107,7 +118,10 @@ def _cmd_doctor(_: argparse.Namespace) -> int:
107
118
  line(bool(shutil.which("cloudflared")), "cloudflared installed", shutil.which("cloudflared") or "missing")
108
119
  line(CONFIG_FILE.exists(), "config present", str(CONFIG_FILE))
109
120
  line(cfg.has_engine_key, "engine API key set", mask(cfg.engine_api_key))
110
- line(cfg.has_tunnel_token, "tunnel token set", mask(cfg.tunnel_token))
121
+ if cfg.has_tunnel_config:
122
+ line(os.path.isfile(cfg.tunnel_config_path), "tunnel config file", cfg.tunnel_config_path)
123
+ else:
124
+ line(cfg.has_tunnel_token, "tunnel token set", mask(cfg.tunnel_token))
111
125
  h = health.probe_engine(cfg.http_port)
112
126
  if h:
113
127
  line(True, f"engine responding on :{cfg.http_port}",
@@ -124,6 +138,7 @@ def build_parser() -> argparse.ArgumentParser:
124
138
  ps = sub.add_parser("start", help="run engine + tunnel with a live dashboard")
125
139
  ps.add_argument("--engine-key", help="engine API key (overrides stored/env)")
126
140
  ps.add_argument("--tunnel-token", help="Cloudflare tunnel token (overrides stored/env)")
141
+ ps.add_argument("--tunnel-config", help="path to a cloudflared config.yml (named tunnel; used instead of a token)")
127
142
  ps.add_argument("--port", type=int, help=f"HTTP port (default {DEFAULT_PORT})")
128
143
  ps.add_argument("--public-url", help="public base URL")
129
144
  ps.add_argument("--no-tunnel", action="store_true", help="run the engine only (skip cloudflared)")
@@ -27,6 +27,10 @@ class FlowConfig:
27
27
 
28
28
  engine_api_key: str = ""
29
29
  tunnel_token: str = ""
30
+ # Alternative to a token: a cloudflared config file (named-tunnel with a
31
+ # credentials-file + local ingress). When set, the tunnel runs via
32
+ # `cloudflared tunnel --config <path> run` and no token is needed.
33
+ tunnel_config_path: str = ""
30
34
  http_port: int = DEFAULT_PORT
31
35
  public_base_url: str = DEFAULT_PUBLIC_BASE_URL
32
36
 
@@ -38,6 +42,15 @@ class FlowConfig:
38
42
  def has_tunnel_token(self) -> bool:
39
43
  return bool(self.tunnel_token.strip())
40
44
 
45
+ @property
46
+ def has_tunnel_config(self) -> bool:
47
+ return bool(self.tunnel_config_path.strip())
48
+
49
+ @property
50
+ def has_tunnel_auth(self) -> bool:
51
+ """Either a token or a config file is enough to run the tunnel."""
52
+ return self.has_tunnel_token or self.has_tunnel_config
53
+
41
54
 
42
55
  def load_config() -> FlowConfig:
43
56
  """Read the on-disk config, tolerating a missing file or unknown keys.
@@ -64,6 +77,7 @@ def save_config(cfg: FlowConfig) -> Path:
64
77
  payload = {
65
78
  "engine_api_key": cfg.engine_api_key,
66
79
  "tunnel_token": cfg.tunnel_token,
80
+ "tunnel_config_path": cfg.tunnel_config_path,
67
81
  "http_port": cfg.http_port,
68
82
  "public_base_url": cfg.public_base_url,
69
83
  }
@@ -9,6 +9,7 @@ or SIGTERM stops both gracefully.
9
9
  """
10
10
  from __future__ import annotations
11
11
 
12
+ import os
12
13
  import shutil
13
14
  import signal
14
15
  import socket
@@ -45,8 +46,11 @@ def preflight(cfg: FlowConfig, with_tunnel: bool) -> bool:
45
46
  " other: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/"
46
47
  )
47
48
  ok = False
48
- if with_tunnel and not cfg.has_tunnel_token:
49
- console.print("[red]✗[/] no tunnel token — run [bold]flow-engine setup[/] (or pass --no-tunnel)")
49
+ if with_tunnel and cfg.has_tunnel_config and not os.path.isfile(cfg.tunnel_config_path):
50
+ console.print(f"[red]✗[/] tunnel config not found: {cfg.tunnel_config_path}")
51
+ ok = False
52
+ if with_tunnel and not cfg.has_tunnel_auth:
53
+ console.print("[red]✗[/] no tunnel token or --tunnel-config — run [bold]flow-engine setup[/] (or pass --no-tunnel)")
50
54
  ok = False
51
55
  if _port_in_use(cfg.http_port):
52
56
  console.print(f"[yellow]![/] port {cfg.http_port} already in use — the engine may fail to bind")
@@ -94,14 +98,23 @@ class Supervisor:
94
98
  )
95
99
 
96
100
  def _build_tunnel(self) -> ManagedProcess:
97
- token = self.cfg.tunnel_token
98
- argv = ["cloudflared", "--no-autoupdate", "tunnel", "run", "--token", token]
101
+ # A config file (named tunnel with local ingress) takes precedence over a
102
+ # token — it carries its own credentials-file + `flow.getvrex.com → :8101`
103
+ # ingress, which a token/remotely-managed run does not.
104
+ if self.cfg.has_tunnel_config:
105
+ path = self.cfg.tunnel_config_path
106
+ argv = ["cloudflared", "tunnel", "--config", path, "run"]
107
+ display = f"cloudflared tunnel --config {path} run"
108
+ else:
109
+ token = self.cfg.tunnel_token
110
+ argv = ["cloudflared", "--no-autoupdate", "tunnel", "run", "--token", token]
111
+ display = f"cloudflared --no-autoupdate tunnel run --token {mask(token)}"
99
112
  return ManagedProcess(
100
113
  "tunnel",
101
114
  argv,
102
115
  log_file=LOGS_DIR / "tunnel.log",
103
116
  on_line=self._on_line,
104
- display=f"cloudflared --no-autoupdate tunnel run --token {mask(token)}",
117
+ display=display,
105
118
  )
106
119
 
107
120
  def _supervise(self, proc: ManagedProcess) -> None:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "vrex-flow-engine"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  description = "Launcher + supervisor and OpenAI-compatible image/video generation service (Vrex Flow Engine)."
5
5
  # Neutral public description (the in-repo README.md carries the full internal docs).
6
6
  readme = "PYPI_README.md"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vrex-flow-engine
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Launcher + supervisor and OpenAI-compatible image/video generation service (Vrex Flow Engine).
5
5
  Author: Vrex
6
6
  License: Proprietary
@@ -37,6 +37,11 @@ crash-restart), and renders a live status dashboard.
37
37
  uvx vrex-flow-engine start
38
38
  ```
39
39
 
40
+ `uvx` ships with [uv](https://docs.astral.sh/uv/). If you get
41
+ `uvx: command not found`, install uv (`curl -LsSf https://astral.sh/uv/install.sh | sh`,
42
+ or `brew install uv`) and re-run. No uv? `pipx run vrex-flow-engine start`, or
43
+ `pip install --user vrex-flow-engine && flow-engine start`.
44
+
40
45
  First run prompts for the engine API key and the Cloudflare tunnel token (saved
41
46
  to `~/.vrex-flow/config.json`, chmod 600), then brings up the engine + tunnel.
42
47
  Ctrl-C stops both cleanly.
@@ -45,7 +50,7 @@ Ctrl-C stops both cleanly.
45
50
 
46
51
  | Command | What it does |
47
52
  |---|---|
48
- | `vrex-flow-engine start` | Run engine + tunnel with a live dashboard. `--no-tunnel` runs the engine only. |
53
+ | `vrex-flow-engine start` | Run engine + tunnel with a live dashboard. `--no-tunnel` runs the engine only; `--tunnel-config <path>` drives an existing cloudflared config.yml (named tunnel) instead of a token. |
49
54
  | `vrex-flow-engine setup` | Store the engine key + tunnel token. |
50
55
  | `vrex-flow-engine doctor` | Check prerequisites and probe a running engine. |
51
56
  | `vrex-flow-engine serve` | Run just the engine in the foreground. |
@@ -1,5 +1,4 @@
1
1
  PYPI_README.md
2
- README.md
3
2
  pyproject.toml
4
3
  flow_engine/__init__.py
5
4
  flow_engine/catalog.py
@@ -1,259 +0,0 @@
1
- # flow-engine
2
-
3
- Host a **Google Flow browser-extension instance** and expose **OpenAI-compatible
4
- image/video generation endpoints** backed by Google Flow (labs.google).
5
-
6
- flow-engine drives a Chrome extension that borrows your authenticated Flow
7
- session (token + cookies + reCAPTCHA solving) and proxies Flow's private
8
- `aisandbox-pa.googleapis.com` calls, mapping the results onto familiar OpenAI
9
- request/response shapes. Python package `flow-engine`, import module
10
- `flow_engine`, console command `flow-engine` (subcommands: `start`, `serve`,
11
- `setup`, `doctor`).
12
-
13
- ```
14
- OpenAI client ──HTTP :8101──> flow-engine ──WS :9223──> Chrome extension ──fetch──> Google Flow
15
- (this repo) (extension/, in your browser)
16
- ```
17
-
18
- In production the HTTP surface is published at `https://flow.getvrex.com`
19
- (a `cloudflared` tunnel → `127.0.0.1:8101`); the Next.js app reaches it via
20
- `FLOW_ENGINE_URL`. See "Expose to production" below.
21
-
22
- ## Endpoints
23
-
24
- - `POST /v1/images/generations` — synchronous image (Nano Banana 2).
25
- - `POST /v1/videos/generations` + `GET /v1/videos/{id}` — async video job + poll (Veo 3.1 lite).
26
- - `GET /v1/videos/{id}/debug` — raw `{dispatch, last_poll}` Google payloads for a job (troubleshooting).
27
- - `POST /v1/flow/uploads` — image **or video** bytes → Flow media id (routes by mime).
28
- - `GET /v1/models` — model catalog (cached on disk, refresh with `?refresh=1`).
29
- - `GET /media/{id}` — cached generated bytes at a stable URL.
30
- - `GET /api/health` — liveness + extension-connected status (ungated).
31
-
32
- `/v1/*` and `/api/pool/status` require `Authorization: Bearer <FLOW_ENGINE_API_KEY>`.
33
- `/api/health` and `/media/{id}` are ungated.
34
-
35
- ## Requirements
36
-
37
- - Python 3.10+
38
- - Google Chrome signed in to **labs.google** with a Flow tab open, and the
39
- extension in `extension/` loaded (chrome://extensions → Load unpacked).
40
- - A Flow account (Pro or Ultra — the paygate tier is read from your session).
41
- - `cloudflared` (only for exposing to production).
42
-
43
- > The extension's `background.js` hardcodes `ws://127.0.0.1:9223` and
44
- > `http://127.0.0.1:8101/api/ext/callback`, so flow-engine defaults to those
45
- > ports. It therefore **cannot run alongside the legacy flowboard agent on
46
- > defaults** — change both the env vars and the extension URLs to run both.
47
-
48
- ## Quickstart — one command
49
-
50
- `flow-engine start` is a supervisor: it stores your secrets, launches the engine
51
- **and** the Cloudflare tunnel as child processes, restarts either on crash, and
52
- renders a live status dashboard. Run it with [uv](https://docs.astral.sh/uv/)
53
- (the Python equivalent of `npx` — no manual venv):
54
-
55
- ```bash
56
- # from the monorepo (until published to PyPI):
57
- uvx --from 'git+ssh://git@github.com/getvrex/multimodal.git#subdirectory=flow-engine' flow-engine start
58
-
59
- # once published: uvx flow-engine start
60
- ```
61
-
62
- First run prompts for the **engine API key** (must match the value the Next.js
63
- app sends — `FLOW_ENGINE_API_KEY` on Vrex Multimodal / `.env.prd`) and the
64
- **Cloudflare tunnel token** (dashboard → Zero Trust → Networks → Tunnels → your
65
- tunnel → "run a connector"). Both are saved to `~/.vrex-flow/config.json` (0600)
66
- so later runs need no input. Then it brings up the engine + tunnel and shows:
67
-
68
- ```
69
- 🎬 Vrex Flow Engine ● running uptime 3m02s
70
- ┌ engine ──────────────┐ ┌ tunnel ───────────────┐
71
- │ status ● UP │ │ status ● connected │
72
- │ extension ● connected│ │ public flow.getvrex.com│
73
- │ instances 1 │ │ → 127.0.0.1:8101 │
74
- └──────────────────────┘ └───────────────────────┘
75
- ```
76
-
77
- The one thing the CLI can't automate: open a `labs.google/fx/tools/flow` tab in
78
- Chrome with the `extension/` loaded and signed into a Flow Pro/Ultra account.
79
- Until you do, the dashboard shows `extension ● OPEN A FLOW TAB`. Ctrl-C stops
80
- both processes cleanly.
81
-
82
- ### CLI commands
83
-
84
- | Command | What it does |
85
- |---|---|
86
- | `flow-engine start` | Supervise engine + tunnel + live dashboard (the one-liner). `--no-tunnel` runs the engine only; `--non-interactive` fails instead of prompting. |
87
- | `flow-engine setup` | (Re)store the engine key + tunnel token in `~/.vrex-flow/config.json`. |
88
- | `flow-engine doctor` | Check prerequisites (python, cloudflared, config) and probe a running engine. |
89
- | `flow-engine serve` | Run just the engine in the foreground (for systemd / your own supervisor). |
90
-
91
- Secrets can also come from env (`FLOW_ENGINE_API_KEY`, `CLOUDFLARE_TUNNEL_TOKEN`)
92
- or flags (`--engine-key`, `--tunnel-token`) — flags > env > stored config.
93
-
94
- ## Run manually (without the CLI)
95
-
96
- The engine reads config from the environment directly — **no `.env` auto-load**.
97
- `FLOW_ENGINE_API_KEY` is **required**; the server refuses to boot without it.
98
-
99
- ```bash
100
- cd flow-engine
101
- python -m venv .venv && source .venv/bin/activate
102
- pip install -e .
103
- export FLOW_ENGINE_API_KEY=<shared-secret> # must match the caller's key
104
- flow-engine serve # serves :8101 (HTTP) + :9223 (WS)
105
- # equivalent: python -m flow_engine.main
106
- ```
107
-
108
- Load `extension/` in Chrome, open a `labs.google/fx/tools/flow` tab, then check:
109
-
110
- ```bash
111
- curl localhost:8101/api/health
112
- # {"ok":true,"extension_connected":true,"instances":1}
113
- ```
114
-
115
- `extension_connected:false` means no Flow tab/extension is bridged yet — open
116
- the tab and reload the extension.
117
-
118
- ## Expose to production (Cloudflare Tunnel)
119
-
120
- The public origin `https://flow.getvrex.com` is a `cloudflared` tunnel to the
121
- local `:8101`. **`flow-engine start` runs this tunnel for you** (via the stored
122
- token), so normally you don't touch `cloudflared` directly. To run it standalone
123
- — e.g. the `serve` path — start the tunnel as a separate process on the same
124
- machine:
125
-
126
- ```bash
127
- cloudflared tunnel run --token <your-tunnel-token> # flow.getvrex.com → 127.0.0.1:8101
128
- # or, with a named-tunnel config file:
129
- cloudflared tunnel run --config cloudflared.yml
130
- ```
131
-
132
- If `https://flow.getvrex.com` returns **HTTP 530 / `error code: 1033`**, the
133
- tunnel is down (Cloudflare has no active connection to the origin) — restart it
134
- (or `flow-engine start`). A `502`/`1016` instead means the tunnel is up but the
135
- engine on `:8101` is not — restart the engine.
136
-
137
- ## Examples
138
-
139
- Image (`model` must be `NANO_BANANA_2`, or omit to force it):
140
-
141
- ```bash
142
- curl localhost:8101/v1/images/generations \
143
- -H "authorization: Bearer $FLOW_ENGINE_API_KEY" \
144
- -H 'content-type: application/json' -d '{
145
- "prompt": "a studio portrait, soft light", "n": 2,
146
- "size": "1024x1024", "model": "NANO_BANANA_2"
147
- }'
148
- # {"created":..., "data":[{"url":"http://127.0.0.1:8101/media/<uuid>"}, ...]}
149
- ```
150
-
151
- Video (`model` must be `veo_3_1_lite_low_priority`). Text-to-video:
152
-
153
- ```bash
154
- JOB=$(curl -s localhost:8101/v1/videos/generations \
155
- -H "authorization: Bearer $FLOW_ENGINE_API_KEY" \
156
- -H 'content-type: application/json' -d '{
157
- "prompt": "slow dolly in over a city at dusk",
158
- "model": "veo_3_1_lite_low_priority", "seconds": 8, "size": "1280x720"
159
- }' | jq -r .id)
160
- curl -s localhost:8101/v1/videos/$JOB \
161
- -H "authorization: Bearer $FLOW_ENGINE_API_KEY" # poll until status == "completed"
162
- ```
163
-
164
- Image-to-video — pass the start frame inline; flow-engine uploads it for you:
165
-
166
- ```bash
167
- curl localhost:8101/v1/videos/generations \
168
- -H "authorization: Bearer $FLOW_ENGINE_API_KEY" \
169
- -H 'content-type: application/json' -d "{
170
- \"prompt\": \"the subject turns to camera\", \"model\": \"veo_3_1_lite_low_priority\",
171
- \"start_images\": [\"$(base64 < frame.png)\"], \"size\": \"1280x720\"
172
- }"
173
- ```
174
-
175
- `start_images` / `ref_images` accept a data URL, an `http(s)` URL, or bare
176
- base64. Already have a Flow media id (e.g. from `/v1/flow/uploads`)? Pass it in
177
- `start_media_ids` / `ref_media_ids` instead — the two forms merge. Uploading a
178
- reference **video** (for v2v/edit/extend) works the same way via
179
- `/v1/flow/uploads -F file=@clip.mp4`, which returns a `"kind":"video"` media id.
180
-
181
- Extend or edit a clip you already generated — pass back the `media_id` you got
182
- and flow-engine resolves the rest (scene / workflow / frame count) from what it
183
- remembered this run:
184
-
185
- ```bash
186
- # extend (continue a Veo clip — only Veo-generated videos can be extended)
187
- curl localhost:8101/v1/videos/generations \
188
- -H "authorization: Bearer $FLOW_ENGINE_API_KEY" \
189
- -H 'content-type: application/json' -d '{
190
- "prompt": "a dog joins in", "mode": "extend",
191
- "model": "veo_3_1_lite_low_priority", "source_media_id": "<prev media_id>"
192
- }'
193
-
194
- # edit (video-to-video rewrite)
195
- curl localhost:8101/v1/videos/generations \
196
- -H "authorization: Bearer $FLOW_ENGINE_API_KEY" \
197
- -H 'content-type: application/json' -d '{
198
- "prompt": "add a green ball", "mode": "edit",
199
- "model": "veo_3_1_lite_low_priority", "source_media_id": "<prev media_id>",
200
- "source_seconds": 8
201
- }'
202
- ```
203
-
204
- For a source flow-engine didn't generate this run (e.g. after a restart), pass
205
- `workflow_id` explicitly (extend resolves the scene from it; edit uses it
206
- directly) — plus `source_seconds` for edit so the frame range matches. The
207
- generation-context registry is in-memory, so it resets when the server restarts.
208
-
209
- ## Configuration
210
-
211
- These configure the **engine** process (read from its environment). The
212
- `flow-engine start` launcher injects `FLOW_ENGINE_API_KEY` + ports for you from
213
- `~/.vrex-flow/config.json`; set these directly only for the manual `serve` path.
214
- All optional except the API key. Port/host/storage vars keep the `FLOWPROXY_`
215
- prefix for extension compatibility; the API key and pool size use the
216
- `FLOW_ENGINE_` / `FLOW_POOL_` names.
217
-
218
- | Env var | Default | Purpose |
219
- |---|---|---|
220
- | `FLOW_ENGINE_API_KEY` | — (**required**) | Bearer key gating `/v1/*`. Legacy `FLOWPROXY_API_KEY` still accepted. |
221
- | `FLOWPROXY_HTTP_PORT` | `8101` | HTTP surface (OpenAI endpoints + `/media` + callback). |
222
- | `FLOWPROXY_WS_HOST` | `127.0.0.1` | Extension WS bind — **must be loopback** (unauthenticated by design). |
223
- | `FLOWPROXY_EXT_WS_PORT` | `9223` | Extension WebSocket port. |
224
- | `FLOWPROXY_PUBLIC_BASE_URL` | `http://127.0.0.1:8101` | Base used to build absolute `/media` URLs returned to clients. |
225
- | `FLOW_POOL_CONCURRENCY` | `4` | Max concurrent in-flight Flow calls per instance (back-pressure, no 503). |
226
- | `FLOWPROXY_STORAGE` | `./storage` | Local byte cache dir. |
227
- | `FLOWPROXY_CATALOG_TTL` | `86400` | Model-catalog cache TTL (seconds). |
228
- | `VIDEO_POLL_MAX_CYCLES` / `VIDEO_POLL_INTERVAL_S` | `72` / `10` | Video poll budget (default 12 min). |
229
-
230
- ## Layout
231
-
232
- ```
233
- extension/ Chrome MV3 bridge (open a labs.google/fx/tools/flow tab)
234
- flow_engine/
235
- bridge/ WS server + flow_client + flow_sdk (Flow API port)
236
- media.py DB-free media cache (registry + on-disk bytes)
237
- jobs.py in-memory job store + async video poller
238
- session.py readiness gate + default project + pool seam
239
- catalog.py model catalog (flow.projectInitialData), disk-cached
240
- openai/ /v1/images, /v1/videos, /v1/flow/uploads, /v1/models
241
- main.py FastAPI app, /api/ext/callback, /media/{id}, lifespan
242
- cli/ launcher: app (argparse), supervisor, dashboard,
243
- processes, health, config (~/.vrex-flow)
244
- ```
245
-
246
- ## Notes & caveats
247
-
248
- - **Single account.** One extension instance = one Google account = one tier.
249
- Multi-tenant needs a browser-instance pool routed by API key — the
250
- `session.get_bridge(api_key)` indirection is where that goes.
251
- - **Live Flow tab required.** Every dispatch solves an enterprise reCAPTCHA in
252
- an open Flow tab; hosting needs a headful/virtual-display Chrome kept warm.
253
- - **Signed URLs expire.** flow-engine caches bytes locally and serves stable
254
- `/media/{id}` URLs; workflow-mode video arrives as inline base64 MP4.
255
- - **Localhost WS is unauthenticated** by design — never bind it to a network
256
- interface. Gate the HTTP `/v1/*` surface with `FLOW_ENGINE_API_KEY` when exposed.
257
- - **No `.env` auto-load.** The process reads `os.environ` directly — export vars
258
- in the shell (or a wrapper/launchd unit), don't rely on a `.env` file.
259
- ```