Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ This reference covers the supported public Python surface exported by `jointfm_c
| `DataFrameSchema` | Describes tabular history layout. Fields are `columns`, `time_index_mode`, `time_column`, `time_scale_seconds`, `use_local_normalized_time`, `calendar_id`, and `timezone`. |
| `ForecastRequestMetadata` | Holds `schema_version`, `model_version`, `query_mode`, and `return_mode` for one forecast request. |
| `ForecastRequest` | Validated request object that combines metadata, schema, history rows, query times, requested columns, sample or quantile controls, and `seed`, then emits a JSON-compatible payload with `to_payload()`. |
| `HealthMetadata` | Typed service-health payload with service status, schema and model versions, checkpoint metadata, device, head, advertised modes, time-index encoding, `max_sample_count`, and an optional `data_generation` block carrying advertised capacity limits. The container exposes it on `GET /healthz` for direct local access and as the response to `POST {"request_type": "health"}` on the unstructured prediction route for DataRobot-hosted deployments. |
| `HealthMetadata` | Typed service-health payload with service status, schema and model versions, checkpoint metadata, device, head, advertised modes, time-index encoding, `max_sample_count`, an optional `data_generation` block carrying advertised capacity limits, and the optional informational `backend` and `decoding_strategy` fields. The container exposes it on `GET /healthz` for direct local access and as the response to `POST {"request_type": "health"}` on the unstructured prediction route for DataRobot-hosted deployments. |
| `DataGenerationCapabilities` | Optional service-health block describing the deployed checkpoint's data-generation capacity. Fields are `sampler_type`, `min_features`, `max_features`, `min_targets`, `max_targets`, `t_input`, `t_output`, `n_input`, and `n_output`. |
| `ForecastPlan` | Validated forecast plan returned by `plan_forecast_columns`. Fields are `columns` (ordered `ColumnSpec` tuple), `feature_columns`, `target_columns` (both reflect post-downgrade roles), and `requested_columns` (the caller's original target list). |
| `StructuredError` | One structured JointFM service error with `code`, `message`, and optional `field`. |
Expand Down Expand Up @@ -215,13 +215,15 @@ The string literals are exposed as `PREDICT_REQUEST_TYPE`, `HEALTH_REQUEST_TYPE`
| `checkpoint_version` | Loaded checkpoint version. |
| `checkpoint_path` | Loaded checkpoint path reported by the service. |
| `device` | Device used by inference. |
| `backend` | Informational serving backend: `legacy` or `architecture`. `None` when the deployment predates the field. Not client-selectable and never validated by the SDK. |
| `head` | Active forecast head. |
| `supported_query_modes` | Must match the SDK V1 query modes. |
| `supported_return_modes` | Must match the SDK V1 return modes (`mean`, `samples`, `quantiles`, `log_prob`). |
| `supported_time_index_modes` | Must match the SDK V1 time-index modes. |
| `time_index_encoding` | Time-index encoding advertised by the service. |
| `max_sample_count` | Maximum sample-count budget the service accepts in a single prediction. The client reads it during health probes and batches oversized sample requests locally, so the service never has to reject them. |
| `data_generation` | Optional capability block describing the deployed checkpoint's advertised data-generation capacity. Absent on legacy checkpoints; present payloads expose `sampler_type`, `min_features`, `max_features`, `min_targets`, `max_targets`, `t_input`, `t_output`, `n_input`, and `n_output`. |
| `decoding_strategy` | Informational decoding strategy of the mounted checkpoint: `parallel_dense`, `parallel_scalable`, or `autoregressive`. `None` when the service reports it as null (architecture-backend containers and checkpoint configs predating the field) or omits it entirely. It is singular rather than a `supported_*` list because the strategy is fixed at training time and no request field can select another, so it never constrains which requests a deployment accepts and the SDK does not validate against it. Callers still need it to interpret responses: only `autoregressive` couples horizons, so under either parallel strategy `samples` are per-horizon marginal draws rather than coherent sample paths. |

## Docstring Enforcement

Expand Down
7 changes: 7 additions & 0 deletions src/jointfm_client/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,8 @@ class HealthMetadata:
time_index_encoding: str
max_sample_count: int
data_generation: DataGenerationCapabilities | None = None
decoding_strategy: str | None = None
backend: str | None = None

@classmethod
def from_payload(cls, payload: Mapping[str, Any]) -> Self:
Expand Down Expand Up @@ -533,6 +535,11 @@ def from_payload(cls, payload: Mapping[str, Any]) -> Self:
field="max_sample_count",
),
data_generation=parsed_data_generation,
decoding_strategy=_optional_string(
payload.get("decoding_strategy"),
field="decoding_strategy",
),
backend=_optional_string(payload.get("backend"), field="backend"),
)


Expand Down
2 changes: 2 additions & 0 deletions tests/fixtures/health_metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"checkpoint_version": "sdk-test",
"checkpoint_path": "/models/jointfm.pt",
"device": "cpu",
"backend": "legacy",
"head": "studentt",
"supported_query_modes": ["forecast"],
"supported_return_modes": ["mean", "samples", "quantiles", "log_prob"],
Expand All @@ -16,6 +17,7 @@
],
"time_index_encoding": "legacy_discrete_grid",
"max_sample_count": 4096,
"decoding_strategy": "parallel_dense",
"data_generation": {
"sampler_type": "studentt",
"min_features": 0,
Expand Down
4 changes: 4 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ def health(self) -> HealthMetadata:
),
time_index_encoding="legacy_discrete_grid",
max_sample_count=4096,
decoding_strategy="parallel_dense",
backend="legacy",
)


Expand Down Expand Up @@ -125,6 +127,8 @@ def test_health_command_prints_non_secret_metadata(monkeypatch, capsys) -> None:
payload = json.loads(output)
assert exit_code == 0
assert payload["service"]["status"] == "ok"
assert payload["service"]["decoding_strategy"] == "parallel_dense"
assert payload["service"]["backend"] == "legacy"
assert payload["deployment"]["deployment_id"] == "deployment-id"
assert "secret-token" not in output

Expand Down
26 changes: 26 additions & 0 deletions tests/test_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ def _health_metadata() -> dict[str, object]:
"checkpoint_version": "smoke-1",
"checkpoint_path": "/models/jointfm.pt",
"device": "cpu",
"backend": "legacy",
"head": "dummy",
"supported_query_modes": ["forecast"],
"supported_return_modes": ["mean", "quantiles", "samples", "log_prob"],
Expand All @@ -87,6 +88,7 @@ def _health_metadata() -> dict[str, object]:
],
"time_index_encoding": "legacy_discrete_grid",
"max_sample_count": 4096,
"decoding_strategy": "parallel_dense",
"data_generation": {
"sampler_type": "studentt",
"min_features": 0,
Expand Down Expand Up @@ -778,6 +780,8 @@ def test_health_and_response_models_parse_current_payloads() -> None:
)

assert health.model_version == "jointfm-inference:0.2.0+ckpt.smoke-1"
assert health.decoding_strategy == "parallel_dense"
assert health.backend == "legacy"
assert isinstance(response, MeanForecastResult)
assert response.requested_columns == ("target",)
assert response.mean == ((100.0,),)
Expand All @@ -786,6 +790,28 @@ def test_health_and_response_models_parse_current_payloads() -> None:
assert response.errors == ()


@pytest.mark.parametrize("field", ["decoding_strategy", "backend"])
def test_health_metadata_treats_informational_fields_as_optional(field: str) -> None:
"""Health metadata treats informational fields as optional."""
missing = _health_metadata()
del missing[field]
assert getattr(HealthMetadata.from_payload(missing), field) is None

explicit_null = _health_metadata()
explicit_null[field] = None
assert getattr(HealthMetadata.from_payload(explicit_null), field) is None


@pytest.mark.parametrize("field", ["decoding_strategy", "backend"])
def test_health_metadata_rejects_non_string_informational_fields(field: str) -> None:
"""Health metadata rejects non string informational fields."""
metadata = _health_metadata()
metadata[field] = 7

with pytest.raises(ValueError, match=field):
HealthMetadata.from_payload(metadata)


def test_forecast_result_conversion_helpers_cover_mean_samples_and_quantiles() -> None:
"""Forecast result conversion helpers cover mean samples and quantiles."""
mean_result = ForecastResponse.from_payload(
Expand Down
2 changes: 2 additions & 0 deletions tests/test_fixture_compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ def test_health_fixture_matches_current_v1_service_contract(
metadata = HealthMetadata.from_payload(payload)

assert metadata.schema_version == "v1"
assert metadata.decoding_strategy == payload["decoding_strategy"]
assert metadata.backend == payload["backend"]
assert metadata.supported_return_modes == (
"mean",
"samples",
Expand Down
Loading