Homepage · PyPI · API reference · Support
jointfm-client is the Python client for JointFM, DataRobot's forecasting foundation model. Given a running JointFM service, it builds and validates forecast requests, checks that the service speaks the same contract, and returns forecasts as NumPy arrays or pandas frames. It also installs a jointfm-client command for health checks and CSV forecasts.
Status: alpha. The API and the service contract can still change. While the version is
0.x, a breaking change ships as a minor version bump, so pin a single minor series (jointfm-client~=0.N.0) if you need stability.
This package is only the client. It contains no model and no weights: every call goes to a running JointFM service, so you need one before the first forecast works. The client connects to either kind:
- A service you run yourself, on your own machine or GPU host. Point the client at its base URL with
JOINTFM_LOCAL_BASE_URL. No DataRobot account is needed. - A JointFM deployment hosted on DataRobot. Point the client at it with
DATAROBOT_ENDPOINT,DATAROBOT_API_TOKEN, andJOINTFM_DEPLOYMENT_ID.
Running the server, deploying it, and obtaining model weights are covered by the JointFM inference server project, which is not public yet. This README will link to it once it is published.
Python 3.11 or newer is required.
pip install jointfm-client
# or, in a uv project:
uv add jointfm-clientThe pandas helpers and the example notebooks need the optional extra: pip install "jointfm-client[notebooks]".
Verify the install:
python -c "import jointfm_client; print(jointfm_client.__version__)"Create a .env file in your working directory, or set the same values in your shell. .env.sample documents every option. For a service you run yourself:
JOINTFM_LOCAL_BASE_URL=http://127.0.0.1:8080
JOINTFM_SCHEMA_VERSION=v6For a deployment hosted on DataRobot:
DATAROBOT_ENDPOINT=https://app.datarobot.com/api/v2
DATAROBOT_API_TOKEN=<token>
JOINTFM_SCHEMA_VERSION=v6
JOINTFM_DEPLOYMENT_ID=<deployment-id>
# Or: JOINTFM_DEPLOYMENT_IDS=<deployment-id-1>,<deployment-id-2>
# Optional drift-detection pin; the SDK discovers the model version from /healthz when unset:
# JOINTFM_MODEL_VERSION=jointfm-inference:<image-version>+ckpt.<checkpoint-version>Check that the client can reach the service and that its metadata matches the configured schema and model pins:
jointfm-client healthThis minimal example uses row dictionaries plus explicit column metadata, so it does not require pandas:
from jointfm_client import (
ColumnSpec,
DataFrameSchema,
JointFMClient,
build_ordinal_query_times,
)
history_rows = [
{"t": 0, "sales": 10.0},
{"t": 1, "sales": 12.0},
{"t": 2, "sales": 13.5},
{"t": 3, "sales": 15.0},
]
schema = DataFrameSchema(
columns=(ColumnSpec(name="sales", modality="numeric", role="target"),),
time_index_mode="ordinal",
time_column="t",
)
client = JointFMClient.from_env()
result = client.forecast_mean(
history_rows,
schema=schema,
query_times=build_ordinal_query_times([row["t"] for row in history_rows], periods=3),
requested_columns=("sales",),
)
print(result.to_pandas_tidy())Use forecast_samples(...) for sampled trajectories or forecast_quantiles(...) with quantiles=(0.1, 0.5, 0.9) for quantile surfaces. For pandas inputs, install the optional extra and pass a DataFrame to forecast(...) with target_columns and requested_columns set explicitly.
- Distribution package:
jointfm-client - Import namespace:
jointfm_client - Supported Python:
>=3.11 - Current SDK package version:
0.15.0 - Current JointFM service schema:
schema_version="v6"
The public API shape is a synchronous low-level JointFMClient with health(), health_instances(), and predict(payload) methods plus high-level forecast(...), forecast_mean(...), forecast_samples(...), and forecast_quantiles(...) helpers. The SDK is not a proxy service; callers use it as a local Python library that talks to the hosted or local JointFM endpoint.
SDK package versions are standard Python distribution versions: [project].version in pyproject.toml is the single declared value, and jointfm_client.__version__ reports it back from the installed distribution metadata. JointFM schema_version, image_version, model_version, and checkpoint_version are service compatibility identifiers carried in configuration, health metadata, requests, and responses. They are not SDK package versions, and changing a deployment pin does not by itself require changing the SDK package version.
See docs/api-reference.md for the checked-in API reference covering public classes, functions, exceptions, environment variables, and payload fields.
The DataRobot-hosted prediction URL is built from the DataRobot API v2 endpoint and deployment ID:
from urllib.parse import urljoin
service_base_url = DATAROBOT_ENDPOINT.rstrip("/") + "/"
predict_url = urljoin(
service_base_url,
f"deployments/{deployment_id}/predictionsUnstructured",
)The direct local service exposes GET /healthz and POST /predict.
Structured SDK defaults live in jointfm_client.configuration.JointFMConfig and are mirrored in the checked-in config.sample.yaml. Copy config.sample.yaml to config.yaml and change only the fields needed for your deployment or transport defaults. JointFMClient.from_env() and load_settings() read config.yaml by default, then layer .env values over it, then layer process environment variables or the supplied env mapping over both. Explicit Python arguments such as timeout= and retry_config= still override YAML transport defaults.
JointFMClient.from_env() and load_settings() resolve JOINTFM_SCHEMA_VERSION and exactly one service selector from that layered configuration. Hosted options include JOINTFM_DEPLOYMENT_ID or load-balanced JOINTFM_DEPLOYMENT_IDS (comma-separated same-checkpoint peers; mutually exclusive with other selectors). JOINTFM_MODEL_VERSION is optional: when unset the SDK discovers the model version from /healthz on first use, and when set the SDK validates it against /healthz as a drift-detection guard. Local options are JOINTFM_LOCAL_BASE_URL or load-balanced JOINTFM_LOCAL_BASE_URLS (comma-separated same-checkpoint local services; mutually exclusive with other selectors). Hosted selectors also require DATAROBOT_ENDPOINT and DATAROBOT_API_TOKEN; the local selectors do not use DataRobot credentials. Missing credentials, missing schema version, malformed credentials, unsupported schema versions, missing selectors, and multiple selectors raise JointFMConfigurationError.
DATAROBOT_ENDPOINT must be a normalized HTTPS DataRobot API v2 URL ending in /api/v2; the SDK stores it without a trailing slash. DATAROBOT_API_TOKEN must be non-empty and whitespace-free. The token is excluded from JointFMSettings repr output.
Required .env entries for hosted SDK calls are DATAROBOT_ENDPOINT, DATAROBOT_API_TOKEN, JOINTFM_SCHEMA_VERSION, and exactly one hosted selector from the list below. Required .env entries for local REST calls are JOINTFM_LOCAL_BASE_URL and JOINTFM_SCHEMA_VERSION. JOINTFM_MODEL_VERSION may be set in .env to pin a specific deployment artifact (the SDK then hard-errors on mismatch with /healthz); leave it unset to let the SDK use whatever version the deployment currently advertises. .env is the right place for these pins when using from_env() because they describe the selected JointFM service rather than a package-wide default. They are not secrets, and callers can still override them with process environment variables. Optional live DataRobot smoke tests additionally read DATAROBOT_DEPLOYMENT_ID from .env and use it as the hosted deployment ID for the deployments/{deployment_id}/predictionsUnstructured route.
Example deployment configuration:
deployment:
datarobot_endpoint: https://app.datarobot.com/api/v2
datarobot_api_token: <token>
schema_version: v6
deployment_id: <deployment-id>
# Optional model-version pin; the SDK discovers it from /healthz when unset:
# model_version: jointfm-inference:<image-version>+ckpt.<checkpoint-version>
transport:
timeout:
connect_seconds: 10.0
read_seconds: 120.0
retry:
max_attempts: 20
backoff_seconds: 2Equivalent .env deployment configuration:
DATAROBOT_ENDPOINT=https://app.datarobot.com/api/v2
DATAROBOT_API_TOKEN=<token>
JOINTFM_SCHEMA_VERSION=v6
JOINTFM_DEPLOYMENT_ID=<deployment-id>
# Optional drift-detection pin; the SDK discovers the model version from /healthz when unset:
# JOINTFM_MODEL_VERSION=jointfm-inference:<image-version>+ckpt.<checkpoint-version>Equivalent configuration for a JointFM inference service you run yourself:
JOINTFM_LOCAL_BASE_URL=http://127.0.0.1:8080
JOINTFM_SCHEMA_VERSION=v6
# Optional drift-detection pin; the SDK discovers the model version from /healthz when unset:
# JOINTFM_MODEL_VERSION=jointfm-inference:<image-version>+ckpt.<checkpoint-version>Choose exactly one service selector:
JOINTFM_DEPLOYMENT_ID: buildsDATAROBOT_ENDPOINT.rstrip("/") + "/"plusdeployments/{deployment_id}/predictionsUnstructuredJOINTFM_DEPLOYMENT_IDS: comma-separated hosted deployment IDs (≥2 unique, same checkpoint) for round-robin load balancing; mutually exclusive with other selectorsJOINTFM_DEPLOYMENT_URL: appends/predictionsUnstructuredto a hosted deployment URLJOINTFM_PREDICT_URL: uses a full hosted prediction URL ending in/predictionsUnstructuredJOINTFM_DEPLOYMENT_TARGETwithJOINTFM_PULUMI_OUTPUTS_PATH: resolves a named target from saved Pulumi outputs JSON, preferringdeployment_id, thendeployment_url, thenpredict_urlJOINTFM_LOCAL_BASE_URL: builds direct localGET /healthzandPOST /predictURLs without DataRobot authenticationJOINTFM_LOCAL_BASE_URLS: comma-separated local service base URLs (≥2 unique after normalization, same checkpoint), for example one instance per GPU on distinct ports, load-balanced exactly likeJOINTFM_DEPLOYMENT_IDSand without DataRobot authentication; mutually exclusive with other selectors
Pulumi deployment discovery is explicit and file-backed. Export stack outputs to a JSON object keyed by target name, then set JOINTFM_DEPLOYMENT_TARGET to the key and JOINTFM_PULUMI_OUTPUTS_PATH to that JSON file:
{
"my-target": {
"deployment_id": "<deployment-id>"
}
}For each target, the SDK accepts exactly one of these output fields: deployment_id, deployment_url, or predict_url. deployment_id is preferred because it lets the SDK build both the hosted health URL and the deployments/{deployment_id}/predictionsUnstructured URL from the configured DATAROBOT_ENDPOINT. deployment_url is normalized and extended with /healthz and /predictionsUnstructured. predict_url is accepted when the full hosted prediction URL has already been discovered; the SDK derives the owning deployment URL from it for health checks.
Hosted prediction calls use the same authorization scheme as the notebook helper:
{
"Authorization": f"Bearer {DATAROBOT_API_TOKEN}",
"Accept": "*/*",
"Content-Type": "application/json;charset=UTF-8",
}The SDK still decodes hosted prediction responses as JSON; the broad Accept value avoids hosted unstructured prediction content negotiation failures before the deployment body is returned.
Direct local URL helpers are used by the local service selector: build_local_health_url("http://localhost:8080") returns /healthz, and build_local_predict_url("http://localhost:8080") returns /predict.
Hosted deployments are probed through the prediction route: health() POSTs {"request_type": "health"} to the resolved predict_url, because the DataRobot deployment gateway proxies only the unstructured prediction route. JointFMClient.health(cache=True) stores typed HealthMetadata only when the caller asks for caching, and JointFMClient.refresh_health() fetches a fresh copy.
Each endpoint's health payload describes only that endpoint. With a pool (JOINTFM_DEPLOYMENT_IDS or JOINTFM_LOCAL_BASE_URLS), the client probes every configured peer by POSTing {"request_type": "health"} to its prediction route, which local services answer as well, and aggregates locally: health() returns consensus metadata whose max_sample_count is the minimum reachable cap (the sample-batch size), while health_instances() returns one InstanceHealth per configured instance, named by its instance_id (the deployment ID, or the local base URL), plus the sum of reachable caps (one request per instance; each instance's max_concurrent_requests says how many such requests it serves at once), a compact topology / topology_label (for example 2x5000 or 1x7000, 1x3000), and errors for unavailable peers.
The package installs a jointfm-client command. It reads .env by default, accepts --dotenv <path> for another file, and accepts --no-dotenv when the process environment should be the only source.
Validate credentials, resolve the deployment, probe health, and print non-secret service metadata plus per-instance availability and sample topology:
jointfm-client healthThe health command includes consensus service metadata, an instances list (available/unavailable, per-instance sample cap and request concurrency, errors), topology (for example 1x7000, 1x3000), overall max_sample_count (sum of reachable caps), and non-secret deployment settings when configured.
Submit one low-level JSON request file and write the JSON response file:
jointfm-client predict request.json response.jsonForecast from CSV history and write tidy forecast rows as CSV:
jointfm-client forecast-csv history.csv forecast.csv \
--query-times 2,3,4 \
--target-column target \
--return-mode meanforecast-csv supports --time-index-mode ordinal|continuous_float|absolute_datetime, --time-column, repeated --target-column, repeated --requested-column, --return-mode mean|samples|quantiles, --n-samples, --quantiles, and --seed. The output is the same tidy shape returned by the Python result helpers.
Check that the deployment serves the largest request its health envelope advertises:
jointfm-client max-utilizationmax-utilization fills every request slot at once with a seeded synthetic sample forecast of max_series target columns, an n_input-row history, an n_output-step horizon, and max_sample_count samples: each reachable endpoint (every pool instance with JOINTFM_DEPLOYMENT_IDS or JOINTFM_LOCAL_BASE_URLS) receives as many simultaneous maximal requests as its max_concurrent_requests admits. It prints the burst's wall-clock time and a final VERDICT: PASS or VERDICT: FAIL line. It exits with status 1 on FAIL: when a request raises a service or transport error or returns the wrong sample count, or when an endpoint fails the health probe. Retries are disabled for this command, so a request that fails under load is reported instead of retried. Each run uses real GPU time on the deployment. From a clone of this repository, task max-utilization runs the same command, and notebooks/max_utilization.ipynb shows the burst timing as a table.
Checked-in example notebooks live under notebooks/. Every example starts with:
from jointfm_client import bootstrap_notebook
bootstrap_notebook(add_src_root=True)The notebooks run from a clone of this repository. Run task setup there first (see CONTRIBUTING.md) so VS Code can select the registered Python (joint-client-python) notebook kernel backed by the clone's .venv.
The bootstrap helper resolves the nearest src-layout Python project root, switches the working directory there, and prepends that project's local src tree during development. The examples cover hosted health checks, a maximum-utilization check, low-level JSON prediction, mean forecasts, sample forecasts, quantile forecasts, conditional forecasts (one conditional read as a mean, as draws, as quantiles inside a bounded band, and as the log density of observed values, plus the ranking of candidate conditions), pandas/NumPy result conversion, and CSV forecast workflows. They use .env.sample placeholders and checked-in fixture payloads; no real tokens or deployment IDs are stored in notebooks.
The current forecast request contract is:
schema_version: exactly"v6", configured asJOINTFM_SCHEMA_VERSIONforfrom_env()clientsmodel_version: exact model version advertised by/healthzor otherwise selected by the caller. Optional forfrom_env()clients: whenJOINTFM_MODEL_VERSIONis unset the SDK reads it from/healthzon first use; when set it acts as a drift-detection pinquery_mode:"forecast"for the unconditional forecast, or"condition"for the forecast given conditions on some columns; the high-level helpers set it from whether aconditionwas passedreturn_mode: one of"mean","samples", or"quantiles"time_index_mode: one of"ordinal","continuous_float", or"absolute_datetime"time_column: required for"absolute_datetime", and used for ordered ordinal or continuous histories when suppliedquery_times: non-empty future forecast times onlyrequested_columns: optional column names or integer column indices, with duplicates rejectedn_samples: positive sample count for sampled forecasts and quantile estimation. Whenreturn_mode="samples"exceeds themax_sample_countadvertised by the deployment's health metadata,forecast_samples(...)splits the request into capped prediction batches up front and returns one mergedSampleForecastResult. With a pool (JOINTFM_DEPLOYMENT_IDSorJOINTFM_LOCAL_BASE_URLS), every sample request is split evenly across all reachable endpoints, whatever its size, and each endpoint serves at most one of its batches at a time. Batchiuses seedseed + i, so a seeded result depends on how many endpoints were reachable.condition: required withquery_mode="condition"and forbidden otherwise. OneEqualityConditionorIntervalCondition, or a list of them, each covering the future positions itsquery_time_indicesnames; sent on the wire as theconditionslist, see below.
The condition query mode asks for the forecast given something about some of its columns. Each condition names the future positions it covers by index into query_times through query_time_indices, and covers every position when that is None. Two conditions on the same column must cover disjoint positions; conditions on different columns may share positions, which is how one position mixes both kinds:
EqualityCondition(column, value, query_time_indices=None)pins the column to a finite value. It stays nameable inrequested_columnsand reads back the value the request supplied, so a scenario answer lines up column for column with an unconditioned one.IntervalCondition(column, lower=None, upper=None, query_time_indices=None)confines the column to a range;Noneleaves that side open, and at least one side must be bounded. The column stays readable, and what comes back is its distribution inside the range.
The response answers every entry of query_times. A position no condition covers carries the unconditioned forecast, and that is exact: the model draws each future position from its own joint, independently of the others, so a condition relates columns to each other within a position and never reaches another one. At every covered position at least one column must stay unconditioned, because what the position reads out is the conditional distribution of those columns. requested_columns chooses what the response carries independently of that, defaulting to every declared column in declared order. Pass one condition or a list of them to forecast(...), forecast_mean(...), forecast_samples(...), forecast_quantiles(...), or forecast_log_prob(...):
from jointfm_client import EqualityCondition, IntervalCondition
result = client.forecast_mean(
history,
query_times=query_times,
requested_columns=["portfolio_nav", "realized_volatility"],
columns=plan.columns,
condition=[
EqualityCondition(column="equity_index_level", value=4780.0),
IntervalCondition(
column="treasury_10y_yield", lower=0.041, upper=0.045, query_time_indices=[0]
),
],
)
print(result.plausibility)Whether a deployment can condition depends on the mounted checkpoint's head. /healthz advertises condition in supported_query_modes and the kinds it answers in supported_condition_kinds (empty when the mode is absent). The client checks that advertisement before sending, so a deployment that cannot condition is refused with UnsupportedServiceContractError rather than after a paid round trip; require_condition_support(metadata, condition) exposes the same check.
A condition response carries a plausibility block: equality_log_density is the log density the model assigns to the pinned values and region_log_probability the log probability it gives the interval region given the pinned values at the same position, so the two add to the plausibility of the whole condition set, each None when the request carried no condition of that kind. Over several covered positions each is the sum of the per-position values, exact because positions are independent. They separate a confident answer from one conditioned on something the model finds implausible; the service reports them and never refuses on them. diagnostics.condition_draws counts the draws behind sampled outputs, and diagnostics.interval_estimator reports the numerical accounting (points, effective_sample_size) when some position bounds more than one column and its region probability had to be estimated; across several estimated positions it describes the one with the smallest effective sample size.
Column descriptors support the server fields name, modality, role, nullable, vocabulary_size, level_count, mapping, lower_bound, upper_bound, time_value_kind, time_value_scale_seconds, time_value_use_local_normalized_time, time_value_calendar_id, time_value_timezone, and traits. traits carries expert hints, such as {"jump_process": "present"}. The deployment validates them against its trait vocabulary and refuses any field it does not know, and the client sends a hint only to a deployment whose /healthz lists the trait in supported_metadata_traits.
Forecast values come back as read-only float64 NumPy arrays (result.samples, result.mean, each quantile surface's values); NumPy is a core dependency. DataFrame helpers and the notebook examples are available through one optional extra that pulls in pandas:
uv add "jointfm-client[notebooks]"Use build_forecast_payload_from_dataframe(...) when history is already in a pandas DataFrame. It can accept explicit ColumnSpec objects or infer basic numeric, categorical, ordinal, count, binary, and time-valued columns from the DataFrame plus role, mapping, nullable, and bounds hints. The helper emits history_rows in the same order as the service frame builder: time_column first when present, followed by the ordered modeled columns. build_forecast_payload_from_arrays(...) provides the same request path for two-dimensional NumPy-like arrays when callers already have array values and column metadata. build_datetime_query_times(...), build_ordinal_query_times(...), build_continuous_query_times(...), and validate_forecast_horizon(...) perform local future-horizon validation before the SDK sends the request.
Successful forecast responses preserve schema_version, image_version, model_version, checkpoint_version, head, query_mode, return_mode, outputs, plausibility, and diagnostics. Structured service errors use this shape:
{
"schema_version": "v6",
"errors": [
{
"code": "VALIDATION_ERROR",
"message": "request field explanation",
"field": "request_field"
}
]
}Known error codes are VALIDATION_ERROR, UNSUPPORTED_HEAD_QUERY_COMBINATION, UNSUPPORTED_RETURN_MODE, SCHEMA_VERSION_MISMATCH, MODEL_VERSION_MISMATCH, INPUT_SIZE_EXCEEDED, and INTERNAL_ERROR.
The SDK supports only schema_version="v6". validate_service_metadata() checks /healthz metadata and raises typed compatibility errors before prediction if the service advertises a different schema, an unexpected model version, mode capabilities outside the recorded service contract, or an unsupported decoding_strategy. Return modes and time-index modes must match the SDK's lists exactly. Query modes and condition kinds are derived by the service from the mounted head, so a deployment may advertise fewer of them than the SDK knows; it must advertise at least one query mode and nothing the SDK does not know.
Callers should pass an expected model_version when they already know which deployment artifact they intend to use. A mismatch is treated as a hard compatibility error rather than silently downgrading, guessing, or retrying another model.
High-level forecast helpers build the same validated request payloads as build_forecast_payload(...) and return ForecastResponse. Row-list inputs require an explicit DataFrameSchema or ColumnSpec sequence, while pandas DataFrame inputs can use the DataFrame adapter inference options. predict(payload) remains the low-level JSON method and requires the payload to include model_version.
- Bugs and questions: open an issue at datarobot/joint-client-python.
- Security issues: do not open a public issue; follow SECURITY.md.
- DataRobot platform help: see DataRobot support.
See CONTRIBUTING.md for development setup, the commit format, and how releases are cut.
Apache License 2.0. See LICENSE.