apheris-foundry-cli 0.2.0__tar.gz → 0.3.0__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 (39) hide show
  1. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/PKG-INFO +36 -17
  2. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/README.md +35 -16
  3. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/pyproject.toml +1 -1
  4. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/src/apheris_foundry_cli.egg-info/PKG-INFO +36 -17
  5. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/src/apherisfold_cli/commands/foundry.py +12 -1
  6. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/src/apherisfold_cli/commands/login.py +5 -5
  7. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/src/apherisfold_cli/core/auth.py +15 -13
  8. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/src/apherisfold_cli/core/hub_requests.py +15 -3
  9. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/src/apherisfold_cli/main.py +168 -17
  10. apheris_foundry_cli-0.3.0/src/apherisfold_cli/version.py +1 -0
  11. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/tests/test_auth.py +8 -0
  12. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/tests/test_cli.py +162 -6
  13. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/tests/test_hub_requests.py +16 -0
  14. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/tests/test_main.py +5 -5
  15. apheris_foundry_cli-0.2.0/src/apherisfold_cli/version.py +0 -1
  16. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/LICENSE +0 -0
  17. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/setup.cfg +0 -0
  18. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/src/apheris_foundry_cli.egg-info/SOURCES.txt +0 -0
  19. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/src/apheris_foundry_cli.egg-info/dependency_links.txt +0 -0
  20. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/src/apheris_foundry_cli.egg-info/entry_points.txt +0 -0
  21. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/src/apheris_foundry_cli.egg-info/requires.txt +0 -0
  22. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/src/apheris_foundry_cli.egg-info/top_level.txt +0 -0
  23. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/src/apherisfold_cli/__init__.py +0 -0
  24. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/src/apherisfold_cli/commands/__init__.py +0 -0
  25. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/src/apherisfold_cli/core/__init__.py +0 -0
  26. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/src/apherisfold_cli/core/config.py +0 -0
  27. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/src/apherisfold_cli/core/context.py +0 -0
  28. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/src/apherisfold_cli/core/errors.py +0 -0
  29. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/src/apherisfold_cli/core/io.py +0 -0
  30. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/src/apherisfold_cli/core/login_profiles.py +0 -0
  31. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/src/apherisfold_cli/core/render.py +0 -0
  32. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/src/apherisfold_cli/core/result_artifacts.py +0 -0
  33. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/src/apherisfold_cli/handlers.py +0 -0
  34. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/tests/test_config.py +0 -0
  35. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/tests/test_foundry_helpers.py +0 -0
  36. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/tests/test_handlers.py +0 -0
  37. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/tests/test_io.py +0 -0
  38. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/tests/test_render.py +0 -0
  39. {apheris_foundry_cli-0.2.0 → apheris_foundry_cli-0.3.0}/tests/test_result_artifacts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apheris-foundry-cli
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Command-line interface for Apheris Foundry workflows.
5
5
  Author: Apheris
6
6
  License-Expression: LicenseRef-Apheris-Foundry-Software-License
@@ -39,37 +39,37 @@ Recommended if you already use conda for scientific Python work. A dedicated env
39
39
  #### Linux / macOS
40
40
 
41
41
  ```bash
42
- conda create -n apherisfold python=3.11 -y
43
- conda activate apherisfold
44
- pip install apherisfold-cli
42
+ conda create -n apheris-foundry python=3.11 -y
43
+ conda activate apheris-foundry
44
+ pip install apheris-foundry-cli
45
45
  ```
46
46
 
47
47
  #### Windows (Anaconda Prompt or PowerShell)
48
48
 
49
49
  ```powershell
50
- conda create -n apherisfold python=3.11 -y
51
- conda activate apherisfold
52
- pip install apherisfold-cli
50
+ conda create -n apheris-foundry python=3.11 -y
51
+ conda activate apheris-foundry
52
+ pip install apheris-foundry-cli
53
53
  ```
54
54
 
55
- Activate the environment at the start of each session with `conda activate apherisfold`.
55
+ Activate the environment at the start of each session with `conda activate apheris-foundry`.
56
56
 
57
57
  ### venv (standard Python)
58
58
 
59
59
  #### Linux / macOS (venv)
60
60
 
61
61
  ```bash
62
- python3 -m venv ~/.venvs/apherisfold
63
- source ~/.venvs/apherisfold/bin/activate
64
- pip install apherisfold-cli
62
+ python3 -m venv ~/.venvs/apheris-foundry
63
+ source ~/.venvs/apheris-foundry/bin/activate
64
+ pip install apheris-foundry-cli
65
65
  ```
66
66
 
67
67
  #### Windows (PowerShell, venv)
68
68
 
69
69
  ```powershell
70
- python -m venv $env:USERPROFILE\.venvs\apherisfold
71
- & $env:USERPROFILE\.venvs\apherisfold\Scripts\Activate.ps1
72
- pip install apherisfold-cli
70
+ python -m venv $env:USERPROFILE\.venvs\apheris-foundry
71
+ & $env:USERPROFILE\.venvs\apheris-foundry\Scripts\Activate.ps1
72
+ pip install apheris-foundry-cli
73
73
  ```
74
74
 
75
75
  Activate the environment at the start of each session with the `activate` command above.
@@ -84,7 +84,7 @@ apheris-foundry --help
84
84
  ### Upgrade
85
85
 
86
86
  ```bash
87
- pip install --upgrade apherisfold-cli
87
+ pip install --upgrade apheris-foundry-cli
88
88
  ```
89
89
 
90
90
  ## Quick start
@@ -99,6 +99,22 @@ apheris-foundry workflows list
99
99
  # Submit a run
100
100
  apheris-foundry workflows run --workflow predict --input ./request.json
101
101
 
102
+ # Inspect the model IDs, weights, and default parameters advertised by Hub
103
+ apheris-foundry workflows models --workflow predict
104
+
105
+ # Override prediction parameters with a Hub-schema-validated JSON object
106
+ apheris-foundry workflows run --workflow predict --input ./request.json \
107
+ --model-params '{"seeds":[7,9],"num_diffusion_samples":3}'
108
+
109
+ # Or provide the same JSON object from a file
110
+ apheris-foundry workflows run --workflow predict --input ./request.json \
111
+ --model-params @./model-params.json
112
+
113
+ # Select a non-default model/weight advertised by the workflow
114
+ apheris-foundry workflows run --workflow predict --input ./request.json \
115
+ --param model=<model-id> --weight <weight-version> \
116
+ --model-params @./model-params.json
117
+
102
118
  # List submitted runs
103
119
  apheris-foundry jobs list
104
120
 
@@ -132,9 +148,10 @@ apheris-foundry jobs error --id <job-id>
132
148
  | Command | Description |
133
149
  | ------- | ----------- |
134
150
  | `apheris-foundry workflows list` | List workflow definitions exposed by Hub |
135
- | `apheris-foundry workflows get --workflow <id>` | Inspect one workflow definition |
151
+ | `apheris-foundry workflows get --workflow <id>` | Inspect one workflow definition and advertised model defaults |
152
+ | `apheris-foundry workflows models --workflow <id>` | List models, weights, and default prediction parameters advertised by Hub (`--json` includes full metadata) |
136
153
  | `apheris-foundry workflows graph --workflow <id>` | Show the workflow DAG |
137
- | `apheris-foundry workflows run --workflow <id>` | Submit a workflow run |
154
+ | `apheris-foundry workflows run --workflow <id> [--model-params <JSON\|@file>]` | Submit a workflow run with Hub-schema-validated model parameters |
138
155
 
139
156
  ### Run instances (`runs`)
140
157
 
@@ -153,6 +170,8 @@ apheris-foundry workflows run --help
153
170
  apheris-foundry jobs get --help
154
171
  ```
155
172
 
173
+ `--model-params` accepts one JSON object. Use an inline object for short overrides, or prefix a JSON filename with `@` to load the object from that file. Before submitting the run, the CLI retrieves the selected model version, weight, and capability schema from Hub and validates the object. Use `workflows models` to discover available model IDs and weights.
174
+
156
175
  ## License
157
176
 
158
177
  This package is licensed under the [Apheris Foundry Software License](https://www.apheris.com/docs/hub/hub-license.html). The full license text is also included in every published wheel as `LICENSE`.
@@ -16,37 +16,37 @@ Recommended if you already use conda for scientific Python work. A dedicated env
16
16
  #### Linux / macOS
17
17
 
18
18
  ```bash
19
- conda create -n apherisfold python=3.11 -y
20
- conda activate apherisfold
21
- pip install apherisfold-cli
19
+ conda create -n apheris-foundry python=3.11 -y
20
+ conda activate apheris-foundry
21
+ pip install apheris-foundry-cli
22
22
  ```
23
23
 
24
24
  #### Windows (Anaconda Prompt or PowerShell)
25
25
 
26
26
  ```powershell
27
- conda create -n apherisfold python=3.11 -y
28
- conda activate apherisfold
29
- pip install apherisfold-cli
27
+ conda create -n apheris-foundry python=3.11 -y
28
+ conda activate apheris-foundry
29
+ pip install apheris-foundry-cli
30
30
  ```
31
31
 
32
- Activate the environment at the start of each session with `conda activate apherisfold`.
32
+ Activate the environment at the start of each session with `conda activate apheris-foundry`.
33
33
 
34
34
  ### venv (standard Python)
35
35
 
36
36
  #### Linux / macOS (venv)
37
37
 
38
38
  ```bash
39
- python3 -m venv ~/.venvs/apherisfold
40
- source ~/.venvs/apherisfold/bin/activate
41
- pip install apherisfold-cli
39
+ python3 -m venv ~/.venvs/apheris-foundry
40
+ source ~/.venvs/apheris-foundry/bin/activate
41
+ pip install apheris-foundry-cli
42
42
  ```
43
43
 
44
44
  #### Windows (PowerShell, venv)
45
45
 
46
46
  ```powershell
47
- python -m venv $env:USERPROFILE\.venvs\apherisfold
48
- & $env:USERPROFILE\.venvs\apherisfold\Scripts\Activate.ps1
49
- pip install apherisfold-cli
47
+ python -m venv $env:USERPROFILE\.venvs\apheris-foundry
48
+ & $env:USERPROFILE\.venvs\apheris-foundry\Scripts\Activate.ps1
49
+ pip install apheris-foundry-cli
50
50
  ```
51
51
 
52
52
  Activate the environment at the start of each session with the `activate` command above.
@@ -61,7 +61,7 @@ apheris-foundry --help
61
61
  ### Upgrade
62
62
 
63
63
  ```bash
64
- pip install --upgrade apherisfold-cli
64
+ pip install --upgrade apheris-foundry-cli
65
65
  ```
66
66
 
67
67
  ## Quick start
@@ -76,6 +76,22 @@ apheris-foundry workflows list
76
76
  # Submit a run
77
77
  apheris-foundry workflows run --workflow predict --input ./request.json
78
78
 
79
+ # Inspect the model IDs, weights, and default parameters advertised by Hub
80
+ apheris-foundry workflows models --workflow predict
81
+
82
+ # Override prediction parameters with a Hub-schema-validated JSON object
83
+ apheris-foundry workflows run --workflow predict --input ./request.json \
84
+ --model-params '{"seeds":[7,9],"num_diffusion_samples":3}'
85
+
86
+ # Or provide the same JSON object from a file
87
+ apheris-foundry workflows run --workflow predict --input ./request.json \
88
+ --model-params @./model-params.json
89
+
90
+ # Select a non-default model/weight advertised by the workflow
91
+ apheris-foundry workflows run --workflow predict --input ./request.json \
92
+ --param model=<model-id> --weight <weight-version> \
93
+ --model-params @./model-params.json
94
+
79
95
  # List submitted runs
80
96
  apheris-foundry jobs list
81
97
 
@@ -109,9 +125,10 @@ apheris-foundry jobs error --id <job-id>
109
125
  | Command | Description |
110
126
  | ------- | ----------- |
111
127
  | `apheris-foundry workflows list` | List workflow definitions exposed by Hub |
112
- | `apheris-foundry workflows get --workflow <id>` | Inspect one workflow definition |
128
+ | `apheris-foundry workflows get --workflow <id>` | Inspect one workflow definition and advertised model defaults |
129
+ | `apheris-foundry workflows models --workflow <id>` | List models, weights, and default prediction parameters advertised by Hub (`--json` includes full metadata) |
113
130
  | `apheris-foundry workflows graph --workflow <id>` | Show the workflow DAG |
114
- | `apheris-foundry workflows run --workflow <id>` | Submit a workflow run |
131
+ | `apheris-foundry workflows run --workflow <id> [--model-params <JSON\|@file>]` | Submit a workflow run with Hub-schema-validated model parameters |
115
132
 
116
133
  ### Run instances (`runs`)
117
134
 
@@ -130,6 +147,8 @@ apheris-foundry workflows run --help
130
147
  apheris-foundry jobs get --help
131
148
  ```
132
149
 
150
+ `--model-params` accepts one JSON object. Use an inline object for short overrides, or prefix a JSON filename with `@` to load the object from that file. Before submitting the run, the CLI retrieves the selected model version, weight, and capability schema from Hub and validates the object. Use `workflows models` to discover available model IDs and weights.
151
+
133
152
  ## License
134
153
 
135
154
  This package is licensed under the [Apheris Foundry Software License](https://www.apheris.com/docs/hub/hub-license.html). The full license text is also included in every published wheel as `LICENSE`.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "apheris-foundry-cli"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Command-line interface for Apheris Foundry workflows."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apheris-foundry-cli
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Command-line interface for Apheris Foundry workflows.
5
5
  Author: Apheris
6
6
  License-Expression: LicenseRef-Apheris-Foundry-Software-License
@@ -39,37 +39,37 @@ Recommended if you already use conda for scientific Python work. A dedicated env
39
39
  #### Linux / macOS
40
40
 
41
41
  ```bash
42
- conda create -n apherisfold python=3.11 -y
43
- conda activate apherisfold
44
- pip install apherisfold-cli
42
+ conda create -n apheris-foundry python=3.11 -y
43
+ conda activate apheris-foundry
44
+ pip install apheris-foundry-cli
45
45
  ```
46
46
 
47
47
  #### Windows (Anaconda Prompt or PowerShell)
48
48
 
49
49
  ```powershell
50
- conda create -n apherisfold python=3.11 -y
51
- conda activate apherisfold
52
- pip install apherisfold-cli
50
+ conda create -n apheris-foundry python=3.11 -y
51
+ conda activate apheris-foundry
52
+ pip install apheris-foundry-cli
53
53
  ```
54
54
 
55
- Activate the environment at the start of each session with `conda activate apherisfold`.
55
+ Activate the environment at the start of each session with `conda activate apheris-foundry`.
56
56
 
57
57
  ### venv (standard Python)
58
58
 
59
59
  #### Linux / macOS (venv)
60
60
 
61
61
  ```bash
62
- python3 -m venv ~/.venvs/apherisfold
63
- source ~/.venvs/apherisfold/bin/activate
64
- pip install apherisfold-cli
62
+ python3 -m venv ~/.venvs/apheris-foundry
63
+ source ~/.venvs/apheris-foundry/bin/activate
64
+ pip install apheris-foundry-cli
65
65
  ```
66
66
 
67
67
  #### Windows (PowerShell, venv)
68
68
 
69
69
  ```powershell
70
- python -m venv $env:USERPROFILE\.venvs\apherisfold
71
- & $env:USERPROFILE\.venvs\apherisfold\Scripts\Activate.ps1
72
- pip install apherisfold-cli
70
+ python -m venv $env:USERPROFILE\.venvs\apheris-foundry
71
+ & $env:USERPROFILE\.venvs\apheris-foundry\Scripts\Activate.ps1
72
+ pip install apheris-foundry-cli
73
73
  ```
74
74
 
75
75
  Activate the environment at the start of each session with the `activate` command above.
@@ -84,7 +84,7 @@ apheris-foundry --help
84
84
  ### Upgrade
85
85
 
86
86
  ```bash
87
- pip install --upgrade apherisfold-cli
87
+ pip install --upgrade apheris-foundry-cli
88
88
  ```
89
89
 
90
90
  ## Quick start
@@ -99,6 +99,22 @@ apheris-foundry workflows list
99
99
  # Submit a run
100
100
  apheris-foundry workflows run --workflow predict --input ./request.json
101
101
 
102
+ # Inspect the model IDs, weights, and default parameters advertised by Hub
103
+ apheris-foundry workflows models --workflow predict
104
+
105
+ # Override prediction parameters with a Hub-schema-validated JSON object
106
+ apheris-foundry workflows run --workflow predict --input ./request.json \
107
+ --model-params '{"seeds":[7,9],"num_diffusion_samples":3}'
108
+
109
+ # Or provide the same JSON object from a file
110
+ apheris-foundry workflows run --workflow predict --input ./request.json \
111
+ --model-params @./model-params.json
112
+
113
+ # Select a non-default model/weight advertised by the workflow
114
+ apheris-foundry workflows run --workflow predict --input ./request.json \
115
+ --param model=<model-id> --weight <weight-version> \
116
+ --model-params @./model-params.json
117
+
102
118
  # List submitted runs
103
119
  apheris-foundry jobs list
104
120
 
@@ -132,9 +148,10 @@ apheris-foundry jobs error --id <job-id>
132
148
  | Command | Description |
133
149
  | ------- | ----------- |
134
150
  | `apheris-foundry workflows list` | List workflow definitions exposed by Hub |
135
- | `apheris-foundry workflows get --workflow <id>` | Inspect one workflow definition |
151
+ | `apheris-foundry workflows get --workflow <id>` | Inspect one workflow definition and advertised model defaults |
152
+ | `apheris-foundry workflows models --workflow <id>` | List models, weights, and default prediction parameters advertised by Hub (`--json` includes full metadata) |
136
153
  | `apheris-foundry workflows graph --workflow <id>` | Show the workflow DAG |
137
- | `apheris-foundry workflows run --workflow <id>` | Submit a workflow run |
154
+ | `apheris-foundry workflows run --workflow <id> [--model-params <JSON\|@file>]` | Submit a workflow run with Hub-schema-validated model parameters |
138
155
 
139
156
  ### Run instances (`runs`)
140
157
 
@@ -153,6 +170,8 @@ apheris-foundry workflows run --help
153
170
  apheris-foundry jobs get --help
154
171
  ```
155
172
 
173
+ `--model-params` accepts one JSON object. Use an inline object for short overrides, or prefix a JSON filename with `@` to load the object from that file. Before submitting the run, the CLI retrieves the selected model version, weight, and capability schema from Hub and validates the object. Use `workflows models` to discover available model IDs and weights.
174
+
156
175
  ## License
157
176
 
158
177
  This package is licensed under the [Apheris Foundry Software License](https://www.apheris.com/docs/hub/hub-license.html). The full license text is also included in every published wheel as `LICENSE`.
@@ -783,10 +783,14 @@ def foundry_models(workflow: str, *, json_output: bool) -> None:
783
783
  table.add_column("VERSION", overflow="fold")
784
784
  table.add_column("WEIGHT", no_wrap=True)
785
785
  table.add_column("CAPABILITIES", overflow="fold")
786
+ table.add_column("DEFAULT PARAMETERS", overflow="fold")
786
787
  table.add_column("DESCRIPTION", overflow="fold")
787
788
  for model in models:
788
789
  if not isinstance(model, dict):
789
790
  continue
791
+ wrapper = model.get("wrapper") if isinstance(model.get("wrapper"), dict) else {}
792
+ defaults = wrapper.get("predictParamsDefaults") if isinstance(wrapper, dict) else {}
793
+ defaults_text = json.dumps(defaults, sort_keys=True) if isinstance(defaults, dict) and defaults else ""
790
794
  weights = model.get("weights") if isinstance(model.get("weights"), list) else []
791
795
  if not weights:
792
796
  table.add_row(
@@ -794,6 +798,7 @@ def foundry_models(workflow: str, *, json_output: bool) -> None:
794
798
  str(model.get("version", "")),
795
799
  "",
796
800
  ", ".join(str(item) for item in model.get("capabilities", []) if item),
801
+ defaults_text,
797
802
  "",
798
803
  )
799
804
  continue
@@ -810,14 +815,20 @@ def foundry_models(workflow: str, *, json_output: bool) -> None:
810
815
  str(model.get("version", "")),
811
816
  f"{weight.get('version', '')}{marker}",
812
817
  ", ".join(str(item) for item in capabilities if item),
818
+ defaults_text,
813
819
  str(weight.get("description", "")),
814
820
  )
815
821
 
816
822
  examples = Table(box=box.SIMPLE_HEAD, show_header=False, padding=(0, 1), expand=False)
817
823
  examples.add_column("COMMAND", style="cyan", overflow="fold")
824
+ examples.add_row(f"apheris-foundry workflows run --workflow {workflow} --input request.json")
818
825
  examples.add_row(
819
- f"apheris-foundry workflows run --workflow {workflow} --param model=mock --param weight=0.0.0 --input request.json"
826
+ f"apheris-foundry workflows run --workflow {workflow} --weight <weight-version> --input request.json"
820
827
  )
828
+ examples.add_row(
829
+ f'apheris-foundry workflows run --workflow {workflow} --model-params \'{{"seeds":[42],"num_diffusion_samples":1}}\''
830
+ )
831
+ examples.add_row(f"apheris-foundry workflows run --workflow {workflow} --model-params @model-params.json")
821
832
  examples.add_row(f"apheris-foundry workflows get --workflow {workflow}")
822
833
  run_simple_listing(
823
834
  task_name="foundry.models",
@@ -43,15 +43,15 @@ def _is_verbose() -> bool:
43
43
  def _build_examples_table() -> Table:
44
44
  table = Table(box=box.SIMPLE_HEAD, show_header=False, padding=(0, 1))
45
45
  table.add_column("command")
46
- table.add_row(Text("apherisfold login --url https://hub.example.com", style="cyan"))
47
- table.add_row(Text("apherisfold login --url https://hub.example.com --callback-host localhost", style="cyan"))
46
+ table.add_row(Text("apheris-foundry login --url https://hub.example.com", style="cyan"))
47
+ table.add_row(Text("apheris-foundry login --url https://hub.example.com --callback-host localhost", style="cyan"))
48
48
  table.add_row(
49
49
  Text(
50
- "apherisfold login --url https://hub.example.com --issuer https://dex.example.com --client-id apheris-hub",
50
+ "apheris-foundry login --url https://hub.example.com --issuer https://dex.example.com --client-id apheris-hub",
51
51
  style="cyan",
52
52
  )
53
53
  )
54
- table.add_row(Text("apherisfold login status", style="cyan"))
54
+ table.add_row(Text("apheris-foundry login status", style="cyan"))
55
55
  return table
56
56
 
57
57
 
@@ -70,7 +70,7 @@ def _render_login_overview() -> None:
70
70
 
71
71
  console.print(
72
72
  Group(
73
- Text("Usage: apherisfold login --url HUB_URL [--callback-host HOST]", style="bold"),
73
+ Text("Usage: apheris-foundry login --url HUB_URL [--callback-host HOST]", style="bold"),
74
74
  Text(""),
75
75
  Panel(options_table, title="Login Options", border_style="dim"),
76
76
  Panel(_build_examples_table(), title="Examples", border_style="dim"),
@@ -242,10 +242,10 @@ def token_exchange_url(config: RuntimeAuthConfig) -> str:
242
242
 
243
243
 
244
244
  def config_directory() -> Path:
245
- override = os.getenv("APHERISFOLD_CONFIG_DIR")
245
+ override = os.getenv("APHERIS_FOUNDRY_CONFIG_DIR") or os.getenv("APHERISFOLD_CONFIG_DIR")
246
246
  if override:
247
247
  return Path(override)
248
- return Path.home() / ".config" / "apherisfold"
248
+ return Path.home() / ".config" / "apheris-foundry"
249
249
 
250
250
 
251
251
  def config_path() -> Path:
@@ -756,23 +756,23 @@ def _login_status_error(
756
756
  if session is not None and not bool(expired):
757
757
  return None, None
758
758
  if config is None:
759
- return "No Hub runtime configuration found.", "Run 'apherisfold login' to configure a Hub."
759
+ return "No Hub runtime configuration found.", "Run 'apheris-foundry login' to configure a Hub."
760
760
  if not config.auth_enabled:
761
761
  return None, None
762
762
  if active_session is None:
763
- return "No saved login session found.", "Run 'apherisfold login' to create a session."
763
+ return "No saved login session found.", "Run 'apheris-foundry login' to create a session."
764
764
  if not _session_matches_config(config, active_session):
765
765
  return (
766
766
  "Saved login session belongs to a different Hub or client.",
767
- f"Configured Hub is {config.hub_url}; saved session Hub is {active_session.hub_url}. Run 'apherisfold login' for the configured Hub.",
767
+ f"Configured Hub is {config.hub_url}; saved session Hub is {active_session.hub_url}. Run 'apheris-foundry login' for the configured Hub.",
768
768
  )
769
769
  if refresh_error:
770
770
  return "Token refresh failed.", refresh_error
771
771
  if expired and not active_session.refresh_token:
772
- return "Saved login session is expired and has no refresh token.", "Run 'apherisfold login' again."
772
+ return "Saved login session is expired and has no refresh token.", "Run 'apheris-foundry login' again."
773
773
  if expired:
774
- return "Saved login session is expired.", "Run 'apherisfold login' again."
775
- return "No usable authenticated session found.", "Run 'apherisfold login' again."
774
+ return "Saved login session is expired.", "Run 'apheris-foundry login' again."
775
+ return "No usable authenticated session found.", "Run 'apheris-foundry login' again."
776
776
 
777
777
 
778
778
  def format_expires_at(expires_at: int | None) -> str | None:
@@ -925,10 +925,12 @@ def _browser_open(url: str) -> bool:
925
925
 
926
926
 
927
927
  def _browser_auto_open_enabled() -> bool:
928
- open_raw = os.getenv("APHERISFOLD_OPEN_BROWSER", "").strip().lower()
928
+ open_raw = (os.getenv("APHERIS_FOUNDRY_OPEN_BROWSER") or os.getenv("APHERISFOLD_OPEN_BROWSER", "")).strip().lower()
929
929
  if open_raw in {"1", "true", "yes", "on"}:
930
930
  return True
931
- no_browser_raw = os.getenv("APHERISFOLD_NO_BROWSER", "").strip().lower()
931
+ no_browser_raw = (
932
+ (os.getenv("APHERIS_FOUNDRY_NO_BROWSER") or os.getenv("APHERISFOLD_NO_BROWSER", "")).strip().lower()
933
+ )
932
934
  if no_browser_raw in {"1", "true", "yes", "on"}:
933
935
  return False
934
936
  return False
@@ -977,7 +979,7 @@ class _CallbackServer:
977
979
  <head>
978
980
  <meta charset="utf-8">
979
981
  <meta name="viewport" content="width=device-width, initial-scale=1">
980
- <title>ApherisFold Hub Login</title>
982
+ <title>Apheris Foundry Hub Login</title>
981
983
  <style>
982
984
  :root {
983
985
  color-scheme: light;
@@ -1042,7 +1044,7 @@ class _CallbackServer:
1042
1044
  </head>
1043
1045
  <body>
1044
1046
  <main class="shell">
1045
- <div class="brand" aria-label="ApherisFold Hub">
1047
+ <div class="brand" aria-label="Apheris Foundry Hub">
1046
1048
  <svg width="263" height="43" viewBox="0 0 263 43" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
1047
1049
  <path opacity="0.5" d="M172.148 20.701H184.391V23.671H172.148V34H168.518V10.438H184.919V13.474H172.148V20.701ZM198.859 9.91C201.081 9.91 203.028 10.416 204.7 11.428C206.394 12.418 207.703 13.848 208.627 15.718C209.551 17.566 210.013 19.733 210.013 22.219C210.013 24.705 209.551 26.883 208.627 28.753C207.703 30.601 206.394 32.031 204.7 33.043C203.028 34.033 201.081 34.528 198.859 34.528C196.637 34.528 194.679 34.033 192.985 33.043C191.313 32.031 190.015 30.601 189.091 28.753C188.167 26.883 187.705 24.705 187.705 22.219C187.705 19.733 188.167 17.566 189.091 15.718C190.015 13.848 191.313 12.418 192.985 11.428C194.679 10.416 196.637 9.91 198.859 9.91ZM198.859 31.492C201.147 31.492 202.962 30.678 204.304 29.05C205.646 27.4 206.317 25.123 206.317 22.219C206.317 19.315 205.646 17.049 204.304 15.421C202.962 13.771 201.147 12.946 198.859 12.946C196.571 12.946 194.756 13.771 193.414 15.421C192.072 17.049 191.401 19.315 191.401 22.219C191.401 25.123 192.072 27.4 193.414 29.05C194.756 30.678 196.571 31.492 198.859 31.492ZM213.585 10.438H217.215V30.865H230.085V34H213.585V10.438ZM242.113 10.438C244.379 10.438 246.348 10.889 248.02 11.791C249.714 12.671 251.023 13.991 251.947 15.751C252.871 17.511 253.333 19.667 253.333 22.219C253.333 24.771 252.871 26.927 251.947 28.687C251.023 30.447 249.714 31.778 248.02 32.68C246.348 33.56 244.379 34 242.113 34H233.17V10.438H242.113ZM241.651 30.964C244.203 30.964 246.172 30.216 247.558 28.72C248.944 27.224 249.637 25.057 249.637 22.219C249.637 19.381 248.944 17.214 247.558 15.718C246.172 14.222 244.203 13.474 241.651 13.474H236.8V30.964H241.651Z" fill="#21484D"/>
1048
1050
  <path d="M16.4623 27.0782H6.04042L3.6931 33.5385H0L8.96258 10.4609H13.6538L22.5785 33.5385H18.8097L16.4623 27.0782ZM7.00071 24.3866H15.4608L14.4213 21.5402C13.4611 18.9623 12.4595 16.0023 11.7298 13.5793H10.7696C10.002 16.0402 9.00043 18.9657 8.04015 21.5402L7.00071 24.3866Z" fill="#21484D"/>
@@ -1059,7 +1061,7 @@ class _CallbackServer:
1059
1061
  <div class="brand-subtitle">Hub</div>
1060
1062
  </div>
1061
1063
  <div class="status">Sign-in completed</div>
1062
- <p>Return to your terminal to continue using the ApherisFold CLI.</p>
1064
+ <p>Return to your terminal to continue using the Apheris Foundry CLI.</p>
1063
1065
  </main>
1064
1066
  </body>
1065
1067
  </html>"""
@@ -28,7 +28,7 @@ def _request_headers(
28
28
  raise CLIError(
29
29
  code=EXIT_PRECONDITION,
30
30
  category="precondition",
31
- message="No Hub runtime configuration found. Run 'apherisfold login' first.",
31
+ message="No Hub runtime configuration found. Run 'apheris-foundry login' first.",
32
32
  task="hub",
33
33
  )
34
34
 
@@ -42,7 +42,7 @@ def _request_headers(
42
42
  raise CLIError(
43
43
  code=EXIT_PRECONDITION,
44
44
  category="precondition",
45
- message="No authenticated Hub session found. Run 'apherisfold login' first.",
45
+ message="No authenticated Hub session found. Run 'apheris-foundry login' first.",
46
46
  task="hub",
47
47
  )
48
48
  headers["Authorization"] = f"Bearer {session.access_token}"
@@ -110,7 +110,7 @@ def _hub_url(path: str) -> str:
110
110
  raise CLIError(
111
111
  code=EXIT_PRECONDITION,
112
112
  category="precondition",
113
- message="No Hub runtime configuration found. Run 'apherisfold login' first.",
113
+ message="No Hub runtime configuration found. Run 'apheris-foundry login' first.",
114
114
  task="hub",
115
115
  )
116
116
  return f"{config.hub_url.rstrip('/')}{path}"
@@ -208,6 +208,18 @@ def get_foundry_workflow(workflow_id: str) -> dict[str, Any]:
208
208
  return _request_json(path=f"/api/v1/foundry/workflows/{encoded}", method="GET", task="foundry.workflows.get")
209
209
 
210
210
 
211
+ def get_model_schema(*, model_version_id: str, weight_version: str, capability: str) -> dict[str, Any]:
212
+ """Return the Hub OpenAPI schema for one model weight and capability."""
213
+ version_key = urllib.parse.quote(model_version_id, safe="")
214
+ weight_key = urllib.parse.quote(weight_version, safe="")
215
+ capability_key = urllib.parse.quote(capability, safe="")
216
+ return _request_json(
217
+ path=f"/api/v1/schemas/{version_key}/{weight_key}?capability={capability_key}",
218
+ method="GET",
219
+ task="foundry.model-schema",
220
+ )
221
+
222
+
211
223
  def list_foundry_runs() -> dict[str, Any]:
212
224
  return _request_json(path="/api/v1/foundry/runs", method="GET", task="foundry.runs.list")
213
225
 
@@ -1,9 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  from pathlib import Path
4
- from typing import Annotated
5
+ from typing import Annotated, Any
5
6
 
6
7
  import typer
8
+ from jsonschema import ValidationError, validate
7
9
  from rich import box
8
10
  from rich.console import Console, Group
9
11
  from rich.panel import Panel
@@ -18,6 +20,7 @@ from apherisfold_cli.commands.foundry import (
18
20
  foundry_get,
19
21
  foundry_list,
20
22
  foundry_logs,
23
+ foundry_models,
21
24
  foundry_run,
22
25
  foundry_workflow_graph,
23
26
  )
@@ -245,18 +248,36 @@ def workflows_get_cmd(
245
248
  info.add_row("Output", str(payload.get("output") or ""))
246
249
  info.add_row("Profiles", ", ".join(str(item) for item in payload.get("profiles", []) if item))
247
250
 
251
+ models_value = payload.get("models")
252
+ models = models_value if isinstance(models_value, list) else []
253
+ for model in models:
254
+ if not isinstance(model, dict):
255
+ continue
256
+ defaults = _predict_params_defaults(model)
257
+ if defaults:
258
+ info.add_row(f"{model.get('id', 'model')} defaults", json.dumps(defaults, sort_keys=True))
259
+
248
260
  examples = Table(box=box.SIMPLE_HEAD, show_header=False, padding=(0, 1), expand=False)
249
261
  examples.add_column("COMMAND", style="cyan", overflow="fold")
250
262
  examples.add_row(f"apheris-foundry workflows run --workflow {workflow_id} --input input.csv")
263
+ if models:
264
+ examples.add_row(f"apheris-foundry workflows run --workflow {workflow_id} --model-params '{{\"seeds\":[42]}}'")
265
+ examples.add_row(f"apheris-foundry workflows run --workflow {workflow_id} --model-params @model-params.json")
251
266
  examples.add_row(f"apheris-foundry workflows graph --workflow {workflow_id}")
252
267
 
268
+ parameter_help = "Workflow-specific values can be passed with repeated --param key=value options."
269
+ if models:
270
+ parameter_help = (
271
+ "Use --param key=value for workflow values and --model-params JSON (or @file.json) for model values."
272
+ )
273
+
253
274
  run_simple_listing(
254
275
  task_name="foundry.workflows.get",
255
276
  payload=payload,
256
277
  renderable_output=Group(
257
278
  Panel(info, title="Foundry Workflow", border_style="cyan"),
258
279
  Panel(
259
- Text("Workflow-specific values can be passed with repeated --param key=value options."),
280
+ Text(parameter_help),
260
281
  title="Parameters",
261
282
  border_style="dim",
262
283
  expand=False,
@@ -266,6 +287,14 @@ def workflows_get_cmd(
266
287
  )
267
288
 
268
289
 
290
+ @workflows_app.command("models", help="List models and prediction parameters for a Foundry workflow.")
291
+ def workflows_models_cmd(
292
+ workflow_id: Annotated[str, typer.Option("--workflow", help="Workflow ID.")],
293
+ json_output: Annotated[bool, typer.Option("--json", help="Render as JSON.")] = False,
294
+ ) -> None:
295
+ foundry_models(workflow_id, json_output=json_output)
296
+
297
+
269
298
  @workflows_app.command("graph", help="Show a Foundry workflow graph served by Hub.")
270
299
  def workflows_graph_cmd(
271
300
  workflow_id: Annotated[str, typer.Option("--workflow", help="Workflow ID.")],
@@ -297,6 +326,17 @@ def workflows_run_cmd(
297
326
  list[str] | None,
298
327
  typer.Option("--param", help="Nextflow parameter as key=value. Repeat for multiple parameters."),
299
328
  ] = None,
329
+ weight: Annotated[
330
+ str | None,
331
+ typer.Option("--weight", help="Selected model weight version."),
332
+ ] = None,
333
+ model_params_raw: Annotated[
334
+ str | None,
335
+ typer.Option(
336
+ "--model-params",
337
+ help="Model parameters as an inline JSON object or @path/to/model-params.json.",
338
+ ),
339
+ ] = None,
300
340
  tag: Annotated[
301
341
  list[str] | None,
302
342
  typer.Option("--tag", help="Tag to attach to the run. Repeat for multiple tags."),
@@ -305,7 +345,22 @@ def workflows_run_cmd(
305
345
  wait: Annotated[bool, typer.Option("--wait", help="Block until the run reaches a terminal status.")] = False,
306
346
  json_output: Annotated[bool, typer.Option("--json", help="Render the run response as JSON.")] = False,
307
347
  ) -> None:
308
- params = _foundry_params_from_options(param)
348
+ try:
349
+ params = _foundry_params_from_options(param)
350
+ if weight:
351
+ params = {**(params or {}), "weight": weight}
352
+ model_params = _load_model_params(model_params_raw)
353
+ if model_params is not None:
354
+ params = _validate_foundry_model_params(workflow_id, params, model_params)
355
+ params = {**(params or {}), "modelParams": json.dumps(model_params, separators=(",", ":"))}
356
+ elif weight:
357
+ # A weight identifies a model version only in the context of a
358
+ # model. For a one-model workflow we can make that selection
359
+ # explicit without burdening the user with --param model=... .
360
+ _, params = _resolve_foundry_model_selection(workflow_id, params)
361
+ except typer.BadParameter as error:
362
+ typer.echo(str(error), err=True)
363
+ raise typer.Exit(code=2) from error
309
364
  inputs = _foundry_input_files(workflow_id, input_path)
310
365
  foundry_run(
311
366
  workflow_id,
@@ -327,10 +382,116 @@ def _foundry_params_from_options(param: list[str] | None) -> dict[str, str] | No
327
382
  if "=" not in kv:
328
383
  raise typer.BadParameter(f"--param must be key=value, got: {kv!r}")
329
384
  key, value = kv.split("=", 1)
385
+ if key == "modelParams":
386
+ raise typer.BadParameter("use --model-params JSON (or @file.json) for model parameters")
387
+ if key == "weight":
388
+ raise typer.BadParameter("use --weight for the selected model weight version")
330
389
  params[key] = value
331
390
  return params
332
391
 
333
392
 
393
+ def _load_model_params(raw_value: str | None) -> dict[str, Any] | None:
394
+ if raw_value is None:
395
+ return None
396
+
397
+ source = raw_value
398
+ if raw_value.startswith("@"):
399
+ path = Path(raw_value[1:])
400
+ if not raw_value[1:] or not path.is_file():
401
+ raise typer.BadParameter(f"--model-params file does not exist or is not a file: {path}")
402
+ try:
403
+ source = path.read_text(encoding="utf-8")
404
+ except OSError as error:
405
+ raise typer.BadParameter(f"could not read --model-params file: {path}") from error
406
+ try:
407
+ value = json.loads(source)
408
+ except json.JSONDecodeError as error:
409
+ raise typer.BadParameter("--model-params must be a valid JSON object or @path/to/file.json") from error
410
+ if not isinstance(value, dict):
411
+ raise typer.BadParameter("--model-params must be a JSON object")
412
+ return value
413
+
414
+
415
+ def _predict_params_defaults(model: dict[str, Any]) -> dict[str, Any]:
416
+ wrapper = model.get("wrapper")
417
+ if not isinstance(wrapper, dict):
418
+ return {}
419
+ defaults = wrapper.get("predictParamsDefaults")
420
+ return defaults if isinstance(defaults, dict) else {}
421
+
422
+
423
+ def _validate_foundry_model_params(
424
+ workflow_id: str,
425
+ params: dict[str, str] | None,
426
+ model_params: dict[str, Any],
427
+ ) -> dict[str, str]:
428
+ selected_model, resolved_params = _resolve_foundry_model_selection(workflow_id, params)
429
+ schema = _foundry_model_params_schema(selected_model)
430
+ try:
431
+ validate(instance=model_params, schema=schema)
432
+ except ValidationError as error:
433
+ raise typer.BadParameter(f"invalid --model-params: {error.message}") from error
434
+ return resolved_params
435
+
436
+
437
+ def _resolve_foundry_model_selection(
438
+ workflow_id: str, params: dict[str, str] | None
439
+ ) -> tuple[dict[str, Any], dict[str, str]]:
440
+ payload = get_foundry_workflow(workflow_id)
441
+ models_value = payload.get("models")
442
+ models = models_value if isinstance(models_value, list) else []
443
+ resolved_params = dict(params or {})
444
+ selected_model_id = resolved_params.get("model")
445
+ selected_model = next(
446
+ (model for model in models if isinstance(model, dict) and model.get("id") == selected_model_id),
447
+ None,
448
+ )
449
+ if selected_model is None and len(models) == 1 and isinstance(models[0], dict):
450
+ selected_model = models[0]
451
+ model_id = selected_model.get("id")
452
+ if isinstance(model_id, str) and model_id:
453
+ resolved_params["model"] = model_id
454
+ if selected_model is None:
455
+ raise typer.BadParameter("this option requires a workflow with one selected model; pass --param model=<id>")
456
+
457
+ weights = selected_model.get("weights") if isinstance(selected_model.get("weights"), list) else []
458
+ requested_weight = resolved_params.get("weight")
459
+ selected_weight = (
460
+ next(
461
+ (item for item in weights if isinstance(item, dict) and item.get("version") == requested_weight),
462
+ None,
463
+ )
464
+ if requested_weight
465
+ else next((item for item in weights if isinstance(item, dict) and item.get("default")), None)
466
+ )
467
+ if selected_weight is None and not requested_weight:
468
+ selected_weight = next((item for item in weights if isinstance(item, dict)), None)
469
+ if not isinstance(selected_weight, dict) or not isinstance(selected_weight.get("version"), str):
470
+ raise typer.BadParameter("the selected model does not advertise the requested weight")
471
+ if not requested_weight:
472
+ resolved_params["weight"] = selected_weight["version"]
473
+ return selected_model, resolved_params
474
+
475
+
476
+ def _foundry_model_params_schema(model: dict[str, Any]) -> dict[str, Any]:
477
+ defaults = _predict_params_defaults(model)
478
+ schemas = model.get("schemas")
479
+ tasks = schemas.get("tasks") if isinstance(schemas, dict) else None
480
+ task = (
481
+ next((item for item in tasks if isinstance(item, dict) and item.get("name") == "predict-openfold3"), None)
482
+ if isinstance(tasks, list)
483
+ else None
484
+ )
485
+ parameters = task.get("parameters") if isinstance(task, dict) else None
486
+ properties = parameters.get("properties") if isinstance(parameters, dict) else None
487
+ if not defaults or not isinstance(properties, dict):
488
+ raise typer.BadParameter("Hub did not provide model parameter schema for the selected model")
489
+ model_properties = {name: properties[name] for name in defaults if isinstance(properties.get(name), dict)}
490
+ if set(model_properties) != set(defaults):
491
+ raise typer.BadParameter("Hub model parameter defaults do not match its advertised schema")
492
+ return {"type": "object", "additionalProperties": False, "properties": model_properties}
493
+
494
+
334
495
  def _generic_input_files(input_paths: list[Path] | None) -> dict[str, str] | None:
335
496
  if not input_paths:
336
497
  return None
@@ -345,20 +506,10 @@ def _generic_input_files(input_paths: list[Path] | None) -> dict[str, str] | Non
345
506
 
346
507
 
347
508
  def _foundry_input_files(workflow: str, input_paths: list[Path] | None) -> dict[str, str] | None:
348
- inputs = _generic_input_files(input_paths)
349
- if workflow != "predict" or not inputs:
350
- return inputs
351
-
352
- json_names = [name for name in inputs if name.lower().endswith(".json")]
353
- if not json_names:
354
- return inputs
355
-
356
- request_name = "queries.json" if "queries.json" in inputs else json_names[0]
357
- request_content = inputs[request_name]
358
- if request_name != "queries.json":
359
- del inputs[request_name]
360
- inputs["queries.json"] = request_content
361
- return inputs
509
+ del workflow
510
+ # The Foundry MSA task accepts one request JSON by content, not filename.
511
+ # Preserve the user-provided name so staging never creates a second JSON.
512
+ return _generic_input_files(input_paths)
362
513
 
363
514
 
364
515
  def main() -> int:
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
@@ -15,6 +15,7 @@ from apherisfold_cli.core.auth import (
15
15
  build_authorization_parameters,
16
16
  build_token_exchange_form,
17
17
  clear_session,
18
+ config_directory,
18
19
  fetch_discovery_document,
19
20
  fetch_runtime_auth_config,
20
21
  format_expires_at,
@@ -39,6 +40,13 @@ def isolate_auth_config_dir(tmp_path: Path, monkeypatch) -> None:
39
40
  monkeypatch.setenv("APHERISFOLD_CONFIG_DIR", str(tmp_path / "auth-config"))
40
41
 
41
42
 
43
+ def test_config_directory_prefers_foundry_environment_variable(tmp_path: Path, monkeypatch) -> None:
44
+ expected = tmp_path / "foundry-config"
45
+ monkeypatch.setenv("APHERIS_FOUNDRY_CONFIG_DIR", str(expected))
46
+
47
+ assert config_directory() == expected
48
+
49
+
42
50
  def test_fetch_runtime_auth_config_parses_forgerock_payload(monkeypatch) -> None:
43
51
  payload = {
44
52
  "AUTH0_ENABLED": True,
@@ -244,6 +244,14 @@ def hub_api_fixture(monkeypatch):
244
244
  ],
245
245
  },
246
246
  },
247
+ {
248
+ "id": "predict",
249
+ "description": "Predict structures",
250
+ "title": "Predict",
251
+ "input": "Request JSON",
252
+ "output": "Structure predictions",
253
+ "profiles": ["local", "k8s"],
254
+ },
247
255
  {
248
256
  "id": "fragment-analysis",
249
257
  "description": "Fragment analysis",
@@ -275,20 +283,55 @@ def hub_api_fixture(monkeypatch):
275
283
  for workflow in workflows:
276
284
  if workflow["id"] != workflow_id:
277
285
  continue
278
- if workflow_id == "legacy_predict":
286
+ if workflow_id in {"legacy_predict", "predict"}:
279
287
  return {
280
288
  **workflow,
281
289
  "models": [
282
290
  {
283
- "id": "mock",
284
- "version": "mock:0.0.0",
291
+ "id": "openfold3",
292
+ "version": "openfold3:0.0.0",
293
+ "wrapper": {
294
+ "predictParamsDefaults": {
295
+ "num_diffusion_samples": 1,
296
+ "seeds": [42],
297
+ }
298
+ },
299
+ "schemas": {
300
+ "tasks": [
301
+ {
302
+ "name": "predict-openfold3",
303
+ "parameters": {
304
+ "type": "object",
305
+ "additionalProperties": False,
306
+ "required": ["weights"],
307
+ "properties": {
308
+ "weights": {"type": "string"},
309
+ "num_diffusion_samples": {
310
+ "type": "integer",
311
+ "minimum": 1,
312
+ },
313
+ "seeds": {
314
+ "type": "array",
315
+ "items": {"type": "integer", "minimum": 0},
316
+ "minItems": 1,
317
+ },
318
+ },
319
+ },
320
+ }
321
+ ]
322
+ },
285
323
  "weights": [
286
324
  {
287
325
  "version": "0.0.0",
288
326
  "description": "Mock model weights",
289
327
  "capabilities": ["inference"],
290
328
  "default": True,
291
- }
329
+ },
330
+ {
331
+ "version": "0.3.0",
332
+ "description": "Alternative model weights",
333
+ "capabilities": ["inference"],
334
+ },
292
335
  ],
293
336
  }
294
337
  ],
@@ -359,9 +402,27 @@ def test_foundry_workflows_get_shows_workflow_contract(hub_api_fixture) -> None:
359
402
  assert "Molecule CSV" in result.stdout
360
403
  assert "Fragment frequency" in result.stdout
361
404
  assert "--param key=value" in result.stdout
405
+ assert "--model-params" not in result.stdout
362
406
  assert hub_api_fixture["get_foundry_workflow"] == ["fragment-analysis"]
363
407
 
364
408
 
409
+ def test_foundry_workflows_get_shows_prediction_defaults(hub_api_fixture) -> None:
410
+ result = runner.invoke(app, ["workflows", "get", "--workflow", "predict"])
411
+
412
+ assert result.exit_code == 0
413
+ assert '"num_diffusion_samples": 1' in result.stdout
414
+ assert "--model-params" in result.stdout
415
+ assert "@model-params.json" in result.stdout
416
+ assert '"seeds": [42]' in result.stdout
417
+
418
+
419
+ def test_foundry_workflows_models_shows_prediction_defaults(hub_api_fixture) -> None:
420
+ result = runner.invoke(app, ["workflows", "models", "--workflow", "predict"], env={"COLUMNS": "180"})
421
+
422
+ assert result.exit_code == 0
423
+ assert '"num_diffusion_samples": 1' in result.stdout
424
+
425
+
365
426
  def test_foundry_workflows_graph_supports_mounted_workflows(hub_api_fixture) -> None:
366
427
  result = runner.invoke(app, ["workflows", "graph", "--workflow", "fragment-analysis"])
367
428
 
@@ -403,6 +464,101 @@ def test_foundry_workflows_run_stages_generic_input_and_params(hub_api_fixture)
403
464
  assert body["inputs"]["molecules.csv"] == "smiles,name\nCCO,ethanol\n"
404
465
 
405
466
 
467
+ def test_foundry_workflows_run_serializes_inline_validated_model_params(hub_api_fixture) -> None:
468
+ result = runner.invoke(
469
+ app,
470
+ [
471
+ "workflows",
472
+ "run",
473
+ "--workflow",
474
+ "predict",
475
+ "--model-params",
476
+ '{"seeds":[7,9],"num_diffusion_samples":3}',
477
+ ],
478
+ )
479
+
480
+ assert result.exit_code == 0
481
+ body = hub_api_fixture["create_foundry_run"][0]
482
+ assert json.loads(body["params"]["modelParams"]) == {
483
+ "num_diffusion_samples": 3,
484
+ "seeds": [7, 9],
485
+ }
486
+ assert body["params"]["model"] == "openfold3"
487
+ assert body["params"]["weight"] == "0.0.0"
488
+
489
+
490
+ def test_foundry_workflows_run_loads_model_params_from_json_file(tmp_path: Path, hub_api_fixture) -> None:
491
+ params_file = tmp_path / "model-params.json"
492
+ params_file.write_text('{"seeds":[7],"num_diffusion_samples":2}', encoding="utf-8")
493
+
494
+ result = runner.invoke(
495
+ app,
496
+ ["workflows", "run", "--workflow", "predict", "--model-params", f"@{params_file}"],
497
+ )
498
+
499
+ assert result.exit_code == 0
500
+ assert json.loads(hub_api_fixture["create_foundry_run"][0]["params"]["modelParams"]) == {
501
+ "seeds": [7],
502
+ "num_diffusion_samples": 2,
503
+ }
504
+
505
+
506
+ def test_foundry_workflows_run_serializes_selected_weight(hub_api_fixture) -> None:
507
+ result = runner.invoke(
508
+ app,
509
+ ["workflows", "run", "--workflow", "predict", "--weight", "0.3.0"],
510
+ )
511
+
512
+ assert result.exit_code == 0
513
+ assert hub_api_fixture["create_foundry_run"][0]["params"] == {"model": "openfold3", "weight": "0.3.0"}
514
+
515
+
516
+ def test_foundry_workflows_run_preserves_predict_request_filename(tmp_path: Path, hub_api_fixture) -> None:
517
+ request = tmp_path / "request-protein-ligand.json"
518
+ request.write_text('{"queries": []}', encoding="utf-8")
519
+
520
+ result = runner.invoke(app, ["workflows", "run", "--workflow", "predict", "--input", str(request)])
521
+
522
+ assert result.exit_code == 0
523
+ assert hub_api_fixture["create_foundry_run"][0]["inputs"] == {"request-protein-ligand.json": '{"queries": []}'}
524
+
525
+
526
+ def test_foundry_workflows_run_rejects_model_params_outside_schema(hub_api_fixture) -> None:
527
+ result = runner.invoke(
528
+ app,
529
+ [
530
+ "workflows",
531
+ "run",
532
+ "--workflow",
533
+ "predict",
534
+ "--model-params",
535
+ '{"seed":7}',
536
+ ],
537
+ )
538
+
539
+ assert result.exit_code != 0
540
+ assert "invalid --model-params" in result.stderr
541
+ assert hub_api_fixture["create_foundry_run"] == []
542
+
543
+
544
+ def test_foundry_workflows_run_rejects_raw_model_params(hub_api_fixture) -> None:
545
+ result = runner.invoke(
546
+ app,
547
+ [
548
+ "workflows",
549
+ "run",
550
+ "--workflow",
551
+ "predict",
552
+ "--param",
553
+ 'modelParams={"seeds":[7]}',
554
+ ],
555
+ )
556
+
557
+ assert result.exit_code != 0
558
+ assert "use --model-params" in result.stderr
559
+ assert hub_api_fixture["create_foundry_run"] == []
560
+
561
+
406
562
  def test_foundry_list_shows_parallel_active_steps(monkeypatch) -> None:
407
563
  monkeypatch.setattr(
408
564
  "apherisfold_cli.commands.foundry.list_foundry_runs",
@@ -695,14 +851,14 @@ def test_login_requires_profile_or_url() -> None:
695
851
  result = runner.invoke(app, ["login"])
696
852
  try:
697
853
  assert result.exit_code == 2
698
- assert "Usage: apherisfold login --url HUB_URL [--callback-host HOST]" in result.stdout
854
+ assert "Usage: apheris-foundry login --url HUB_URL [--callback-host HOST]" in result.stdout
699
855
  assert "Login Options" in result.stdout
700
856
  assert "Built-In Profiles" not in result.stdout
701
857
  assert "--profile" not in result.stdout
702
858
  assert "apheris-integ" not in result.stdout
703
859
  assert "apheris-staging" not in result.stdout
704
860
  assert "Examples" in result.stdout
705
- assert "apherisfold login --url https://hub.example.com" in result.stdout
861
+ assert "apheris-foundry login --url https://hub.example.com" in result.stdout
706
862
  assert "--callback-host" in result.stdout
707
863
  assert "localhost" in result.stdout
708
864
  finally:
@@ -17,6 +17,7 @@ from apherisfold_cli.core.hub_requests import (
17
17
  get_foundry_run,
18
18
  get_foundry_run_logs,
19
19
  get_foundry_workflow,
20
+ get_model_schema,
20
21
  list_foundry_runs,
21
22
  list_foundry_workflows,
22
23
  )
@@ -189,6 +190,21 @@ def test_get_foundry_workflow_encodes_id(tmp_path: Path, monkeypatch) -> None:
189
190
  assert "fragment-analysis" in seen_url[0]
190
191
 
191
192
 
193
+ def test_get_model_schema_encodes_model_selection(tmp_path: Path, monkeypatch) -> None:
194
+ _save_refreshable_session(tmp_path, monkeypatch)
195
+ seen_url: list[str] = []
196
+
197
+ def fake_urlopen(request: urllib.request.Request, **_kwargs: object) -> FakeResponse:
198
+ seen_url.append(request.full_url)
199
+ return FakeResponse(b'{"paths": {}}')
200
+
201
+ monkeypatch.setattr("apherisfold_cli.core.hub_requests.urllib.request.urlopen", fake_urlopen)
202
+ assert get_model_schema(model_version_id="openfold3:0.0.0", weight_version="0.0.0", capability="inference") == {
203
+ "paths": {}
204
+ }
205
+ assert "/api/v1/schemas/openfold3%3A0.0.0/0.0.0?capability=inference" in seen_url[0]
206
+
207
+
192
208
  # ---------------------------------------------------------------------------
193
209
  # get_foundry_run_logs
194
210
  # ---------------------------------------------------------------------------
@@ -87,7 +87,7 @@ def test_main_writes_error_file_when_write_errors_set(monkeypatch, tmp_path) ->
87
87
  assert data["message"] == "Input error"
88
88
 
89
89
 
90
- def test_foundry_predict_input_uses_queries_json(tmp_path: Path) -> None:
90
+ def test_foundry_predict_input_preserves_request_filename(tmp_path: Path) -> None:
91
91
  request = '{"queries": [{"chains": [{"chain_id": "A", "polymer_type": "protein", "sequence": "ACDE"}]}]}'
92
92
  request_path = tmp_path / "request-minimal.json"
93
93
  request_path.write_text(request, encoding="utf-8")
@@ -95,8 +95,8 @@ def test_foundry_predict_input_uses_queries_json(tmp_path: Path) -> None:
95
95
  inputs = _foundry_input_files("predict", [request_path])
96
96
 
97
97
  assert inputs is not None
98
- assert list(inputs) == ["queries.json"]
99
- assert inputs["queries.json"] == request
98
+ assert list(inputs) == ["request-minimal.json"]
99
+ assert inputs["request-minimal.json"] == request
100
100
 
101
101
 
102
102
  def test_foundry_predict_new_schema_passes_through(tmp_path: Path) -> None:
@@ -109,7 +109,7 @@ def test_foundry_predict_new_schema_passes_through(tmp_path: Path) -> None:
109
109
  assert inputs == {"queries.json": request}
110
110
 
111
111
 
112
- def test_foundry_predict_preserves_msa_asset_and_rewrites_request_name(tmp_path: Path) -> None:
112
+ def test_foundry_predict_preserves_msa_asset_and_request_name(tmp_path: Path) -> None:
113
113
  request = tmp_path / "request.json"
114
114
  request.write_text(
115
115
  '{"queries": [{"chains": [{"chain_id": "A", "sequence": "ACDE", "msa_path": "protein.a3m"}]}]}',
@@ -121,7 +121,7 @@ def test_foundry_predict_preserves_msa_asset_and_rewrites_request_name(tmp_path:
121
121
  inputs = _foundry_input_files("predict", [request, msa])
122
122
 
123
123
  assert inputs == {
124
- "queries.json": request.read_text(encoding="utf-8"),
124
+ "request.json": request.read_text(encoding="utf-8"),
125
125
  "protein.a3m": "##4\nA\n",
126
126
  }
127
127
 
@@ -1 +0,0 @@
1
- __version__ = "0.2.0"