Skip to content

Commit aae5afc

Browse files
Uniform region/environment selection: ROBOFLOW_REGION x ROBOFLOW_ENVIRONMENT across inference, cli, sdk and models (#2701)
* Add ROBOFLOW_REGION env var for easy EU region selection in inference-cli ROBOFLOW_REGION=eu points the CLI's Roboflow API defaults at https://api.roboflow.eu (the EU data-residency platform). Explicit API_BASE_URL / ROBOFLOW_API_HOST overrides still win, unknown region values warn and fall back to US, and with no region set behavior is unchanged. Mirrors the region switch shipping in roboflow-python (ROBOFLOW_REGION / roboflow auth login --region eu). INF-314 * Generalize region/environment selection: ROBOFLOW_REGION x ROBOFLOW_ENVIRONMENT matrix propagated to all components Replace the one-off EU branch with a canonical region/environment registry (inference_sdk.regions) resolving default hosts from the 2x2 matrix of ROBOFLOW_REGION (us/eu) and ROBOFLOW_ENVIRONMENT (prod/staging), and wire it into inference, inference-cli, inference-sdk and inference-models the same way. Explicit URL env variables keep precedence; legacy PROJECT signal still honored, with ROBOFLOW_ENVIRONMENT taking precedence when both are set. inference-models mirrors the matrix since it is a standalone distribution. EU serverless hosts added to ALL_ROBOFLOW_API_URLS so EU URLs get V0 client mode and TURN auto-fetch. * Add mirroring for utils * Add missing files --------- Co-authored-by: Paweł Pęczek <pawel@roboflow.com> Co-authored-by: Paweł Pęczek <146137186+PawelPeczek-Roboflow@users.noreply.github.com>
1 parent 8b1bd5c commit aae5afc

12 files changed

Lines changed: 759 additions & 23 deletions

File tree

docs/server_configuration/environmental_variables.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Below is a list of some environmental values that require more in-depth explanat
66

77
Environmental variable | Description | Default
88
-----------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------
9+
`ROBOFLOW_REGION` | Roboflow region to run against: `us` or `eu`. Selects the default API/app hosts across `inference`, `inference-cli`, `inference-sdk` and `inference-models` (e.g. `eu` points the API at `https://api.roboflow.eu`). Explicit URL variables such as `API_BASE_URL` or `ROBOFLOW_API_HOST` always take precedence. | `us`
10+
`ROBOFLOW_ENVIRONMENT` | Roboflow environment to run against: `prod` or `staging`. Combines with `ROBOFLOW_REGION` to pick default hosts (e.g. `eu` + `staging` points the API at `https://api.roboflow-eu.one`). Takes precedence over the legacy `PROJECT` variable (`roboflow-platform` vs `roboflow-staging`). | `prod`
911
`ONNXRUNTIME_EXECUTION_PROVIDERS` | List of execution providers in priority order, warning message will be displayed if provider is not supported on user platform | See [here](https://github.com/roboflow/inference/blob/main/inference/core/env.py#L262)
1012
`SAM2_MAX_EMBEDDING_CACHE_SIZE` | The number of sam2 embeddings that will be held in memory. The embeddings will be held in gpu memory. Each embedding takes 16777216 bytes. | 100
1113
`SAM2_MAX_LOGITS_CACHE_SIZE` | The number of sam2 logits that will be held in memory. The the logits will be in cpu memory. Each logit takes 262144 bytes. | 1000

inference/core/env.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
from dotenv import load_dotenv
1111

1212
from inference.core.utils.environment import safe_split_value, str2bool
13+
from inference.core.utils.regions import (
14+
get_roboflow_region,
15+
resolve_roboflow_service_url,
16+
)
1317
from inference.core.warnings import (
1418
InferenceConfigurationWarning,
1519
InferenceDeprecationWarning,
@@ -39,6 +43,9 @@
3943
# The project name, default is "roboflow-platform"
4044
PROJECT = os.getenv("PROJECT", "roboflow-platform")
4145

46+
# Selected Roboflow region ("us" or "eu"), default is "us"
47+
ROBOFLOW_REGION = get_roboflow_region()
48+
4249
# Allow numpy input, default is False
4350
ALLOW_NUMPY_INPUT = str2bool(os.getenv("ALLOW_NUMPY_INPUT", False))
4451
ALLOW_URL_INPUT = str2bool(os.getenv("ALLOW_URL_INPUT", True))
@@ -88,11 +95,7 @@
8895
# Base URL for the API
8996
API_BASE_URL = os.getenv(
9097
"API_BASE_URL",
91-
(
92-
"https://api.roboflow.com"
93-
if PROJECT == "roboflow-platform"
94-
else "https://api.roboflow.one"
95-
),
98+
resolve_roboflow_service_url("api", region=ROBOFLOW_REGION, project=PROJECT),
9699
)
97100
API_PROXY_BASE_URL = os.getenv("API_PROXY_BASE_URL", API_BASE_URL)
98101

@@ -689,7 +692,10 @@ def _reset_offline_mode_lock_after_fork() -> None:
689692

690693
# Enable the builder, default is False
691694
ENABLE_BUILDER = str2bool(os.getenv("ENABLE_BUILDER", False))
692-
BUILDER_ORIGIN = os.getenv("BUILDER_ORIGIN", "https://app.roboflow.com")
695+
BUILDER_ORIGIN = os.getenv(
696+
"BUILDER_ORIGIN",
697+
resolve_roboflow_service_url("app", region=ROBOFLOW_REGION, project=PROJECT),
698+
)
693699

694700
# Enable jupyter notebook server route, default is False
695701
NOTEBOOK_ENABLED = str2bool(os.getenv("NOTEBOOK_ENABLED", False))

inference/core/utils/regions.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""Canonical Roboflow region / environment selection.
2+
3+
Single source of truth for resolving default Roboflow service URLs from the
4+
two public switches:
5+
6+
* ``ROBOFLOW_REGION`` - ``us`` (default) or ``eu``
7+
* ``ROBOFLOW_ENVIRONMENT`` - ``prod`` (default) or ``staging``
8+
9+
Explicit URL env variables (``API_BASE_URL``, ``ROBOFLOW_API_HOST``,
10+
``RF_API_BASE_URL``, ...) always take precedence over values resolved here -
11+
callers apply this module's output only as the default. The legacy
12+
``PROJECT`` variable (``roboflow-platform`` vs anything else) is honoured as
13+
an environment signal wherever it was honoured before, with
14+
``ROBOFLOW_ENVIRONMENT`` taking precedence when both are set.
15+
16+
This module deliberately mirrors ``inference_sdk/regions.py`` - ``inference``
17+
core config must not import ``inference_sdk`` at module level. Keep both
18+
copies (and the inline matrix in ``inference_models/configuration.py``) in
19+
sync; a parity test guards the first two.
20+
"""
21+
22+
import os
23+
import warnings
24+
from typing import Optional
25+
26+
PROD_ENVIRONMENT_NAME = "prod"
27+
STAGING_ENVIRONMENT_NAME = "staging"
28+
US_PROD_PROJECT_NAME = "roboflow-platform"
29+
30+
DEFAULT_REGION = "us"
31+
DEFAULT_ENVIRONMENT = PROD_ENVIRONMENT_NAME
32+
33+
ROBOFLOW_SERVICE_URLS = {
34+
("us", "prod"): {
35+
"api": "https://api.roboflow.com",
36+
"app": "https://app.roboflow.com",
37+
"serverless": "https://serverless.roboflow.com",
38+
},
39+
("us", "staging"): {
40+
"api": "https://api.roboflow.one",
41+
"app": "https://app.roboflow.one",
42+
"serverless": "https://serverless.roboflow.one",
43+
},
44+
("eu", "prod"): {
45+
"api": "https://api.roboflow.eu",
46+
"app": "https://app.roboflow.eu",
47+
"serverless": "https://serverless.roboflow.eu",
48+
},
49+
("eu", "staging"): {
50+
"api": "https://api.roboflow-eu.one",
51+
"app": "https://app.roboflow-eu.one",
52+
"serverless": "https://serverless.roboflow-eu.one",
53+
},
54+
}
55+
56+
SUPPORTED_REGIONS = sorted({region for region, _ in ROBOFLOW_SERVICE_URLS})
57+
58+
59+
def get_roboflow_region() -> str:
60+
"""Return the selected Roboflow region (``us`` or ``eu``).
61+
62+
Unknown values warn on stderr and fall back to ``us`` - never raise at
63+
import time.
64+
"""
65+
region = os.getenv("ROBOFLOW_REGION", DEFAULT_REGION).strip().lower()
66+
if region not in SUPPORTED_REGIONS:
67+
warnings.warn(
68+
f"Unknown ROBOFLOW_REGION {region!r} - falling back to "
69+
f"{DEFAULT_REGION!r}. Supported regions: {', '.join(SUPPORTED_REGIONS)}.",
70+
)
71+
return DEFAULT_REGION
72+
return region
73+
74+
75+
def get_roboflow_environment(project: Optional[str] = None) -> str:
76+
"""Return the selected Roboflow environment (``prod`` or ``staging``).
77+
78+
``ROBOFLOW_ENVIRONMENT`` wins when set (any value other than ``prod``
79+
selects staging, matching historical behaviour). Otherwise the legacy
80+
``project`` signal decides (``roboflow-platform`` means prod), and with
81+
neither present the environment defaults to prod.
82+
"""
83+
environment = os.getenv("ROBOFLOW_ENVIRONMENT")
84+
if environment is not None:
85+
if environment.strip().lower() == PROD_ENVIRONMENT_NAME:
86+
return PROD_ENVIRONMENT_NAME
87+
return STAGING_ENVIRONMENT_NAME
88+
if project is not None and project != US_PROD_PROJECT_NAME:
89+
return STAGING_ENVIRONMENT_NAME
90+
return DEFAULT_ENVIRONMENT
91+
92+
93+
def resolve_roboflow_service_url(
94+
service: str,
95+
region: Optional[str] = None,
96+
environment: Optional[str] = None,
97+
project: Optional[str] = None,
98+
) -> str:
99+
"""Resolve the default URL for a Roboflow service in the selected region / environment."""
100+
if region is None:
101+
region = get_roboflow_region()
102+
if environment is None:
103+
environment = get_roboflow_environment(project=project)
104+
return ROBOFLOW_SERVICE_URLS[(region, environment)][service]

inference_cli/lib/enterprise/inference_compiler/constants.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import os
22

3+
from inference_cli.lib.env import PROJECT
4+
from inference_sdk.regions import resolve_roboflow_service_url
5+
36
PROD_ENVIRONMENT_NAME = "prod"
47
ROBOFLOW_ENVIRONMENT = os.getenv("ROBOFLOW_ENVIRONMENT", PROD_ENVIRONMENT_NAME)
58
ROBOFLOW_API_HOST = os.getenv(
6-
"ROBOFLOW_API_HOST",
7-
(
8-
"https://api.roboflow.com"
9-
if ROBOFLOW_ENVIRONMENT == PROD_ENVIRONMENT_NAME
10-
else "https://api.roboflow.one"
11-
),
9+
"ROBOFLOW_API_HOST", resolve_roboflow_service_url("api", project=PROJECT)
1210
)
1311
ROBOFLOW_API_KEY = os.getenv("ROBOFLOW_API_KEY", None)
1412
HTTP_CODES_TO_RETRY = {408, 429, 500, 502, 503, 504}

inference_cli/lib/env.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import os
22

3+
from inference_sdk.regions import get_roboflow_region, resolve_roboflow_service_url
4+
35
CLI_LOG_LEVEL = os.getenv("CLI_LOG_LEVEL", "INFO")
46
ROBOFLOW_API_KEY = os.getenv("ROBOFLOW_API_KEY")
57
PROJECT = os.getenv("PROJECT", "roboflow-platform")
8+
ROBOFLOW_REGION = get_roboflow_region()
69
API_BASE_URL = os.getenv(
710
"API_BASE_URL",
8-
(
9-
"https://api.roboflow.com"
10-
if PROJECT == "roboflow-platform"
11-
else "https://api.roboflow.one"
12-
),
11+
resolve_roboflow_service_url("api", region=ROBOFLOW_REGION, project=PROJECT),
1312
)

inference_models/inference_models/configuration.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,29 @@
3939
)
4040
)
4141
ROBOFLOW_ENVIRONMENT = os.getenv("ROBOFLOW_ENVIRONMENT", "prod")
42+
# Region / environment matrix mirroring inference_sdk.regions - inference-models
43+
# is a standalone distribution and cannot depend on inference_sdk.
44+
_ROBOFLOW_API_HOSTS = {
45+
("us", "prod"): "https://api.roboflow.com",
46+
("us", "staging"): "https://api.roboflow.one",
47+
("eu", "prod"): "https://api.roboflow.eu",
48+
("eu", "staging"): "https://api.roboflow-eu.one",
49+
}
50+
ROBOFLOW_REGION = os.getenv("ROBOFLOW_REGION", "us").strip().lower()
51+
if ROBOFLOW_REGION not in {region for region, _ in _ROBOFLOW_API_HOSTS}:
52+
warnings.warn(
53+
f"Unknown ROBOFLOW_REGION {ROBOFLOW_REGION!r} - falling back to 'us'. "
54+
"Supported regions: eu, us.",
55+
)
56+
ROBOFLOW_REGION = "us"
4257
ROBOFLOW_API_HOST = os.getenv(
4358
"ROBOFLOW_API_HOST",
44-
(
45-
"https://api.roboflow.com"
46-
if ROBOFLOW_ENVIRONMENT.lower() == "prod"
47-
else "https://api.roboflow.one"
48-
),
59+
_ROBOFLOW_API_HOSTS[
60+
(
61+
ROBOFLOW_REGION,
62+
"prod" if ROBOFLOW_ENVIRONMENT.lower() == "prod" else "staging",
63+
)
64+
],
4965
)
5066
_legacy_license_server = os.getenv("LICENSE_SERVER")
5167
SECURE_GATEWAY = os.getenv("SECURE_GATEWAY") or _legacy_license_server or None

inference_models/tests/unit_tests/test_configuration.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
import importlib
2+
import os
3+
from typing import Callable
4+
15
import pytest
26

7+
import inference_models.configuration
38
from inference_models.configuration import (
49
DEFAULT_RFDETR_PIPELINE_DEPTH,
510
MAX_RFDETR_PIPELINE_DEPTH,
@@ -8,6 +13,33 @@
813
)
914
from inference_models.errors import InvalidEnvVariable
1015

16+
REGION_ENVIRONMENT_KEYS = [
17+
"ROBOFLOW_REGION",
18+
"ROBOFLOW_ENVIRONMENT",
19+
"ROBOFLOW_API_HOST",
20+
]
21+
22+
23+
@pytest.fixture
24+
def reload_configuration() -> Callable[..., object]:
25+
saved_environment = {
26+
key: os.environ.pop(key) for key in REGION_ENVIRONMENT_KEYS if key in os.environ
27+
}
28+
29+
def _reload(**environment: str) -> object:
30+
for key in REGION_ENVIRONMENT_KEYS:
31+
os.environ.pop(key, None)
32+
os.environ.update(environment)
33+
return importlib.reload(inference_models.configuration)
34+
35+
try:
36+
yield _reload
37+
finally:
38+
for key in REGION_ENVIRONMENT_KEYS:
39+
os.environ.pop(key, None)
40+
os.environ.update(saved_environment)
41+
importlib.reload(inference_models.configuration)
42+
1143

1244
def test_parse_rfdetr_pipeline_depth_uses_default_when_env_missing() -> None:
1345
assert parse_rfdetr_pipeline_depth(None) == DEFAULT_RFDETR_PIPELINE_DEPTH
@@ -48,3 +80,48 @@ def test_get_rfdetr_pipeline_depth_rejects_invalid_environment(
4880
monkeypatch.setenv("RFDETR_PIPELINE_DEPTH", value)
4981
with pytest.raises(InvalidEnvVariable):
5082
get_rfdetr_pipeline_depth()
83+
84+
85+
def test_roboflow_api_host_defaults_to_us_production(reload_configuration) -> None:
86+
configuration = reload_configuration()
87+
assert configuration.ROBOFLOW_REGION == "us"
88+
assert configuration.ROBOFLOW_API_HOST == "https://api.roboflow.com"
89+
90+
91+
@pytest.mark.parametrize(
92+
"region, environment, expected_api_host",
93+
[
94+
("us", "prod", "https://api.roboflow.com"),
95+
("us", "staging", "https://api.roboflow.one"),
96+
("eu", "prod", "https://api.roboflow.eu"),
97+
("eu", "staging", "https://api.roboflow-eu.one"),
98+
],
99+
)
100+
def test_roboflow_api_host_follows_region_and_environment_matrix(
101+
reload_configuration,
102+
region: str,
103+
environment: str,
104+
expected_api_host: str,
105+
) -> None:
106+
configuration = reload_configuration(
107+
ROBOFLOW_REGION=region, ROBOFLOW_ENVIRONMENT=environment
108+
)
109+
assert configuration.ROBOFLOW_API_HOST == expected_api_host
110+
111+
112+
def test_explicit_roboflow_api_host_beats_region_selection(
113+
reload_configuration,
114+
) -> None:
115+
configuration = reload_configuration(
116+
ROBOFLOW_REGION="eu", ROBOFLOW_API_HOST="https://api.example.com"
117+
)
118+
assert configuration.ROBOFLOW_API_HOST == "https://api.example.com"
119+
120+
121+
def test_unknown_roboflow_region_warns_and_falls_back_to_us(
122+
reload_configuration,
123+
) -> None:
124+
with pytest.warns(UserWarning, match="Unknown ROBOFLOW_REGION"):
125+
configuration = reload_configuration(ROBOFLOW_REGION="mars")
126+
assert configuration.ROBOFLOW_REGION == "us"
127+
assert configuration.ROBOFLOW_API_HOST == "https://api.roboflow.com"

inference_sdk/config.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import threading
55
from typing import Iterable, Optional, Tuple
66

7+
from inference_sdk.regions import resolve_roboflow_service_url
78
from inference_sdk.utils.environment import str2bool
89

910
execution_id = contextvars.ContextVar("execution_id", default=None)
@@ -182,6 +183,8 @@ def summarize(self, max_detail_bytes: int = 4096) -> Tuple[float, Optional[str]]
182183
"https://infer.roboflow.com",
183184
"https://serverless.roboflow.com",
184185
"https://serverless.roboflow.one",
186+
"https://serverless.roboflow.eu",
187+
"https://serverless.roboflow-eu.one",
185188
"https://asyncinfer.roboflow.com",
186189
"https://asyncinfer.roboflow.one",
187190
}
@@ -203,7 +206,7 @@ def summarize(self, max_detail_bytes: int = 4096) -> Tuple[float, Optional[str]]
203206
) # 256KB max buffered before backpressure
204207

205208
# Roboflow API base URL for TURN config and other services
206-
RF_API_BASE_URL = os.getenv("RF_API_BASE_URL", "https://api.roboflow.com")
209+
RF_API_BASE_URL = os.getenv("RF_API_BASE_URL", resolve_roboflow_service_url("api"))
207210

208211

209212
class InferenceSDKDeprecationWarning(Warning):

0 commit comments

Comments
 (0)