Skip to content
Merged
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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -581,3 +581,12 @@ Also see the official [Python SDK docs](https://clarifai-python.readthedocs.io/e

Examples for uploading models and runners have been moved to this [repo](https://github.com/Clarifai/runners-examples).
Find our official documentation at [docs.clarifai.com/compute/models/upload](https://docs.clarifai.com/compute/models/upload).

## Versioning

This project uses [CalVer](https://calver.org/) with the format **`YY.MM.PATCH`**:
- **YY** — two-digit year (e.g. `12` for 2026)
- **MM** — month number, not zero-padded (e.g. `1` for January, `12` for December)
- **PATCH** — incremental release within that month, starting at `0`

Git tags use the same format without a `v` prefix (e.g. `12.2.0`). The version is defined in `clarifai/__init__.py`.
4 changes: 2 additions & 2 deletions clarifai/cli/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1211,7 +1211,7 @@ def local_runner(ctx, model_path, pool_size, suppress_toolkit_logs, mode, keep_i
model_path = os.path.abspath(model_path)
_ensure_hf_token(ctx, model_path)
builder = ModelBuilder(model_path, download_validation_only=True)
manager = ModelRunLocally(model_path)
manager = ModelRunLocally(model_path, model_builder=builder)

port = 8080
if mode == "env":
Expand Down Expand Up @@ -1674,7 +1674,7 @@ def print_code_snippet():
else:
print_code_snippet()
# This reads the config.yaml from the model_path so we alter it above first.
server = ModelServer(model_path=model_path, model_runner_local=None)
server = ModelServer(model_path=model_path, model_runner_local=None, model_builder=builder)
server.serve(**serving_args)


Expand Down
24 changes: 17 additions & 7 deletions clarifai/runners/models/model_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ def __init__(
platform: Optional[str] = None,
pat: Optional[str] = None,
base_url: Optional[str] = None,
compute_info_required: bool = False,
):
"""
:param folder: The folder containing the model.py, config.yaml, requirements.txt and
Expand All @@ -194,6 +195,7 @@ def __init__(
:param platform: Target platform(s) for Docker image build (e.g., "linux/amd64" or "linux/amd64,linux/arm64"). This overrides the platform specified in config.yaml.
:param pat: Personal access token for authentication. If None, will use environment variables.
:param base_url: Base URL for the API. If None, will use environment variables.
:param compute_info_required: Whether inference compute info is required. This affects certain validation and behavior.
"""
assert app_not_found_action in ["auto_create", "prompt", "error"], ValueError(
f"Expected one of {['auto_create', 'prompt', 'error']}, got {app_not_found_action=}"
Expand All @@ -214,7 +216,9 @@ def __init__(
self.model_proto = self._get_model_proto()
self.model_id = self.model_proto.id
self.model_version_id = None
self.inference_compute_info = self._get_inference_compute_info()
self.inference_compute_info = self._get_inference_compute_info(
compute_info_required=compute_info_required
)
self.is_v3 = True # Do model build for v3

def create_model_instance(self, load_model=True, mocking=False) -> ModelClass:
Expand Down Expand Up @@ -944,11 +948,12 @@ def _get_model_proto(self):

return model_proto

def _get_inference_compute_info(self):
assert "inference_compute_info" in self.config, (
"inference_compute_info not found in the config file"
)
inference_compute_info = self.config.get('inference_compute_info')
def _get_inference_compute_info(self, compute_info_required=False):
if compute_info_required:
assert "inference_compute_info" in self.config, (
"inference_compute_info not found in the config file"
)
inference_compute_info = self.config.get('inference_compute_info') or {}
# Ensure cpu_limit is a string if it exists and is an int
if 'cpu_limit' in inference_compute_info and isinstance(
inference_compute_info['cpu_limit'], int
Expand Down Expand Up @@ -1945,7 +1950,12 @@ def upload_model(
:param base_url: Base URL for the API. If None, will use environment variables.
"""
builder = ModelBuilder(
folder, app_not_found_action="prompt", platform=platform, pat=pat, base_url=base_url
folder,
app_not_found_action="prompt",
platform=platform,
pat=pat,
base_url=base_url,
compute_info_required=True,
)
builder.download_checkpoints(stage=stage)

Expand Down
23 changes: 21 additions & 2 deletions clarifai/runners/models/model_run_locally.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,31 @@


class ModelRunLocally:
def __init__(self, model_path):
def __init__(self, model_path, model_builder: ModelBuilder = None):
"""
Initialize a helper to run a Clarifai model locally in an isolated environment.

Parameters
----------
model_path : str
Filesystem path to the root directory of the model. This directory is expected
to contain the model code and a ``requirements.txt`` file describing its
Python dependencies.
model_builder : ModelBuilder, optional
An existing :class:`ModelBuilder` instance to use for interacting with the
model. Pass this when you already have a configured builder you want to
reuse. If ``None`` (the default), a new ``ModelBuilder`` is created for
``model_path`` with ``download_validation_only=True``.
"""
self.model_path = os.path.abspath(model_path)
self.requirements_file = os.path.join(self.model_path, "requirements.txt")

# ModelBuilder contains multiple useful methods to interact with the model
self.builder = ModelBuilder(self.model_path, download_validation_only=True)
self.builder = (
model_builder
if model_builder
else ModelBuilder(self.model_path, download_validation_only=True)
)
self.config = self.builder.config

def _get_method_signatures(self):
Expand Down
2 changes: 2 additions & 0 deletions clarifai/runners/models/model_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ def __init__(
HealthProbeRequestHandler.is_startup = True

start_health_server_thread(port=health_check_port, address='')
if health_check_port is not None:
start_health_server_thread(port=health_check_port, address='')

def get_runner_item_output_for_status(
self, status: status_pb2.Status
Expand Down
13 changes: 11 additions & 2 deletions clarifai/runners/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,12 @@ def main():


class ModelServer:
def __init__(self, model_path, model_runner_local: ModelRunLocally = None):
def __init__(
self,
model_path,
model_runner_local: ModelRunLocally = None,
model_builder: ModelBuilder = None,
):
"""Initialize the ModelServer.
Args:
model_path: Path to the model directory
Expand All @@ -158,7 +163,11 @@ def __init__(self, model_path, model_runner_local: ModelRunLocally = None):
self._initialize_secrets_system()

# Build model after secrets are loaded
self._builder = ModelBuilder(model_path, download_validation_only=True)
self._builder = (
model_builder
if model_builder
else ModelBuilder(model_path, download_validation_only=True)
)
self._current_model = self._builder.create_model_instance()

logger.info("ModelServer initialized successfully")
Expand Down
2 changes: 1 addition & 1 deletion clarifai/runners/utils/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def handles_list(self):

def is_repeated_field(field_name):
descriptor = resources_pb2.Data.DESCRIPTOR.fields_by_name.get(field_name)
return descriptor and descriptor.label == descriptor.LABEL_REPEATED
return descriptor and descriptor.is_repeated


class AtomicFieldSerializer(Serializer):
Expand Down
2 changes: 1 addition & 1 deletion clarifai/utils/protobuf.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def dict_to_protobuf(pb_obj: Message, data: dict) -> None:

try:
# Handle repeated fields (lists)
if field_descriptor.label == FieldDescriptor.LABEL_REPEATED:
if field_descriptor.is_repeated:
_handle_repeated_field(pb_obj, field_descriptor, field, value)

# Handle message fields (nested messages)
Expand Down
18 changes: 13 additions & 5 deletions tests/cli/test_local_runner_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,9 @@ def test_local_runner_creates_resources_when_missing(
# Mock ModelBuilder
mock_builder = MagicMock()
mock_builder.config = {"model": {"model_type_id": "multimodal-to-text"}, "toolkit": {}}
mock_builder.get_method_signatures.return_value = {"predict": "mock_signature"}
mock_method_sig = MagicMock()
mock_method_sig.name = "predict"
mock_builder.get_method_signatures.return_value = [mock_method_sig]
mock_builder_class.return_value = mock_builder
mock_builder_class._load_config.return_value = mock_builder.config

Expand Down Expand Up @@ -266,7 +268,9 @@ def test_local_runner_uses_existing_resources(
# Mock ModelBuilder
mock_builder = MagicMock()
mock_builder.config = {"model": {"model_type_id": "multimodal-to-text"}, "toolkit": {}}
mock_builder.get_method_signatures.return_value = {"predict": "mock_signature"}
mock_method_sig = MagicMock()
mock_method_sig.name = "predict"
mock_builder.get_method_signatures.return_value = [mock_method_sig]
mock_builder_class.return_value = mock_builder
mock_builder_class._load_config.return_value = mock_builder.config

Expand Down Expand Up @@ -398,7 +402,9 @@ def test_local_runner_with_pool_size_parameter(
# Mock ModelBuilder
mock_builder = MagicMock()
mock_builder.config = {"model": {"model_type_id": "multimodal-to-text"}, "toolkit": {}}
mock_builder.get_method_signatures.return_value = {"predict": "mock_signature"}
mock_method_sig = MagicMock()
mock_method_sig.name = "predict"
mock_builder.get_method_signatures.return_value = [mock_method_sig]
mock_builder_class.return_value = mock_builder
mock_builder_class._load_config.return_value = mock_builder.config

Expand Down Expand Up @@ -534,7 +540,9 @@ def test_local_runner_model_serving(
# Mock ModelBuilder
mock_builder = MagicMock()
mock_builder.config = {"model": {"model_type_id": "multimodal-to-text"}, "toolkit": {}}
mock_builder.get_method_signatures.return_value = {"predict": "mock_signature"}
mock_method_sig = MagicMock()
mock_method_sig.name = "predict"
mock_builder.get_method_signatures.return_value = [mock_method_sig]
mock_builder_class.return_value = mock_builder
mock_builder_class._load_config.return_value = mock_builder.config

Expand Down Expand Up @@ -625,7 +633,7 @@ def validate_ctx_mock(ctx):

# Verify ModelServer was instantiated with the correct model path
mock_server_class.assert_called_once_with(
model_path=str(dummy_model_dir), model_runner_local=None
model_path=str(dummy_model_dir), model_runner_local=None, model_builder=mock_builder
)

# Verify serve method was called with correct parameters for local runner
Expand Down
2 changes: 1 addition & 1 deletion tests/runners/test_model_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def test_model_uploader_flow(dummy_models_path, client):
7. Delete the deployment
"""
# Initialize
builder = ModelBuilder(folder=str(dummy_models_path))
builder = ModelBuilder(folder=str(dummy_models_path), compute_info_required=True)
assert builder.folder == str(dummy_models_path), "Uploader folder mismatch"

# Basic checks on config
Expand Down
Loading