synth-ai 0.2.16__py3-none-any.whl → 0.2.17__py3-none-any.whl
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.
Potentially problematic release.
This version of synth-ai might be problematic. Click here for more details.
- examples/analyze_semantic_words.sh +2 -2
- examples/blog_posts/pokemon_vl/README.md +98 -0
- examples/blog_posts/pokemon_vl/configs/eval_qwen3_vl.toml +25 -0
- examples/blog_posts/pokemon_vl/configs/eval_rl_final.toml +24 -0
- examples/blog_posts/pokemon_vl/configs/filter_high_reward.toml +10 -0
- examples/blog_posts/pokemon_vl/configs/train_rl_from_sft.toml +42 -0
- examples/blog_posts/pokemon_vl/configs/train_sft_qwen4b_vl.toml +40 -0
- examples/blog_posts/warming_up_to_rl/README.md +158 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b.toml +25 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_groq_qwen32b.toml +25 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_openai_gpt_oss_120b.toml +29 -0
- examples/blog_posts/warming_up_to_rl/configs/filter_high_reward_dataset.toml +10 -0
- examples/blog_posts/warming_up_to_rl/configs/train_rl_from_sft.toml +41 -0
- examples/blog_posts/warming_up_to_rl/configs/train_sft_qwen4b.toml +40 -0
- examples/dev/qwen3_32b_qlora_4xh100.toml +5 -0
- examples/multi_step/configs/crafter_rl_outcome.toml +1 -1
- examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +65 -107
- examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +1 -1
- examples/multi_step/configs/crafter_rl_stepwise_simple.toml +1 -1
- examples/multi_step/configs/crafter_rl_stepwise_simple_NEW_FORMAT.toml +105 -0
- examples/multi_step/configs/verilog_rl_lora.toml +80 -123
- examples/qwen_coder/configs/coder_lora_30b.toml +1 -3
- examples/qwen_coder/configs/coder_lora_4b.toml +4 -1
- examples/qwen_coder/configs/coder_lora_small.toml +1 -3
- examples/qwen_vl/README.md +10 -12
- examples/qwen_vl/SETUP_COMPLETE.md +7 -8
- examples/qwen_vl/VISION_TESTS_COMPLETE.md +2 -3
- examples/qwen_vl/collect_data_via_cli.md +76 -84
- examples/qwen_vl/collect_vision_traces.py +4 -4
- examples/qwen_vl/configs/crafter_rl_vision_qwen3vl4b.toml +40 -57
- examples/qwen_vl/configs/crafter_vlm_sft_example.toml +1 -2
- examples/qwen_vl/configs/eval_gpt4o_mini_vision.toml +20 -37
- examples/qwen_vl/configs/eval_gpt5nano_vision.toml +21 -40
- examples/qwen_vl/configs/eval_qwen3vl_vision.toml +26 -0
- examples/qwen_vl/configs/{filter_qwen2vl_sft.toml → filter_qwen3vl_sft.toml} +4 -5
- examples/qwen_vl/configs/filter_vision_sft.toml +2 -3
- examples/qwen_vl/crafter_qwen_vl_agent.py +5 -5
- examples/qwen_vl/run_vision_comparison.sh +6 -7
- examples/rl/README.md +5 -5
- examples/rl/configs/rl_from_base_qwen.toml +26 -1
- examples/rl/configs/rl_from_base_qwen17.toml +5 -2
- examples/rl/task_app/README.md +1 -2
- examples/rl/task_app/math_single_step.py +2 -2
- examples/run_crafter_demo.sh +2 -2
- examples/sft/README.md +1 -1
- examples/sft/configs/crafter_fft_qwen0p6b.toml +4 -1
- examples/sft/configs/crafter_lora_qwen0p6b.toml +4 -1
- examples/swe/task_app/README.md +32 -2
- examples/swe/task_app/grpo_swe_mini.py +4 -0
- examples/swe/task_app/hosted/envs/crafter/react_agent.py +1 -1
- examples/swe/task_app/hosted/envs/mini_swe/environment.py +37 -10
- examples/swe/task_app/hosted/inference/openai_client.py +4 -4
- examples/swe/task_app/morph_backend.py +178 -0
- examples/task_apps/crafter/task_app/README.md +1 -1
- examples/task_apps/crafter/task_app/grpo_crafter.py +66 -3
- examples/task_apps/crafter/task_app/grpo_crafter_task_app.py +1 -1
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/policy.py +4 -26
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/react_agent.py +1 -2
- examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +17 -49
- examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +13 -5
- examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +15 -1
- examples/task_apps/enron/task_app/grpo_enron_task_app.py +1 -1
- examples/task_apps/math/README.md +1 -2
- examples/task_apps/pokemon_red/README.md +3 -4
- examples/task_apps/pokemon_red/eval_image_only_gpt4o.toml +6 -5
- examples/task_apps/pokemon_red/eval_pokemon_red_policy.py +1 -2
- examples/task_apps/pokemon_red/task_app.py +36 -5
- examples/task_apps/sokoban/README.md +2 -3
- examples/task_apps/verilog/eval_groq_qwen32b.toml +12 -14
- examples/task_apps/verilog/task_app/grpo_verilog_task_app.py +1 -1
- examples/vlm/configs/crafter_vlm_gpt4o.toml +4 -1
- examples/warming_up_to_rl/configs/crafter_fft.toml +4 -1
- examples/warming_up_to_rl/configs/crafter_fft_4b.toml +0 -2
- examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +2 -2
- examples/warming_up_to_rl/run_local_rollout_traced.py +1 -1
- examples/warming_up_to_rl/task_app/README.md +1 -1
- examples/warming_up_to_rl/task_app/grpo_crafter.py +134 -3
- examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +1 -1
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +3 -27
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +1 -1
- examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +4 -4
- examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +6 -3
- examples/workflows/math_rl/configs/rl_from_base_qwen.toml +27 -0
- examples/workflows/math_rl/configs/rl_from_base_qwen17.toml +5 -0
- synth_ai/api/train/builders.py +9 -3
- synth_ai/api/train/cli.py +125 -10
- synth_ai/api/train/configs/__init__.py +8 -1
- synth_ai/api/train/configs/rl.py +32 -7
- synth_ai/api/train/configs/sft.py +6 -2
- synth_ai/api/train/configs/shared.py +59 -2
- synth_ai/auth/credentials.py +119 -0
- synth_ai/cli/__init__.py +12 -4
- synth_ai/cli/commands/__init__.py +17 -0
- synth_ai/cli/commands/demo/__init__.py +6 -0
- synth_ai/cli/commands/demo/core.py +163 -0
- synth_ai/cli/commands/deploy/__init__.py +23 -0
- synth_ai/cli/commands/deploy/core.py +614 -0
- synth_ai/cli/commands/deploy/errors.py +72 -0
- synth_ai/cli/commands/deploy/validation.py +11 -0
- synth_ai/cli/commands/eval/__init__.py +19 -0
- synth_ai/cli/commands/eval/core.py +1109 -0
- synth_ai/cli/commands/eval/errors.py +81 -0
- synth_ai/cli/commands/eval/validation.py +133 -0
- synth_ai/cli/commands/filter/__init__.py +12 -0
- synth_ai/cli/commands/filter/core.py +388 -0
- synth_ai/cli/commands/filter/errors.py +55 -0
- synth_ai/cli/commands/filter/validation.py +77 -0
- synth_ai/cli/commands/help/__init__.py +177 -0
- synth_ai/cli/commands/help/core.py +73 -0
- synth_ai/cli/commands/status/__init__.py +64 -0
- synth_ai/cli/commands/status/client.py +192 -0
- synth_ai/cli/commands/status/config.py +92 -0
- synth_ai/cli/commands/status/errors.py +20 -0
- synth_ai/cli/commands/status/formatters.py +164 -0
- synth_ai/cli/commands/status/subcommands/__init__.py +9 -0
- synth_ai/cli/commands/status/subcommands/files.py +79 -0
- synth_ai/cli/commands/status/subcommands/jobs.py +334 -0
- synth_ai/cli/commands/status/subcommands/models.py +79 -0
- synth_ai/cli/commands/status/subcommands/runs.py +81 -0
- synth_ai/cli/commands/status/subcommands/summary.py +47 -0
- synth_ai/cli/commands/status/utils.py +114 -0
- synth_ai/cli/commands/train/__init__.py +53 -0
- synth_ai/cli/commands/train/core.py +21 -0
- synth_ai/cli/commands/train/errors.py +117 -0
- synth_ai/cli/commands/train/judge_schemas.py +199 -0
- synth_ai/cli/commands/train/judge_validation.py +304 -0
- synth_ai/cli/commands/train/validation.py +443 -0
- synth_ai/cli/demo.py +2 -162
- synth_ai/cli/deploy/__init__.py +28 -0
- synth_ai/cli/deploy/core.py +5 -0
- synth_ai/cli/deploy/errors.py +23 -0
- synth_ai/cli/deploy/validation.py +5 -0
- synth_ai/cli/eval/__init__.py +36 -0
- synth_ai/cli/eval/core.py +5 -0
- synth_ai/cli/eval/errors.py +31 -0
- synth_ai/cli/eval/validation.py +5 -0
- synth_ai/cli/filter/__init__.py +28 -0
- synth_ai/cli/filter/core.py +5 -0
- synth_ai/cli/filter/errors.py +23 -0
- synth_ai/cli/filter/validation.py +5 -0
- synth_ai/cli/modal_serve/__init__.py +12 -0
- synth_ai/cli/modal_serve/core.py +14 -0
- synth_ai/cli/modal_serve/errors.py +8 -0
- synth_ai/cli/modal_serve/validation.py +11 -0
- synth_ai/cli/serve/__init__.py +12 -0
- synth_ai/cli/serve/core.py +14 -0
- synth_ai/cli/serve/errors.py +8 -0
- synth_ai/cli/serve/validation.py +11 -0
- synth_ai/cli/setup.py +20 -265
- synth_ai/cli/status.py +7 -126
- synth_ai/cli/task_app_deploy.py +1 -10
- synth_ai/cli/task_app_modal_serve.py +4 -9
- synth_ai/cli/task_app_serve.py +4 -11
- synth_ai/cli/task_apps.py +58 -1487
- synth_ai/cli/train/__init__.py +12 -0
- synth_ai/cli/train/core.py +21 -0
- synth_ai/cli/train/errors.py +8 -0
- synth_ai/cli/train/validation.py +24 -0
- synth_ai/cli/train.py +1 -14
- synth_ai/demos/crafter/grpo_crafter_task_app.py +1 -1
- synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +1 -1
- synth_ai/environments/examples/red/engine.py +33 -12
- synth_ai/environments/examples/red/engine_helpers/reward_components.py +151 -179
- synth_ai/environments/examples/red/environment.py +26 -0
- synth_ai/environments/examples/red/trace_hooks_v3.py +168 -0
- synth_ai/http.py +12 -0
- synth_ai/judge_schemas.py +10 -11
- synth_ai/learning/rl/client.py +3 -1
- synth_ai/streaming/__init__.py +29 -0
- synth_ai/streaming/config.py +94 -0
- synth_ai/streaming/handlers.py +469 -0
- synth_ai/streaming/streamer.py +301 -0
- synth_ai/streaming/types.py +95 -0
- synth_ai/task/validators.py +2 -2
- synth_ai/tracing_v3/migration_helper.py +1 -2
- synth_ai/utils/env.py +25 -18
- synth_ai/utils/http.py +4 -1
- synth_ai/utils/modal.py +2 -2
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/METADATA +8 -3
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/RECORD +184 -109
- examples/qwen_vl/configs/eval_qwen2vl_vision.toml +0 -44
- synth_ai/cli/tui.py +0 -62
- synth_ai/tui/__init__.py +0 -5
- synth_ai/tui/__main__.py +0 -13
- synth_ai/tui/cli/__init__.py +0 -1
- synth_ai/tui/cli/query_experiments.py +0 -164
- synth_ai/tui/cli/query_experiments_v3.py +0 -164
- synth_ai/tui/dashboard.py +0 -911
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/licenses/LICENSE +0 -0
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from synth_ai.demos import core as demo_core
|
|
9
|
+
from synth_ai.utils.modal import is_modal_public_url
|
|
10
|
+
from synth_ai.utils.process import popen_capture
|
|
11
|
+
|
|
12
|
+
from .errors import (
|
|
13
|
+
DeployCliError,
|
|
14
|
+
EnvFileDiscoveryError,
|
|
15
|
+
EnvironmentKeyLoadError,
|
|
16
|
+
EnvKeyPreflightError,
|
|
17
|
+
MissingEnvironmentApiKeyError,
|
|
18
|
+
ModalCliResolutionError,
|
|
19
|
+
ModalExecutionError,
|
|
20
|
+
TaskAppNotFoundError,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
from synth_ai.cli.commands.help import DEPLOY_HELP
|
|
25
|
+
except ImportError:
|
|
26
|
+
DEPLOY_HELP = """
|
|
27
|
+
Deploy a Synth AI task app locally or to Modal.
|
|
28
|
+
|
|
29
|
+
OVERVIEW
|
|
30
|
+
--------
|
|
31
|
+
The deploy command supports two runtimes:
|
|
32
|
+
• modal: Deploy to Modal's cloud platform (default)
|
|
33
|
+
• uvicorn: Run locally with FastAPI/Uvicorn
|
|
34
|
+
|
|
35
|
+
BASIC USAGE
|
|
36
|
+
-----------
|
|
37
|
+
# Deploy to Modal (production)
|
|
38
|
+
uvx synth-ai deploy
|
|
39
|
+
|
|
40
|
+
# Deploy specific task app
|
|
41
|
+
uvx synth-ai deploy my-math-app
|
|
42
|
+
|
|
43
|
+
# Run locally for development
|
|
44
|
+
uvx synth-ai deploy --runtime=uvicorn --port 8001
|
|
45
|
+
|
|
46
|
+
MODAL DEPLOYMENT
|
|
47
|
+
----------------
|
|
48
|
+
Modal deployment requires:
|
|
49
|
+
1. Modal authentication (run: modal token new)
|
|
50
|
+
2. ENVIRONMENT_API_KEY (run: uvx synth-ai setup)
|
|
51
|
+
|
|
52
|
+
Options:
|
|
53
|
+
--modal-mode [deploy|serve] Use 'deploy' for production (default),
|
|
54
|
+
'serve' for ephemeral development
|
|
55
|
+
--name TEXT Override Modal app name
|
|
56
|
+
--dry-run Preview the deploy command without executing
|
|
57
|
+
--env-file PATH Env file(s) to load (can be repeated)
|
|
58
|
+
|
|
59
|
+
Examples:
|
|
60
|
+
# Standard production deployment
|
|
61
|
+
uvx synth-ai deploy --runtime=modal
|
|
62
|
+
|
|
63
|
+
# Deploy with custom name
|
|
64
|
+
uvx synth-ai deploy --runtime=modal --name my-task-app-v2
|
|
65
|
+
|
|
66
|
+
# Preview deployment command
|
|
67
|
+
uvx synth-ai deploy --dry-run
|
|
68
|
+
|
|
69
|
+
# Deploy with custom env file
|
|
70
|
+
uvx synth-ai deploy --env-file .env.production
|
|
71
|
+
|
|
72
|
+
LOCAL DEVELOPMENT
|
|
73
|
+
-----------------
|
|
74
|
+
Run locally with auto-reload and tracing:
|
|
75
|
+
|
|
76
|
+
uvx synth-ai deploy --runtime=uvicorn --port 8001 --reload
|
|
77
|
+
|
|
78
|
+
Options:
|
|
79
|
+
--host TEXT Bind address (default: 0.0.0.0)
|
|
80
|
+
--port INTEGER Port number (prompted if not provided)
|
|
81
|
+
--reload/--no-reload Enable auto-reload on code changes
|
|
82
|
+
--force/--no-force Kill existing process on port
|
|
83
|
+
--trace PATH Enable tracing to directory (default: traces/v3)
|
|
84
|
+
--trace-db PATH SQLite DB for traces
|
|
85
|
+
|
|
86
|
+
Examples:
|
|
87
|
+
# Basic local server
|
|
88
|
+
uvx synth-ai deploy --runtime=uvicorn
|
|
89
|
+
|
|
90
|
+
# Development with auto-reload
|
|
91
|
+
uvx synth-ai deploy --runtime=uvicorn --reload --port 8001
|
|
92
|
+
|
|
93
|
+
# With custom trace directory
|
|
94
|
+
uvx synth-ai deploy --runtime=uvicorn --trace ./my-traces
|
|
95
|
+
|
|
96
|
+
TROUBLESHOOTING
|
|
97
|
+
---------------
|
|
98
|
+
Common issues:
|
|
99
|
+
|
|
100
|
+
1. "ENVIRONMENT_API_KEY is required"
|
|
101
|
+
→ Run: uvx synth-ai setup
|
|
102
|
+
|
|
103
|
+
2. "Modal CLI not found"
|
|
104
|
+
→ Install: pip install modal
|
|
105
|
+
→ Authenticate: modal token new
|
|
106
|
+
|
|
107
|
+
3. "Task app not found"
|
|
108
|
+
→ Check app_id matches your task_app.py configuration
|
|
109
|
+
→ Run: uvx synth-ai task-app list (if available)
|
|
110
|
+
|
|
111
|
+
4. "Port already in use" (uvicorn)
|
|
112
|
+
→ Use --force to kill existing process
|
|
113
|
+
→ Or specify different --port
|
|
114
|
+
|
|
115
|
+
5. "No env file discovered"
|
|
116
|
+
→ Create .env file with required keys
|
|
117
|
+
→ Or pass --env-file explicitly
|
|
118
|
+
|
|
119
|
+
ENVIRONMENT VARIABLES
|
|
120
|
+
---------------------
|
|
121
|
+
SYNTH_API_KEY Your Synth platform API key
|
|
122
|
+
ENVIRONMENT_API_KEY Task environment authentication
|
|
123
|
+
TASK_APP_BASE_URL Base URL for deployed task app
|
|
124
|
+
DEMO_DIR Demo directory path
|
|
125
|
+
SYNTH_DEMO_DIR Alternative demo directory
|
|
126
|
+
|
|
127
|
+
For more information: https://docs.usesynth.ai/deploy
|
|
128
|
+
""" # type: ignore[assignment]
|
|
129
|
+
|
|
130
|
+
try: # Click >= 8.1
|
|
131
|
+
from click.core import ParameterSource
|
|
132
|
+
except ImportError: # pragma: no cover - fallback for older versions
|
|
133
|
+
ParameterSource = None # type: ignore[assignment]
|
|
134
|
+
|
|
135
|
+
__all__ = [
|
|
136
|
+
"command",
|
|
137
|
+
"get_command",
|
|
138
|
+
"modal_serve_command",
|
|
139
|
+
"register_task_app_commands",
|
|
140
|
+
"run_modal_runtime",
|
|
141
|
+
"run_uvicorn_runtime",
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _translate_click_exception(err: click.ClickException) -> DeployCliError | None:
|
|
146
|
+
message = getattr(err, "message", str(err)).strip()
|
|
147
|
+
lower = message.lower()
|
|
148
|
+
|
|
149
|
+
def _missing_env_hint() -> str:
|
|
150
|
+
return (
|
|
151
|
+
"Run `uvx synth-ai setup` to mint credentials or pass --env-file pointing to a file "
|
|
152
|
+
"with ENVIRONMENT_API_KEY."
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if "environment_api_key missing" in lower:
|
|
156
|
+
return MissingEnvironmentApiKeyError(hint=_missing_env_hint())
|
|
157
|
+
if "environment api key is required" in lower:
|
|
158
|
+
return MissingEnvironmentApiKeyError(hint=_missing_env_hint())
|
|
159
|
+
if "failed to load environment_api_key from generated .env" in lower:
|
|
160
|
+
return EnvironmentKeyLoadError()
|
|
161
|
+
|
|
162
|
+
if message.startswith("Env file not found:"):
|
|
163
|
+
path = message.split(":", 1)[1].strip()
|
|
164
|
+
return EnvFileDiscoveryError(attempted=(path,), hint=_missing_env_hint())
|
|
165
|
+
if "env file required (--env-file) for this task app" in lower:
|
|
166
|
+
return EnvFileDiscoveryError(hint=_missing_env_hint())
|
|
167
|
+
if message.startswith("No .env file discovered automatically"):
|
|
168
|
+
return EnvFileDiscoveryError(hint=_missing_env_hint())
|
|
169
|
+
if message == "No environment values found":
|
|
170
|
+
return EnvFileDiscoveryError(hint=_missing_env_hint())
|
|
171
|
+
|
|
172
|
+
if message.startswith("Task app '") and " not found. Available:" in message:
|
|
173
|
+
try:
|
|
174
|
+
before, after = message.split(" not found. Available:", 1)
|
|
175
|
+
app_id = before.split("Task app '", 1)[1].rstrip("'")
|
|
176
|
+
available = tuple(item.strip() for item in after.split(",") if item.strip())
|
|
177
|
+
except Exception:
|
|
178
|
+
app_id = None
|
|
179
|
+
available = ()
|
|
180
|
+
return TaskAppNotFoundError(app_id=app_id, available=available)
|
|
181
|
+
if message == "No task apps discovered for this command.":
|
|
182
|
+
return TaskAppNotFoundError()
|
|
183
|
+
|
|
184
|
+
if "modal cli not found" in lower:
|
|
185
|
+
return ModalCliResolutionError(detail=message)
|
|
186
|
+
if "--modal-cli path does not exist" in lower or "--modal-cli is not executable" in lower:
|
|
187
|
+
return ModalCliResolutionError(detail=message)
|
|
188
|
+
if "modal cli resolution found the synth-ai shim" in lower:
|
|
189
|
+
return ModalCliResolutionError(detail=message)
|
|
190
|
+
|
|
191
|
+
if message.startswith("modal ") and "failed with exit code" in message:
|
|
192
|
+
parts = message.split(" failed with exit code ")
|
|
193
|
+
command = parts[0].replace("modal ", "").strip() if len(parts) > 1 else "deploy"
|
|
194
|
+
exit_code = int(parts[1]) if len(parts) > 1 and parts[1].isdigit() else -1
|
|
195
|
+
return ModalExecutionError(command=command, exit_code=exit_code)
|
|
196
|
+
|
|
197
|
+
if message.startswith("[CRITICAL] ") or "[CRITICAL]" in message:
|
|
198
|
+
return EnvKeyPreflightError(detail=message.removeprefix("[CRITICAL] ").strip())
|
|
199
|
+
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _format_deploy_error(err: DeployCliError) -> str:
|
|
204
|
+
if isinstance(err, MissingEnvironmentApiKeyError):
|
|
205
|
+
hint = err.hint or "Provide ENVIRONMENT_API_KEY via --env-file or run `uvx synth-ai setup`."
|
|
206
|
+
return f"ENVIRONMENT_API_KEY is required. {hint}"
|
|
207
|
+
if isinstance(err, EnvironmentKeyLoadError):
|
|
208
|
+
base = "Failed to persist or reload ENVIRONMENT_API_KEY"
|
|
209
|
+
if err.path:
|
|
210
|
+
base += f" from {err.path}"
|
|
211
|
+
return f"{base}. Regenerate the env file with `uvx synth-ai setup` or edit it manually."
|
|
212
|
+
if isinstance(err, EnvFileDiscoveryError):
|
|
213
|
+
attempted = ", ".join(err.attempted) if err.attempted else "No env files located"
|
|
214
|
+
hint = err.hint or "Pass --env-file explicitly or run `uvx synth-ai setup`."
|
|
215
|
+
return f"Unable to locate a usable env file ({attempted}). {hint}"
|
|
216
|
+
if isinstance(err, TaskAppNotFoundError):
|
|
217
|
+
available = ", ".join(err.available) if err.available else "no registered apps"
|
|
218
|
+
app_id = err.app_id or "requested app"
|
|
219
|
+
return f"Could not find task app '{app_id}'. Available choices: {available}."
|
|
220
|
+
if isinstance(err, ModalCliResolutionError):
|
|
221
|
+
detail = err.detail or "Modal CLI could not be resolved."
|
|
222
|
+
return (
|
|
223
|
+
f"{detail} Install the `modal` package or pass --modal-cli with the path to the Modal binary."
|
|
224
|
+
)
|
|
225
|
+
if isinstance(err, ModalExecutionError):
|
|
226
|
+
return (
|
|
227
|
+
f"Modal {err.command} exited with status {err.exit_code}. "
|
|
228
|
+
"Review the Modal output above or rerun with --dry-run."
|
|
229
|
+
)
|
|
230
|
+
if isinstance(err, EnvKeyPreflightError):
|
|
231
|
+
detail = err.detail or "Failed to upload ENVIRONMENT_API_KEY to the backend."
|
|
232
|
+
return f"{detail} Ensure SYNTH_API_KEY is set and retry `uvx synth-ai setup`."
|
|
233
|
+
return str(err)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@lru_cache(maxsize=1)
|
|
237
|
+
def _task_apps_module():
|
|
238
|
+
from synth_ai.cli import task_apps as module # local import to avoid circular deps
|
|
239
|
+
|
|
240
|
+
return module
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _maybe_fix_task_url(modal_name: str | None = None, demo_dir: str | None = None) -> None:
|
|
244
|
+
"""Look up the Modal public URL and persist it to the task app config if needed."""
|
|
245
|
+
env = demo_core.load_env()
|
|
246
|
+
task_app_name = modal_name or env.task_app_name
|
|
247
|
+
if not task_app_name:
|
|
248
|
+
return
|
|
249
|
+
current = env.task_app_base_url
|
|
250
|
+
needs_lookup = not current or not is_modal_public_url(current)
|
|
251
|
+
if not needs_lookup:
|
|
252
|
+
return
|
|
253
|
+
code, out = popen_capture(
|
|
254
|
+
[
|
|
255
|
+
"uv",
|
|
256
|
+
"run",
|
|
257
|
+
"python",
|
|
258
|
+
"-m",
|
|
259
|
+
"modal",
|
|
260
|
+
"app",
|
|
261
|
+
"url",
|
|
262
|
+
task_app_name,
|
|
263
|
+
]
|
|
264
|
+
)
|
|
265
|
+
if code != 0 or not out:
|
|
266
|
+
return
|
|
267
|
+
new_url = ""
|
|
268
|
+
for token in out.split():
|
|
269
|
+
if is_modal_public_url(token):
|
|
270
|
+
new_url = token.strip().rstrip("/")
|
|
271
|
+
break
|
|
272
|
+
if new_url and new_url != current:
|
|
273
|
+
click.echo(f"Updating TASK_APP_BASE_URL from Modal CLI → {new_url}")
|
|
274
|
+
persist_path = demo_dir or os.getcwd()
|
|
275
|
+
demo_core.persist_task_url(new_url, name=task_app_name, path=persist_path)
|
|
276
|
+
os.environ["TASK_APP_BASE_URL"] = new_url
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def run_uvicorn_runtime(
|
|
280
|
+
app_id: str | None,
|
|
281
|
+
host: str,
|
|
282
|
+
port: int | None,
|
|
283
|
+
env_file: Sequence[str],
|
|
284
|
+
reload_flag: bool,
|
|
285
|
+
force: bool,
|
|
286
|
+
trace_dir: str | None,
|
|
287
|
+
trace_db: str | None,
|
|
288
|
+
) -> None:
|
|
289
|
+
module = _task_apps_module()
|
|
290
|
+
|
|
291
|
+
if not host:
|
|
292
|
+
host = "0.0.0.0"
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
demo_dir_path = module._load_demo_directory()
|
|
296
|
+
if demo_dir_path:
|
|
297
|
+
if not demo_dir_path.is_dir():
|
|
298
|
+
raise click.ClickException(
|
|
299
|
+
f"Demo directory not found: {demo_dir_path}\nRun 'synth-ai setup' to create a demo."
|
|
300
|
+
)
|
|
301
|
+
os.chdir(demo_dir_path)
|
|
302
|
+
click.echo(f"Using demo directory: {demo_dir_path}\n")
|
|
303
|
+
os.environ["SYNTH_DEMO_DIR"] = str(demo_dir_path.resolve())
|
|
304
|
+
|
|
305
|
+
if port is None:
|
|
306
|
+
port = click.prompt("Port to serve on", type=int, default=8001)
|
|
307
|
+
|
|
308
|
+
if trace_dir is None:
|
|
309
|
+
click.echo(
|
|
310
|
+
"\nTracing captures rollout data (actions, rewards, model outputs) to a local SQLite DB."
|
|
311
|
+
)
|
|
312
|
+
click.echo("This data can be exported to JSONL for supervised fine-tuning (SFT).")
|
|
313
|
+
enable_tracing = click.confirm("Enable tracing?", default=True)
|
|
314
|
+
if enable_tracing:
|
|
315
|
+
demo_base = Path(os.environ.get("SYNTH_DEMO_DIR") or Path.cwd())
|
|
316
|
+
default_trace_dir = str((demo_base / "traces/v3").resolve())
|
|
317
|
+
trace_dir = click.prompt(
|
|
318
|
+
"Trace directory", type=str, default=default_trace_dir, show_default=True
|
|
319
|
+
)
|
|
320
|
+
else:
|
|
321
|
+
trace_dir = None
|
|
322
|
+
|
|
323
|
+
if trace_dir and trace_db is None:
|
|
324
|
+
demo_base = Path(os.environ.get("SYNTH_DEMO_DIR") or Path.cwd())
|
|
325
|
+
default_trace_db = str((demo_base / "traces/v3/synth_ai.db").resolve())
|
|
326
|
+
trace_db = click.prompt(
|
|
327
|
+
"Trace DB path", type=str, default=default_trace_db, show_default=True
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
choice = module._select_app_choice(app_id, purpose="serve")
|
|
331
|
+
entry = choice.ensure_entry()
|
|
332
|
+
module._serve_entry(
|
|
333
|
+
entry,
|
|
334
|
+
host,
|
|
335
|
+
port,
|
|
336
|
+
env_file,
|
|
337
|
+
reload_flag,
|
|
338
|
+
force,
|
|
339
|
+
trace_dir=trace_dir,
|
|
340
|
+
trace_db=trace_db,
|
|
341
|
+
)
|
|
342
|
+
except DeployCliError:
|
|
343
|
+
raise
|
|
344
|
+
except click.ClickException as err:
|
|
345
|
+
converted = _translate_click_exception(err)
|
|
346
|
+
if converted:
|
|
347
|
+
raise converted from err
|
|
348
|
+
raise
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def run_modal_runtime(
|
|
352
|
+
app_id: str | None,
|
|
353
|
+
*,
|
|
354
|
+
command: Literal["deploy", "serve"],
|
|
355
|
+
modal_name: str | None,
|
|
356
|
+
dry_run: bool,
|
|
357
|
+
modal_cli: str,
|
|
358
|
+
env_file: Sequence[str],
|
|
359
|
+
use_demo_dir: bool = True,
|
|
360
|
+
) -> None:
|
|
361
|
+
module = _task_apps_module()
|
|
362
|
+
|
|
363
|
+
try:
|
|
364
|
+
demo_dir_path = None
|
|
365
|
+
if use_demo_dir:
|
|
366
|
+
demo_dir_path = module._load_demo_directory()
|
|
367
|
+
if demo_dir_path:
|
|
368
|
+
if not demo_dir_path.is_dir():
|
|
369
|
+
raise click.ClickException(
|
|
370
|
+
f"Demo directory not found: {demo_dir_path}\nRun 'synth-ai demo' to create a demo."
|
|
371
|
+
)
|
|
372
|
+
os.chdir(demo_dir_path)
|
|
373
|
+
click.echo(f"Using demo directory: {demo_dir_path}\n")
|
|
374
|
+
|
|
375
|
+
purpose = "modal-serve" if command == "serve" else "deploy"
|
|
376
|
+
choice = module._select_app_choice(app_id, purpose=purpose)
|
|
377
|
+
|
|
378
|
+
if choice.modal_script:
|
|
379
|
+
env_paths = module._resolve_env_paths_for_script(choice.modal_script, env_file)
|
|
380
|
+
click.echo("Using env file(s): " + ", ".join(str(p.resolve()) for p in env_paths))
|
|
381
|
+
module._run_modal_script(
|
|
382
|
+
choice.modal_script,
|
|
383
|
+
modal_cli,
|
|
384
|
+
command,
|
|
385
|
+
env_paths,
|
|
386
|
+
modal_name=modal_name,
|
|
387
|
+
dry_run=dry_run if command == "deploy" else False,
|
|
388
|
+
)
|
|
389
|
+
if command == "deploy" and not dry_run:
|
|
390
|
+
_maybe_fix_task_url(
|
|
391
|
+
modal_name=modal_name,
|
|
392
|
+
demo_dir=str(demo_dir_path) if demo_dir_path else None
|
|
393
|
+
)
|
|
394
|
+
return
|
|
395
|
+
|
|
396
|
+
entry = choice.ensure_entry()
|
|
397
|
+
if command == "serve":
|
|
398
|
+
click.echo(f"[modal-serve] serving entry {entry.app_id} from {choice.path}")
|
|
399
|
+
module._modal_serve_entry(entry, modal_name, modal_cli, env_file, original_path=choice.path)
|
|
400
|
+
else:
|
|
401
|
+
module._deploy_entry(entry, modal_name, dry_run, modal_cli, env_file, original_path=choice.path)
|
|
402
|
+
if not dry_run:
|
|
403
|
+
_maybe_fix_task_url(
|
|
404
|
+
modal_name=modal_name,
|
|
405
|
+
demo_dir=str(demo_dir_path) if demo_dir_path else None
|
|
406
|
+
)
|
|
407
|
+
except DeployCliError:
|
|
408
|
+
raise
|
|
409
|
+
except click.ClickException as err:
|
|
410
|
+
converted = _translate_click_exception(err)
|
|
411
|
+
if converted:
|
|
412
|
+
raise converted from err
|
|
413
|
+
raise
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
@click.command(
|
|
417
|
+
"deploy",
|
|
418
|
+
help=DEPLOY_HELP,
|
|
419
|
+
epilog="Run 'uvx synth-ai deploy --help' for detailed usage information.",
|
|
420
|
+
)
|
|
421
|
+
@click.argument("app_id", type=str, required=False)
|
|
422
|
+
@click.option(
|
|
423
|
+
"--runtime",
|
|
424
|
+
type=click.Choice(["modal", "uvicorn"], case_sensitive=False),
|
|
425
|
+
default="modal",
|
|
426
|
+
show_default=True,
|
|
427
|
+
help="Runtime to execute: 'modal' for remote Modal jobs, 'uvicorn' for the local FastAPI server.",
|
|
428
|
+
)
|
|
429
|
+
@click.option("--name", "modal_name", default=None, help="Override Modal app name")
|
|
430
|
+
@click.option("--dry-run", is_flag=True, help="Print modal deploy command without executing")
|
|
431
|
+
@click.option("--modal-cli", default="modal", help="Path to modal CLI executable")
|
|
432
|
+
@click.option(
|
|
433
|
+
"--modal-mode",
|
|
434
|
+
type=click.Choice(["deploy", "serve"], case_sensitive=False),
|
|
435
|
+
default="deploy",
|
|
436
|
+
show_default=True,
|
|
437
|
+
help="Modal operation to run when --runtime=modal.",
|
|
438
|
+
)
|
|
439
|
+
@click.option(
|
|
440
|
+
"--env-file",
|
|
441
|
+
multiple=True,
|
|
442
|
+
type=click.Path(),
|
|
443
|
+
help="Env file to load into the container (can be repeated)",
|
|
444
|
+
)
|
|
445
|
+
@click.option("--host", default="0.0.0.0", show_default=True, help="Host for --runtime=uvicorn")
|
|
446
|
+
@click.option("--port", default=None, type=int, help="Port to serve on when --runtime=uvicorn")
|
|
447
|
+
@click.option(
|
|
448
|
+
"--reload/--no-reload",
|
|
449
|
+
"reload_flag",
|
|
450
|
+
default=False,
|
|
451
|
+
help="Enable uvicorn auto-reload when --runtime=uvicorn",
|
|
452
|
+
)
|
|
453
|
+
@click.option(
|
|
454
|
+
"--force/--no-force",
|
|
455
|
+
"force",
|
|
456
|
+
default=False,
|
|
457
|
+
help="Kill any process already bound to the selected port (uvicorn runtime)",
|
|
458
|
+
)
|
|
459
|
+
@click.option(
|
|
460
|
+
"--trace",
|
|
461
|
+
"trace_dir",
|
|
462
|
+
type=click.Path(),
|
|
463
|
+
default=None,
|
|
464
|
+
help="Enable tracing and write SFT JSONL files when --runtime=uvicorn (default: traces/v3).",
|
|
465
|
+
)
|
|
466
|
+
@click.option(
|
|
467
|
+
"--trace-db",
|
|
468
|
+
"trace_db",
|
|
469
|
+
type=click.Path(),
|
|
470
|
+
default=None,
|
|
471
|
+
help="Override local trace DB path when --runtime=uvicorn (default: traces/v3/synth_ai.db).",
|
|
472
|
+
)
|
|
473
|
+
def deploy_command(
|
|
474
|
+
app_id: str | None,
|
|
475
|
+
runtime: str,
|
|
476
|
+
modal_name: str | None,
|
|
477
|
+
dry_run: bool,
|
|
478
|
+
modal_cli: str,
|
|
479
|
+
modal_mode: str,
|
|
480
|
+
env_file: Sequence[str],
|
|
481
|
+
host: str,
|
|
482
|
+
port: int | None,
|
|
483
|
+
reload_flag: bool,
|
|
484
|
+
force: bool,
|
|
485
|
+
trace_dir: str | None,
|
|
486
|
+
trace_db: str | None,
|
|
487
|
+
) -> None:
|
|
488
|
+
"""Deploy a task app locally or on Modal.
|
|
489
|
+
|
|
490
|
+
This command deploys your Synth AI task app either to Modal's cloud platform
|
|
491
|
+
or runs it locally with Uvicorn for development. Use --help for detailed usage.
|
|
492
|
+
"""
|
|
493
|
+
|
|
494
|
+
runtime_normalized = runtime.lower()
|
|
495
|
+
modal_mode_normalized = modal_mode.lower()
|
|
496
|
+
ctx = click.get_current_context()
|
|
497
|
+
|
|
498
|
+
def _source(name: str) -> Any:
|
|
499
|
+
if ctx is None:
|
|
500
|
+
return None
|
|
501
|
+
return ctx.get_parameter_source(name)
|
|
502
|
+
|
|
503
|
+
def _was_user_provided(name: str) -> bool:
|
|
504
|
+
source = _source(name)
|
|
505
|
+
if ParameterSource is None:
|
|
506
|
+
return bool(source) and str(source) not in {"ParameterSource.DEFAULT", "ParameterSource.NONE"}
|
|
507
|
+
none_sentinel = getattr(ParameterSource, "NONE", None)
|
|
508
|
+
default_sources = {ParameterSource.DEFAULT}
|
|
509
|
+
if none_sentinel is not None:
|
|
510
|
+
default_sources.add(none_sentinel)
|
|
511
|
+
return bool(source) and source not in default_sources
|
|
512
|
+
|
|
513
|
+
try:
|
|
514
|
+
if runtime_normalized == "modal":
|
|
515
|
+
uvicorn_only_options = [
|
|
516
|
+
("host", "--host"),
|
|
517
|
+
("port", "--port"),
|
|
518
|
+
("reload_flag", "--reload/--no-reload"),
|
|
519
|
+
("force", "--force/--no-force"),
|
|
520
|
+
("trace_dir", "--trace"),
|
|
521
|
+
("trace_db", "--trace-db"),
|
|
522
|
+
]
|
|
523
|
+
invalid = [label for param, label in uvicorn_only_options if _was_user_provided(param)]
|
|
524
|
+
if invalid:
|
|
525
|
+
raise click.ClickException(
|
|
526
|
+
f"{', '.join(invalid)} cannot be used with --runtime=modal."
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
if modal_mode_normalized == "serve" and _was_user_provided("dry_run"):
|
|
530
|
+
raise click.ClickException("--dry-run is not supported with --modal-mode=serve.")
|
|
531
|
+
|
|
532
|
+
command_choice: Literal["deploy", "serve"] = (
|
|
533
|
+
"serve" if modal_mode_normalized == "serve" else "deploy"
|
|
534
|
+
)
|
|
535
|
+
run_modal_runtime(
|
|
536
|
+
app_id,
|
|
537
|
+
command=command_choice,
|
|
538
|
+
modal_name=modal_name,
|
|
539
|
+
dry_run=dry_run,
|
|
540
|
+
modal_cli=modal_cli,
|
|
541
|
+
env_file=env_file,
|
|
542
|
+
)
|
|
543
|
+
return
|
|
544
|
+
|
|
545
|
+
modal_only_options = [
|
|
546
|
+
("modal_name", "--name"),
|
|
547
|
+
("dry_run", "--dry-run"),
|
|
548
|
+
("modal_cli", "--modal-cli"),
|
|
549
|
+
("modal_mode", "--modal-mode"),
|
|
550
|
+
]
|
|
551
|
+
invalid = [label for param, label in modal_only_options if _was_user_provided(param)]
|
|
552
|
+
if invalid:
|
|
553
|
+
raise click.ClickException(
|
|
554
|
+
f"{', '.join(invalid)} cannot be used with --runtime=uvicorn."
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
run_uvicorn_runtime(app_id, host, port, env_file, reload_flag, force, trace_dir, trace_db)
|
|
558
|
+
except DeployCliError as exc:
|
|
559
|
+
raise click.ClickException(_format_deploy_error(exc)) from exc
|
|
560
|
+
except click.ClickException as err:
|
|
561
|
+
converted = _translate_click_exception(err)
|
|
562
|
+
if converted:
|
|
563
|
+
raise click.ClickException(_format_deploy_error(converted)) from err
|
|
564
|
+
raise
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
@click.command("modal-serve")
|
|
568
|
+
@click.argument("app_id", type=str, required=False)
|
|
569
|
+
@click.option("--modal-cli", default="modal", help="Path to modal CLI executable")
|
|
570
|
+
@click.option("--name", "modal_name", default=None, help="Override Modal app name (optional)")
|
|
571
|
+
@click.option(
|
|
572
|
+
"--env-file",
|
|
573
|
+
multiple=True,
|
|
574
|
+
type=click.Path(),
|
|
575
|
+
help="Env file to load into the container (can be repeated)",
|
|
576
|
+
)
|
|
577
|
+
def modal_serve_command(
|
|
578
|
+
app_id: str | None, modal_cli: str, modal_name: str | None, env_file: Sequence[str]
|
|
579
|
+
) -> None:
|
|
580
|
+
click.echo(f"[modal-serve] requested app_id={app_id or '(auto)'} modal_cli={modal_cli}")
|
|
581
|
+
try:
|
|
582
|
+
run_modal_runtime(
|
|
583
|
+
app_id,
|
|
584
|
+
command="serve",
|
|
585
|
+
modal_name=modal_name,
|
|
586
|
+
dry_run=False,
|
|
587
|
+
modal_cli=modal_cli,
|
|
588
|
+
env_file=env_file,
|
|
589
|
+
use_demo_dir=False,
|
|
590
|
+
)
|
|
591
|
+
except DeployCliError as exc:
|
|
592
|
+
raise click.ClickException(_format_deploy_error(exc)) from exc
|
|
593
|
+
except click.ClickException as err:
|
|
594
|
+
converted = _translate_click_exception(err)
|
|
595
|
+
if converted:
|
|
596
|
+
raise click.ClickException(_format_deploy_error(converted)) from err
|
|
597
|
+
raise
|
|
598
|
+
except SystemExit as exc: # bubble up with context (legacy argparse would trigger this)
|
|
599
|
+
raise click.ClickException(
|
|
600
|
+
f"Legacy CLI intercepted modal-serve (exit {exc.code}). "
|
|
601
|
+
"Make sure you're running the Click CLI (synth_ai.cli:cli)."
|
|
602
|
+
) from exc
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
command = deploy_command
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def get_command() -> click.Command:
|
|
609
|
+
return command
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def register_task_app_commands(task_app_group: click.Group) -> None:
|
|
613
|
+
task_app_group.add_command(command)
|
|
614
|
+
task_app_group.add_command(modal_serve_command)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DeployCliError(RuntimeError):
|
|
7
|
+
"""Base exception for deploy CLI failures."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(slots=True)
|
|
11
|
+
class MissingEnvironmentApiKeyError(DeployCliError):
|
|
12
|
+
"""Raised when ENVIRONMENT_API_KEY is absent and cannot be collected interactively."""
|
|
13
|
+
|
|
14
|
+
hint: str | None = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(slots=True)
|
|
18
|
+
class EnvironmentKeyLoadError(DeployCliError):
|
|
19
|
+
"""Raised when we fail to persist or reload ENVIRONMENT_API_KEY from disk."""
|
|
20
|
+
|
|
21
|
+
path: str | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(slots=True)
|
|
25
|
+
class EnvFileDiscoveryError(DeployCliError):
|
|
26
|
+
"""Raised when no suitable env file can be found for a task app."""
|
|
27
|
+
|
|
28
|
+
attempted: tuple[str, ...] = ()
|
|
29
|
+
hint: str | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(slots=True)
|
|
33
|
+
class TaskAppNotFoundError(DeployCliError):
|
|
34
|
+
"""Raised when the requested task app identifier cannot be resolved."""
|
|
35
|
+
|
|
36
|
+
app_id: str | None = None
|
|
37
|
+
available: tuple[str, ...] = ()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(slots=True)
|
|
41
|
+
class ModalCliResolutionError(DeployCliError):
|
|
42
|
+
"""Raised when the Modal CLI executable cannot be located or invoked."""
|
|
43
|
+
|
|
44
|
+
cli_path: str | None = None
|
|
45
|
+
detail: str | None = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(slots=True)
|
|
49
|
+
class ModalExecutionError(DeployCliError):
|
|
50
|
+
"""Raised when a Modal subprocess exits with a non-zero status."""
|
|
51
|
+
|
|
52
|
+
command: str
|
|
53
|
+
exit_code: int
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(slots=True)
|
|
57
|
+
class EnvKeyPreflightError(DeployCliError):
|
|
58
|
+
"""Raised when uploading or minting ENVIRONMENT_API_KEY to the backend fails."""
|
|
59
|
+
|
|
60
|
+
detail: str | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
__all__ = [
|
|
64
|
+
"DeployCliError",
|
|
65
|
+
"MissingEnvironmentApiKeyError",
|
|
66
|
+
"EnvironmentKeyLoadError",
|
|
67
|
+
"EnvFileDiscoveryError",
|
|
68
|
+
"TaskAppNotFoundError",
|
|
69
|
+
"ModalCliResolutionError",
|
|
70
|
+
"ModalExecutionError",
|
|
71
|
+
"EnvKeyPreflightError",
|
|
72
|
+
]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import MutableMapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
__all__ = ["validate_deploy_options"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def validate_deploy_options(options: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
|
|
10
|
+
"""Validate parameters passed to the deploy CLI command."""
|
|
11
|
+
return options
|