diff --git a/docs/api-reference.md b/docs/api-reference.md index 91a4710..527b26a 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -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`. | @@ -215,6 +215,7 @@ 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`). | @@ -222,6 +223,7 @@ The string literals are exposed as `PREDICT_REQUEST_TYPE`, `HEALTH_REQUEST_TYPE` | `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 diff --git a/src/jointfm_client/contract.py b/src/jointfm_client/contract.py index 235132d..53ed337 100644 --- a/src/jointfm_client/contract.py +++ b/src/jointfm_client/contract.py @@ -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: @@ -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"), ) diff --git a/tests/fixtures/health_metadata.json b/tests/fixtures/health_metadata.json index b1b5a8b..7bc3416 100644 --- a/tests/fixtures/health_metadata.json +++ b/tests/fixtures/health_metadata.json @@ -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"], @@ -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, diff --git a/tests/test_cli.py b/tests/test_cli.py index 1d371af..754d421 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -67,6 +67,8 @@ def health(self) -> HealthMetadata: ), time_index_encoding="legacy_discrete_grid", max_sample_count=4096, + decoding_strategy="parallel_dense", + backend="legacy", ) @@ -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 diff --git a/tests/test_contract.py b/tests/test_contract.py index b6b6d6a..973e344 100644 --- a/tests/test_contract.py +++ b/tests/test_contract.py @@ -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"], @@ -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, @@ -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,),) @@ -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( diff --git a/tests/test_fixture_compatibility.py b/tests/test_fixture_compatibility.py index 2e2ac25..a36c2d4 100644 --- a/tests/test_fixture_compatibility.py +++ b/tests/test_fixture_compatibility.py @@ -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",