clarifai 11.8.2__tar.gz → 11.8.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. {clarifai-11.8.2/clarifai.egg-info → clarifai-11.8.4}/PKG-INFO +1 -1
  2. clarifai-11.8.4/clarifai/__init__.py +1 -0
  3. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/cli/model.py +93 -35
  4. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/cli/pipeline.py +84 -6
  5. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/cli/templates/model_templates.py +18 -11
  6. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/client/model.py +31 -9
  7. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/pipeline_steps/pipeline_step_builder.py +97 -1
  8. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/pipelines/pipeline_builder.py +196 -34
  9. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/utils/cli.py +31 -7
  10. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/utils/constants.py +1 -0
  11. clarifai-11.8.4/clarifai/utils/hashing.py +117 -0
  12. {clarifai-11.8.2 → clarifai-11.8.4/clarifai.egg-info}/PKG-INFO +1 -1
  13. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai.egg-info/SOURCES.txt +1 -0
  14. clarifai-11.8.2/clarifai/__init__.py +0 -1
  15. {clarifai-11.8.2 → clarifai-11.8.4}/LICENSE +0 -0
  16. {clarifai-11.8.2 → clarifai-11.8.4}/MANIFEST.in +0 -0
  17. {clarifai-11.8.2 → clarifai-11.8.4}/README.md +0 -0
  18. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/cli/README.md +0 -0
  19. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/cli/__init__.py +0 -0
  20. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/cli/__main__.py +0 -0
  21. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/cli/base.py +0 -0
  22. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/cli/compute_cluster.py +0 -0
  23. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/cli/deployment.py +0 -0
  24. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/cli/nodepool.py +0 -0
  25. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/cli/pipeline_step.py +0 -0
  26. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/cli/templates/__init__.py +0 -0
  27. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/cli/templates/pipeline_step_templates.py +0 -0
  28. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/cli/templates/pipeline_templates.py +0 -0
  29. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/cli.py +0 -0
  30. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/client/__init__.py +0 -0
  31. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/client/app.py +0 -0
  32. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/client/auth/__init__.py +0 -0
  33. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/client/auth/helper.py +0 -0
  34. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/client/auth/register.py +0 -0
  35. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/client/auth/stub.py +0 -0
  36. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/client/base.py +0 -0
  37. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/client/compute_cluster.py +0 -0
  38. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/client/dataset.py +0 -0
  39. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/client/deployment.py +0 -0
  40. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/client/input.py +0 -0
  41. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/client/lister.py +0 -0
  42. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/client/model_client.py +0 -0
  43. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/client/module.py +0 -0
  44. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/client/nodepool.py +0 -0
  45. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/client/pipeline.py +0 -0
  46. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/client/pipeline_step.py +0 -0
  47. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/client/runner.py +0 -0
  48. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/client/search.py +0 -0
  49. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/client/user.py +0 -0
  50. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/client/workflow.py +0 -0
  51. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/constants/base.py +0 -0
  52. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/constants/dataset.py +0 -0
  53. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/constants/input.py +0 -0
  54. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/constants/model.py +0 -0
  55. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/constants/rag.py +0 -0
  56. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/constants/search.py +0 -0
  57. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/constants/workflow.py +0 -0
  58. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/datasets/__init__.py +0 -0
  59. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/datasets/export/__init__.py +0 -0
  60. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/datasets/export/inputs_annotations.py +0 -0
  61. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/datasets/upload/__init__.py +0 -0
  62. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/datasets/upload/base.py +0 -0
  63. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/datasets/upload/features.py +0 -0
  64. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/datasets/upload/image.py +0 -0
  65. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/datasets/upload/loaders/README.md +0 -0
  66. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/datasets/upload/loaders/__init__.py +0 -0
  67. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/datasets/upload/loaders/coco_captions.py +0 -0
  68. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/datasets/upload/loaders/coco_detection.py +0 -0
  69. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/datasets/upload/loaders/imagenet_classification.py +0 -0
  70. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/datasets/upload/loaders/xview_detection.py +0 -0
  71. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/datasets/upload/multimodal.py +0 -0
  72. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/datasets/upload/text.py +0 -0
  73. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/datasets/upload/utils.py +0 -0
  74. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/errors.py +0 -0
  75. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/models/__init__.py +0 -0
  76. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/models/api.py +0 -0
  77. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/modules/README.md +0 -0
  78. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/modules/__init__.py +0 -0
  79. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/modules/css.py +0 -0
  80. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/modules/pages.py +0 -0
  81. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/modules/style.css +0 -0
  82. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/rag/__init__.py +0 -0
  83. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/rag/rag.py +0 -0
  84. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/rag/utils.py +0 -0
  85. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/__init__.py +0 -0
  86. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/dockerfile_template/Dockerfile.template +0 -0
  87. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/models/__init__.py +0 -0
  88. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/models/dummy_openai_model.py +0 -0
  89. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/models/mcp_class.py +0 -0
  90. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/models/model_builder.py +0 -0
  91. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/models/model_class.py +0 -0
  92. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/models/model_run_locally.py +0 -0
  93. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/models/model_runner.py +0 -0
  94. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/models/model_servicer.py +0 -0
  95. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/models/openai_class.py +0 -0
  96. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/models/visual_classifier_class.py +0 -0
  97. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/models/visual_detector_class.py +0 -0
  98. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/pipeline_steps/__init__.py +0 -0
  99. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/pipelines/__init__.py +0 -0
  100. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/server.py +0 -0
  101. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/utils/__init__.py +0 -0
  102. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/utils/code_script.py +0 -0
  103. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/utils/const.py +0 -0
  104. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/utils/data_types/__init__.py +0 -0
  105. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/utils/data_types/data_types.py +0 -0
  106. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/utils/data_utils.py +0 -0
  107. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/utils/loader.py +0 -0
  108. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/utils/method_signatures.py +0 -0
  109. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/utils/model_utils.py +0 -0
  110. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/utils/openai_convertor.py +0 -0
  111. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/utils/pipeline_validation.py +0 -0
  112. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/utils/serializers.py +0 -0
  113. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/runners/utils/url_fetcher.py +0 -0
  114. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/schema/search.py +0 -0
  115. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/urls/helper.py +0 -0
  116. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/utils/__init__.py +0 -0
  117. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/utils/config.py +0 -0
  118. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/utils/evaluation/__init__.py +0 -0
  119. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/utils/evaluation/helpers.py +0 -0
  120. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/utils/evaluation/main.py +0 -0
  121. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/utils/evaluation/testset_annotation_parser.py +0 -0
  122. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/utils/logging.py +0 -0
  123. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/utils/misc.py +0 -0
  124. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/utils/model_train.py +0 -0
  125. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/utils/protobuf.py +0 -0
  126. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/utils/secrets.py +0 -0
  127. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/versions.py +0 -0
  128. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/workflows/__init__.py +0 -0
  129. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/workflows/export.py +0 -0
  130. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/workflows/utils.py +0 -0
  131. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai/workflows/validate.py +0 -0
  132. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai.egg-info/dependency_links.txt +0 -0
  133. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai.egg-info/entry_points.txt +0 -0
  134. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai.egg-info/requires.txt +0 -0
  135. {clarifai-11.8.2 → clarifai-11.8.4}/clarifai.egg-info/top_level.txt +0 -0
  136. {clarifai-11.8.2 → clarifai-11.8.4}/pyproject.toml +0 -0
  137. {clarifai-11.8.2 → clarifai-11.8.4}/requirements.txt +0 -0
  138. {clarifai-11.8.2 → clarifai-11.8.4}/setup.cfg +0 -0
  139. {clarifai-11.8.2 → clarifai-11.8.4}/setup.py +0 -0
  140. {clarifai-11.8.2 → clarifai-11.8.4}/tests/test_app.py +0 -0
  141. {clarifai-11.8.2 → clarifai-11.8.4}/tests/test_async_stub.py +0 -0
  142. {clarifai-11.8.2 → clarifai-11.8.4}/tests/test_auth.py +0 -0
  143. {clarifai-11.8.2 → clarifai-11.8.4}/tests/test_data_upload.py +0 -0
  144. {clarifai-11.8.2 → clarifai-11.8.4}/tests/test_eval.py +0 -0
  145. {clarifai-11.8.2 → clarifai-11.8.4}/tests/test_list_models.py +0 -0
  146. {clarifai-11.8.2 → clarifai-11.8.4}/tests/test_misc.py +0 -0
  147. {clarifai-11.8.2 → clarifai-11.8.4}/tests/test_model_predict.py +0 -0
  148. {clarifai-11.8.2 → clarifai-11.8.4}/tests/test_model_train.py +0 -0
  149. {clarifai-11.8.2 → clarifai-11.8.4}/tests/test_modules.py +0 -0
  150. {clarifai-11.8.2 → clarifai-11.8.4}/tests/test_pipeline_client.py +0 -0
  151. {clarifai-11.8.2 → clarifai-11.8.4}/tests/test_rag.py +0 -0
  152. {clarifai-11.8.2 → clarifai-11.8.4}/tests/test_search.py +0 -0
  153. {clarifai-11.8.2 → clarifai-11.8.4}/tests/test_secrets.py +0 -0
  154. {clarifai-11.8.2 → clarifai-11.8.4}/tests/test_stub.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clarifai
3
- Version: 11.8.2
3
+ Version: 11.8.4
4
4
  Home-page: https://github.com/Clarifai/clarifai-python
5
5
  Author: Clarifai
6
6
  Author-email: support@clarifai.com
@@ -0,0 +1 @@
1
+ __version__ = "11.8.4"
@@ -28,6 +28,7 @@ from clarifai.utils.constants import (
28
28
  DEFAULT_LOCAL_RUNNER_NODEPOOL_CONFIG,
29
29
  DEFAULT_LOCAL_RUNNER_NODEPOOL_ID,
30
30
  DEFAULT_OLLAMA_MODEL_REPO_BRANCH,
31
+ DEFAULT_PYTHON_MODEL_REPO_BRANCH,
31
32
  DEFAULT_TOOLKIT_MODEL_REPO,
32
33
  DEFAULT_VLLM_MODEL_REPO_BRANCH,
33
34
  )
@@ -74,9 +75,11 @@ def model():
74
75
  )
75
76
  @click.option(
76
77
  '--toolkit',
77
- type=click.Choice(['ollama', 'huggingface', 'lmstudio', 'vllm'], case_sensitive=False),
78
+ type=click.Choice(
79
+ ['ollama', 'huggingface', 'lmstudio', 'vllm', 'python'], case_sensitive=False
80
+ ),
78
81
  required=False,
79
- help='Toolkit to use for model initialization. Currently supports "ollama", "huggingface", "lmstudio" and "vllm".',
82
+ help='Toolkit to use for model initialization. Currently supports "ollama", "huggingface", "lmstudio", "vllm" and "python".',
80
83
  )
81
84
  @click.option(
82
85
  '--model-name',
@@ -95,7 +98,9 @@ def model():
95
98
  help='Context length for the Ollama model. Defaults to 8192.',
96
99
  required=False,
97
100
  )
101
+ @click.pass_context
98
102
  def init(
103
+ ctx,
99
104
  model_path,
100
105
  model_type_id,
101
106
  github_pat,
@@ -124,11 +129,13 @@ def init(
124
129
  MODEL_TYPE_ID: Type of model to create. If not specified, defaults to "text-to-text" for text models.\n
125
130
  GITHUB_PAT: GitHub Personal Access Token for authentication when cloning private repositories.\n
126
131
  GITHUB_URL: GitHub repository URL or "repo" format to clone a repository from. If provided, the entire repository contents will be copied to the target directory instead of using default templates.\n
127
- TOOLKIT: Toolkit to use for model initialization. Currently supports "ollama", "huggingface", "lmstudio" and "vllm".\n
132
+ TOOLKIT: Toolkit to use for model initialization. Currently supports "ollama", "huggingface", "lmstudio", "vllm" and "python".\n
128
133
  MODEL_NAME: Model name to configure when using --toolkit. For ollama toolkit, this sets the Ollama model to use (e.g., "llama3.1", "mistral", etc.). For vllm & huggingface toolkit, this sets the Hugging Face model repo_id (e.g., "Qwen/Qwen3-4B-Instruct-2507"). For lmstudio toolkit, this sets the LM Studio model name (e.g., "qwen/qwen3-4b-thinking-2507").\n
129
134
  PORT: Port to run the (Ollama/lmstudio) server on. Defaults to 23333.\n
130
135
  CONTEXT_LENGTH: Context length for the (Ollama/lmstudio) model. Defaults to 8192.\n
131
136
  """
137
+ validate_context(ctx)
138
+ user_id = ctx.obj.current.user_id
132
139
  # Resolve the absolute path
133
140
  model_path = os.path.abspath(model_path)
134
141
 
@@ -176,6 +183,9 @@ def init(
176
183
  elif toolkit == 'vllm':
177
184
  github_url = DEFAULT_TOOLKIT_MODEL_REPO
178
185
  branch = DEFAULT_VLLM_MODEL_REPO_BRANCH
186
+ elif toolkit == 'python':
187
+ github_url = DEFAULT_TOOLKIT_MODEL_REPO
188
+ branch = DEFAULT_PYTHON_MODEL_REPO_BRANCH
179
189
 
180
190
  if github_url:
181
191
  downloader = GitHubDownloader(
@@ -231,6 +241,44 @@ def init(
231
241
  repo_url = format_github_repo_url(github_url)
232
242
  repo_url = f"https://github.com/{owner}/{repo}"
233
243
 
244
+ try:
245
+ # Create a temporary directory for cloning
246
+ with tempfile.TemporaryDirectory(prefix="clarifai_model_") as clone_dir:
247
+ # Clone the repository with explicit branch parameter
248
+ if not clone_github_repo(repo_url, clone_dir, github_pat, branch):
249
+ logger.error(f"Failed to clone repository from {repo_url}")
250
+ github_url = None # Fall back to template mode
251
+
252
+ else:
253
+ # Copy the entire repository content to target directory (excluding .git)
254
+ for item in os.listdir(clone_dir):
255
+ if item == '.git':
256
+ continue
257
+
258
+ source_path = os.path.join(clone_dir, item)
259
+ target_path = os.path.join(model_path, item)
260
+
261
+ if os.path.isdir(source_path):
262
+ shutil.copytree(source_path, target_path, dirs_exist_ok=True)
263
+ else:
264
+ shutil.copy2(source_path, target_path)
265
+
266
+ logger.info(f"Successfully cloned repository to {model_path}")
267
+ logger.info(
268
+ "Model initialization complete with GitHub repository clone"
269
+ )
270
+ logger.info("Next steps:")
271
+ logger.info("1. Review the model configuration")
272
+ logger.info("2. Install any required dependencies manually")
273
+ logger.info(
274
+ "3. Test the model locally using 'clarifai model local-test'"
275
+ )
276
+ return
277
+
278
+ except Exception as e:
279
+ logger.error(f"Failed to clone GitHub repository: {e}")
280
+ github_url = None # Fall back to template mode
281
+
234
282
  if toolkit:
235
283
  logger.info(f"Initializing model from GitHub repository: {github_url}")
236
284
 
@@ -240,41 +288,41 @@ def init(
240
288
  else:
241
289
  repo_url = format_github_repo_url(github_url)
242
290
 
243
- try:
244
- # Create a temporary directory for cloning
245
- with tempfile.TemporaryDirectory(prefix="clarifai_model_") as clone_dir:
246
- # Clone the repository with explicit branch parameter
247
- if not clone_github_repo(repo_url, clone_dir, github_pat, branch):
248
- logger.error(f"Failed to clone repository from {repo_url}")
249
- github_url = None # Fall back to template mode
250
-
251
- else:
252
- # Copy the entire repository content to target directory (excluding .git)
253
- for item in os.listdir(clone_dir):
254
- if item == '.git':
255
- continue
256
-
257
- source_path = os.path.join(clone_dir, item)
258
- target_path = os.path.join(model_path, item)
259
-
260
- if os.path.isdir(source_path):
261
- shutil.copytree(source_path, target_path, dirs_exist_ok=True)
262
- else:
263
- shutil.copy2(source_path, target_path)
291
+ try:
292
+ # Create a temporary directory for cloning
293
+ with tempfile.TemporaryDirectory(prefix="clarifai_model_") as clone_dir:
294
+ # Clone the repository with explicit branch parameter
295
+ if not clone_github_repo(repo_url, clone_dir, github_pat, branch):
296
+ logger.error(f"Failed to clone repository from {repo_url}")
297
+ github_url = None # Fall back to template mode
264
298
 
265
- except Exception as e:
266
- logger.error(f"Failed to clone GitHub repository: {e}")
267
- github_url = None
299
+ else:
300
+ # Copy the entire repository content to target directory (excluding .git)
301
+ for item in os.listdir(clone_dir):
302
+ if item == '.git':
303
+ continue
304
+
305
+ source_path = os.path.join(clone_dir, item)
306
+ target_path = os.path.join(model_path, item)
268
307
 
269
- if (model_name or port or context_length) and (toolkit == 'ollama'):
270
- customize_ollama_model(model_path, model_name, port, context_length)
308
+ if os.path.isdir(source_path):
309
+ shutil.copytree(source_path, target_path, dirs_exist_ok=True)
310
+ else:
311
+ shutil.copy2(source_path, target_path)
271
312
 
272
- if (model_name or port or context_length) and (toolkit == 'lmstudio'):
273
- customize_lmstudio_model(model_path, model_name, port, context_length)
313
+ except Exception as e:
314
+ logger.error(f"Failed to clone GitHub repository: {e}")
315
+ github_url = None
316
+
317
+ if (user_id or model_name or port or context_length) and (toolkit == 'ollama'):
318
+ customize_ollama_model(model_path, user_id, model_name, port, context_length)
319
+
320
+ if (user_id or model_name or port or context_length) and (toolkit == 'lmstudio'):
321
+ customize_lmstudio_model(model_path, user_id, model_name, port, context_length)
274
322
 
275
- if model_name and (toolkit == 'huggingface' or toolkit == 'vllm'):
323
+ if (user_id or model_name) and (toolkit == 'huggingface' or toolkit == 'vllm'):
276
324
  # Update the config.yaml file with the provided model name
277
- customize_huggingface_model(model_path, model_name)
325
+ customize_huggingface_model(model_path, user_id, model_name)
278
326
 
279
327
  if github_url:
280
328
  logger.info("Model initialization complete with GitHub repository")
@@ -288,12 +336,20 @@ def init(
288
336
  logger.info("Initializing model with default templates...")
289
337
  input("Press Enter to continue...")
290
338
 
339
+ from clarifai.cli.base import input_or_default
291
340
  from clarifai.cli.templates.model_templates import (
292
341
  get_config_template,
293
342
  get_model_template,
294
343
  get_requirements_template,
295
344
  )
296
345
 
346
+ # Collect additional parameters for OpenAI template
347
+ template_kwargs = {}
348
+ if model_type_id == "openai":
349
+ logger.info("Configuring OpenAI local runner...")
350
+ port = input_or_default("Enter port (default: 8000): ", "8000")
351
+ template_kwargs = {"port": port}
352
+
297
353
  # Create the 1/ subdirectory
298
354
  model_version_dir = os.path.join(model_path, "1")
299
355
  os.makedirs(model_version_dir, exist_ok=True)
@@ -303,7 +359,7 @@ def init(
303
359
  if os.path.exists(model_py_path):
304
360
  logger.warning(f"File {model_py_path} already exists, skipping...")
305
361
  else:
306
- model_template = get_model_template(model_type_id)
362
+ model_template = get_model_template(model_type_id, **template_kwargs)
307
363
  with open(model_py_path, 'w') as f:
308
364
  f.write(model_template)
309
365
  logger.info(f"Created {model_py_path}")
@@ -325,7 +381,9 @@ def init(
325
381
  else:
326
382
  config_model_type_id = DEFAULT_LOCAL_RUNNER_MODEL_TYPE # default
327
383
 
328
- config_template = get_config_template(config_model_type_id)
384
+ config_template = get_config_template(
385
+ user_id=user_id, model_type_id=config_model_type_id
386
+ )
329
387
  with open(config_path, 'w') as f:
330
388
  f.write(config_template)
331
389
  logger.info(f"Created {config_path}")
@@ -26,14 +26,19 @@ def pipeline():
26
26
 
27
27
  @pipeline.command()
28
28
  @click.argument("path", type=click.Path(exists=True), required=False, default=".")
29
- def upload(path):
29
+ @click.option(
30
+ '--no-lockfile',
31
+ is_flag=True,
32
+ help='Skip creating config-lock.yaml file.',
33
+ )
34
+ def upload(path, no_lockfile):
30
35
  """Upload a pipeline with associated pipeline steps to Clarifai.
31
36
 
32
37
  PATH: Path to the pipeline configuration file or directory containing config.yaml. If not specified, the current directory is used by default.
33
38
  """
34
39
  from clarifai.runners.pipelines.pipeline_builder import upload_pipeline
35
40
 
36
- upload_pipeline(path)
41
+ upload_pipeline(path, no_lockfile=no_lockfile)
37
42
 
38
43
 
39
44
  @pipeline.command()
@@ -106,15 +111,32 @@ def run(
106
111
 
107
112
  validate_context(ctx)
108
113
 
114
+ # Try to load from config-lock.yaml first if no config is specified
115
+ lockfile_path = os.path.join(os.getcwd(), "config-lock.yaml")
116
+ if not config and os.path.exists(lockfile_path):
117
+ logger.info("Found config-lock.yaml, using it as default config source")
118
+ config = lockfile_path
119
+
109
120
  if config:
110
121
  config_data = from_yaml(config)
111
- pipeline_id = config_data.get('pipeline_id', pipeline_id)
112
- pipeline_version_id = config_data.get('pipeline_version_id', pipeline_version_id)
122
+
123
+ # Handle both regular config format and lockfile format
124
+ if 'pipeline' in config_data and isinstance(config_data['pipeline'], dict):
125
+ pipeline_config = config_data['pipeline']
126
+ pipeline_id = pipeline_config.get('id', pipeline_id)
127
+ pipeline_version_id = pipeline_config.get('version_id', pipeline_version_id)
128
+ user_id = pipeline_config.get('user_id', user_id)
129
+ app_id = pipeline_config.get('app_id', app_id)
130
+ else:
131
+ # Fallback to flat config structure
132
+ pipeline_id = config_data.get('pipeline_id', pipeline_id)
133
+ pipeline_version_id = config_data.get('pipeline_version_id', pipeline_version_id)
134
+ user_id = config_data.get('user_id', user_id)
135
+ app_id = config_data.get('app_id', app_id)
136
+
113
137
  pipeline_version_run_id = config_data.get(
114
138
  'pipeline_version_run_id', pipeline_version_run_id
115
139
  )
116
- user_id = config_data.get('user_id', user_id)
117
- app_id = config_data.get('app_id', app_id)
118
140
  nodepool_id = config_data.get('nodepool_id', nodepool_id)
119
141
  compute_cluster_id = config_data.get('compute_cluster_id', compute_cluster_id)
120
142
  pipeline_url = config_data.get('pipeline_url', pipeline_url)
@@ -319,6 +341,62 @@ def init(pipeline_path):
319
341
  logger.info("3. Run 'clarifai pipeline upload config.yaml' to upload your pipeline")
320
342
 
321
343
 
344
+ @pipeline.command()
345
+ @click.argument(
346
+ "lockfile_path", type=click.Path(exists=True), required=False, default="config-lock.yaml"
347
+ )
348
+ def validate_lock(lockfile_path):
349
+ """Validate a config-lock.yaml file for schema and reference consistency.
350
+
351
+ LOCKFILE_PATH: Path to the config-lock.yaml file. If not specified, looks for config-lock.yaml in current directory.
352
+ """
353
+ from clarifai.runners.utils.pipeline_validation import PipelineConfigValidator
354
+ from clarifai.utils.cli import from_yaml
355
+
356
+ try:
357
+ # Load the lockfile
358
+ lockfile_data = from_yaml(lockfile_path)
359
+
360
+ # Validate required fields
361
+ if "pipeline" not in lockfile_data:
362
+ raise ValueError("'pipeline' section not found in lockfile")
363
+
364
+ pipeline = lockfile_data["pipeline"]
365
+ required_fields = ["id", "user_id", "app_id", "version_id"]
366
+
367
+ for field in required_fields:
368
+ if field not in pipeline:
369
+ raise ValueError(f"Required field '{field}' not found in pipeline section")
370
+ if not pipeline[field]:
371
+ raise ValueError(f"Required field '{field}' cannot be empty")
372
+
373
+ # Validate orchestration spec if present
374
+ if "orchestration_spec" in pipeline:
375
+ # Create a temporary config structure for validation
376
+ temp_config = {
377
+ "pipeline": {
378
+ "id": pipeline["id"],
379
+ "user_id": pipeline["user_id"],
380
+ "app_id": pipeline["app_id"],
381
+ "orchestration_spec": pipeline["orchestration_spec"],
382
+ }
383
+ }
384
+
385
+ # Use existing validator to check orchestration spec
386
+ validator = PipelineConfigValidator()
387
+ validator._validate_orchestration_spec(temp_config)
388
+
389
+ logger.info(f"✅ Lockfile {lockfile_path} is valid")
390
+ logger.info(f"Pipeline: {pipeline['id']}")
391
+ logger.info(f"User: {pipeline['user_id']}")
392
+ logger.info(f"App: {pipeline['app_id']}")
393
+ logger.info(f"Version: {pipeline['version_id']}")
394
+
395
+ except Exception as e:
396
+ logger.error(f"❌ Lockfile validation failed: {e}")
397
+ raise click.Abort()
398
+
399
+
322
400
  @pipeline.command(['ls'])
323
401
  @click.option('--page_no', required=False, help='Page number to list.', default=1)
324
402
  @click.option('--per_page', required=False, help='Number of items per page.', default=16)
@@ -99,9 +99,9 @@ class MyModel(MCPModelClass):
99
99
  '''
100
100
 
101
101
 
102
- def get_openai_model_class_template() -> str:
102
+ def get_openai_model_class_template(port: str = "8000") -> str:
103
103
  """Return the template for an OpenAIModelClass-based model."""
104
- return '''from typing import List
104
+ return f'''from typing import List, Iterator
105
105
  from openai import OpenAI
106
106
  from clarifai.runners.models.openai_class import OpenAIModelClass
107
107
  from clarifai.runners.utils.data_utils import Param
@@ -114,12 +114,11 @@ class MyModel(OpenAIModelClass):
114
114
  # Configure your OpenAI-compatible client for local model
115
115
  client = OpenAI(
116
116
  api_key="local-key", # TODO: please fill in - use your local API key
117
- base_url="http://localhost:8000/v1", # TODO: please fill in - your local model server endpoint
117
+ base_url="http://localhost:{port}/v1", # TODO: please fill in - your local model server endpoint
118
118
  )
119
119
 
120
- # TODO: please fill in
121
- # Specify the model name to use
122
- model = "my-local-model" # TODO: please fill in - replace with your local model name
120
+ # Automatically get the first available model
121
+ model = client.models.list().data[0].id
123
122
 
124
123
  def load_model(self):
125
124
  """Optional: Add any additional model loading logic here."""
@@ -157,7 +156,7 @@ class MyModel(OpenAIModelClass):
157
156
  max_tokens: int = Param(default=256, description="The maximum number of tokens to generate. Shorter token lengths will provide faster performance."),
158
157
  temperature: float = Param(default=1.0, description="A decimal number that determines the degree of randomness in the response"),
159
158
  top_p: float = Param(default=1.0, description="An alternative to sampling with temperature, where the model considers the results of the tokens with top_p probability mass."),
160
- ):
159
+ ) -> Iterator[str]:
161
160
  """Stream a completion response using the OpenAI client."""
162
161
  # TODO: please fill in
163
162
  # Implement your streaming logic here
@@ -178,13 +177,13 @@ class MyModel(OpenAIModelClass):
178
177
  '''
179
178
 
180
179
 
181
- def get_config_template(model_type_id: str = "any-to-any") -> str:
180
+ def get_config_template(user_id: str = None, model_type_id: str = "any-to-any") -> str:
182
181
  """Return the template for config.yaml."""
183
182
  return f'''# Configuration file for your Clarifai model
184
183
 
185
184
  model:
186
185
  id: "my-model" # TODO: please fill in - replace with your model ID
187
- user_id: "user_id" # TODO: please fill in - replace with your user ID
186
+ user_id: "{user_id}" # TODO: please fill in - replace with your user ID
188
187
  app_id: "app_id" # TODO: please fill in - replace with your app ID
189
188
  model_type_id: "{model_type_id}" # TODO: please fill in - replace if different model type ID
190
189
 
@@ -237,8 +236,16 @@ MODEL_TYPE_TEMPLATES = {
237
236
  }
238
237
 
239
238
 
240
- def get_model_template(model_type_id: str = None) -> str:
239
+ def get_model_template(model_type_id: str = None, **kwargs) -> str:
241
240
  """Get the appropriate model template based on model_type_id."""
242
241
  if model_type_id in MODEL_TYPE_TEMPLATES:
243
- return MODEL_TYPE_TEMPLATES[model_type_id]()
242
+ template_func = MODEL_TYPE_TEMPLATES[model_type_id]
243
+ # Check if the template function accepts additional parameters
244
+ import inspect
245
+
246
+ sig = inspect.signature(template_func)
247
+ if len(sig.parameters) > 0:
248
+ return template_func(**kwargs)
249
+ else:
250
+ return template_func()
244
251
  return get_model_class_template()
@@ -108,6 +108,7 @@ class Model(Lister, BaseClient):
108
108
  self.training_params = {}
109
109
  self.input_types = None
110
110
  self._client = None
111
+ self._async_client = None
111
112
  self._added_methods = False
112
113
  BaseClient.__init__(
113
114
  self,
@@ -522,11 +523,29 @@ class Model(Lister, BaseClient):
522
523
  model=self.model_info,
523
524
  runner_selector=self._runner_selector,
524
525
  )
526
+ # Pass in None for async stub will create it.
525
527
  self._client = ModelClient(
526
- stub=self.STUB, async_stub=self.async_stub, request_template=request_template
528
+ stub=self.STUB, async_stub=None, request_template=request_template
527
529
  )
528
530
  return self._client
529
531
 
532
+ @property
533
+ def async_client(self):
534
+ """Get the asynchronous client instance (with async stub)."""
535
+ if self._async_client is None:
536
+ request_template = service_pb2.PostModelOutputsRequest(
537
+ user_app_id=self.user_app_id,
538
+ model_id=self.id,
539
+ version_id=self.model_version.id,
540
+ model=self.model_info,
541
+ runner_selector=self._runner_selector,
542
+ )
543
+ # Create async client with async stub
544
+ self._async_client = ModelClient(
545
+ stub=self.STUB, async_stub=self.async_stub, request_template=request_template
546
+ )
547
+ return self._async_client
548
+
530
549
  def predict(self, *args, **kwargs):
531
550
  """
532
551
  Calls the model's predict() method with the given arguments.
@@ -573,16 +592,16 @@ class Model(Lister, BaseClient):
573
592
  )
574
593
  inference_params = kwargs.get('inference_params', {})
575
594
  output_config = kwargs.get('output_config', {})
576
- return await self.client._async_predict_by_proto(
595
+ return await self.async_client._async_predict_by_proto(
577
596
  inputs=inputs, inference_params=inference_params, output_config=output_config
578
597
  )
579
598
 
580
599
  # Adding try-except, since the await works differently with jupyter kernels and in regular python scripts.
581
600
  try:
582
- return await self.client.predict(*args, **kwargs)
601
+ return await self.async_client.predict(*args, **kwargs)
583
602
  except TypeError:
584
603
  # In jupyter, it returns a str object instead of a co-routine.
585
- return self.client.predict(*args, **kwargs)
604
+ return self.async_client.predict(*args, **kwargs)
586
605
 
587
606
  def __getattr__(self, name):
588
607
  try:
@@ -595,7 +614,10 @@ class Model(Lister, BaseClient):
595
614
  self.client.fetch()
596
615
  for method_name in self.client._method_signatures.keys():
597
616
  if not hasattr(self, method_name):
598
- setattr(self, method_name, getattr(self.client, method_name))
617
+ if method_name.startswith('async_'):
618
+ setattr(self, method_name, getattr(self.async_client, method_name))
619
+ else:
620
+ setattr(self, method_name, getattr(self.client, method_name))
599
621
  if hasattr(self.client, name):
600
622
  return getattr(self.client, name)
601
623
  raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
@@ -838,11 +860,11 @@ class Model(Lister, BaseClient):
838
860
  )
839
861
  inference_params = kwargs.get('inference_params', {})
840
862
  output_config = kwargs.get('output_config', {})
841
- return self.client._async_generate_by_proto(
863
+ return self.async_client._async_generate_by_proto(
842
864
  inputs=inputs, inference_params=inference_params, output_config=output_config
843
865
  )
844
866
 
845
- return self.client.generate(*args, **kwargs)
867
+ return self.async_client.generate(*args, **kwargs)
846
868
 
847
869
  def generate_by_filepath(
848
870
  self,
@@ -1047,11 +1069,11 @@ class Model(Lister, BaseClient):
1047
1069
  )
1048
1070
  inference_params = kwargs.get('inference_params', {})
1049
1071
  output_config = kwargs.get('output_config', {})
1050
- return self.client._async_stream_by_proto(
1072
+ return self.async_client._async_stream_by_proto(
1051
1073
  inputs=inputs, inference_params=inference_params, output_config=output_config
1052
1074
  )
1053
1075
 
1054
- return self.client.async_stream(*args, **kwargs)
1076
+ return self.async_client.async_stream(*args, **kwargs)
1055
1077
 
1056
1078
  def stream_by_filepath(
1057
1079
  self,
@@ -4,6 +4,7 @@ import sys
4
4
  import tarfile
5
5
  import time
6
6
  from string import Template
7
+ from typing import List, Optional
7
8
 
8
9
  import yaml
9
10
  from clarifai_grpc.grpc.api import resources_pb2, service_pb2
@@ -11,6 +12,7 @@ from clarifai_grpc.grpc.api.status import status_code_pb2
11
12
  from google.protobuf import json_format
12
13
 
13
14
  from clarifai.client.base import BaseClient
15
+ from clarifai.utils.hashing import hash_directory
14
16
  from clarifai.utils.logging import logger
15
17
  from clarifai.utils.misc import get_uuid
16
18
  from clarifai.versions import CLIENT_VERSION
@@ -22,12 +24,13 @@ UPLOAD_CHUNK_SIZE = 14 * 1024 * 1024
22
24
  class PipelineStepBuilder:
23
25
  """Pipeline Step Builder class for managing pipeline step upload to Clarifai."""
24
26
 
25
- def __init__(self, folder: str):
27
+ def __init__(self, folder: str, hash_exclusions: Optional[List[str]] = None):
26
28
  """
27
29
  Initialize PipelineStepBuilder.
28
30
 
29
31
  :param folder: The folder containing the pipeline step files (config.yaml, requirements.txt,
30
32
  dockerfile, and pipeline_step.py in 1/ subdirectory)
33
+ :param hash_exclusions: List of file names to exclude from hash calculation (defaults to ['config-lock.yaml'])
31
34
  """
32
35
  self._client = None
33
36
  self.folder = self._validate_folder(folder)
@@ -37,6 +40,10 @@ class PipelineStepBuilder:
37
40
  self.pipeline_step_id = self.pipeline_step_proto.id
38
41
  self.pipeline_step_version_id = None
39
42
  self.pipeline_step_compute_info = self._get_pipeline_step_compute_info()
43
+ # Configure files to exclude from hash calculation
44
+ self.hash_exclusions = (
45
+ hash_exclusions if hash_exclusions is not None else ['config-lock.yaml']
46
+ )
40
47
 
41
48
  @property
42
49
  def client(self):
@@ -490,6 +497,95 @@ COPY --link=true requirements.txt config.yaml /home/nonroot/main/
490
497
 
491
498
  raise TimeoutError("Pipeline step build did not finish in time")
492
499
 
500
+ def load_config_lock(self):
501
+ """
502
+ Load existing config-lock.yaml if it exists.
503
+
504
+ :return: Dictionary with config-lock data or None if file doesn't exist
505
+ """
506
+ config_lock_path = os.path.join(self.folder, "config-lock.yaml")
507
+ if os.path.exists(config_lock_path):
508
+ try:
509
+ with open(config_lock_path, 'r', encoding='utf-8') as f:
510
+ return yaml.safe_load(f)
511
+ except Exception as e:
512
+ logger.warning(f"Failed to load config-lock.yaml: {e}")
513
+ return None
514
+ return None
515
+
516
+ def should_upload_step(self, algo="md5"):
517
+ """
518
+ Check if the pipeline step should be uploaded based on hash comparison.
519
+
520
+ :param algo: Hash algorithm to use
521
+ :return: True if step should be uploaded, False otherwise
522
+ """
523
+ config_lock = self.load_config_lock()
524
+
525
+ # If no config-lock.yaml exists, upload the step (first time upload)
526
+ if config_lock is None:
527
+ logger.info("No config-lock.yaml found, will upload pipeline step")
528
+ return True
529
+
530
+ # Compare stored hash with freshly computed one
531
+ current_hash = hash_directory(self.folder, algo=algo, exclude_files=self.hash_exclusions)
532
+ stored_hash_info = config_lock.get("hash", {})
533
+ stored_hash = stored_hash_info.get("value", "")
534
+ stored_algo = stored_hash_info.get("algo", "md5")
535
+
536
+ # If algorithm changed, re-upload to update hash
537
+ if stored_algo != algo:
538
+ logger.info(
539
+ f"Hash algorithm changed from {stored_algo} to {algo}, will upload pipeline step"
540
+ )
541
+ return True
542
+
543
+ # If hash changed, upload
544
+ if current_hash != stored_hash:
545
+ logger.info(
546
+ f"Hash changed (was: {stored_hash}, now: {current_hash}), will upload pipeline step"
547
+ )
548
+ return True
549
+
550
+ logger.info(f"Hash unchanged ({current_hash}), skipping pipeline step upload")
551
+ return False
552
+
553
+ def generate_config_lock(self, version_id, algo="md5"):
554
+ """
555
+ Generate config-lock.yaml content for the pipeline step.
556
+
557
+ :param version_id: Pipeline step version ID
558
+ :param algo: Hash algorithm used
559
+ :return: Dictionary with config-lock data
560
+ """
561
+ # Compute hash
562
+ hash_value = hash_directory(self.folder, algo=algo, exclude_files=self.hash_exclusions)
563
+
564
+ # Create config-lock structure
565
+ config_lock = {"id": version_id, "hash": {"algo": algo, "value": hash_value}}
566
+
567
+ # Append the original config.yaml contents
568
+ config_lock.update(self.config)
569
+
570
+ return config_lock
571
+
572
+ def save_config_lock(self, version_id, algo="md5"):
573
+ """
574
+ Save config-lock.yaml file with pipeline step metadata.
575
+
576
+ :param version_id: Pipeline step version ID
577
+ :param algo: Hash algorithm used
578
+ """
579
+ config_lock_data = self.generate_config_lock(version_id, algo)
580
+ config_lock_path = os.path.join(self.folder, "config-lock.yaml")
581
+
582
+ try:
583
+ with open(config_lock_path, 'w', encoding='utf-8') as f:
584
+ yaml.dump(config_lock_data, f, default_flow_style=False, allow_unicode=True)
585
+ logger.info(f"Generated config-lock.yaml at {config_lock_path}")
586
+ except Exception as e:
587
+ logger.error(f"Failed to save config-lock.yaml: {e}")
588
+
493
589
 
494
590
  def upload_pipeline_step(folder, skip_dockerfile=False):
495
591
  """