together 2.0.0a17__py3-none-any.whl → 2.0.0a19__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.
Files changed (67) hide show
  1. together/_base_client.py +5 -2
  2. together/_client.py +1 -77
  3. together/_compat.py +3 -3
  4. together/_utils/_json.py +35 -0
  5. together/_version.py +1 -1
  6. together/lib/cli/api/beta/__init__.py +2 -0
  7. together/lib/cli/api/beta/jig/__init__.py +52 -0
  8. together/lib/cli/api/beta/jig/_config.py +170 -0
  9. together/lib/cli/api/beta/jig/jig.py +664 -0
  10. together/lib/cli/api/beta/jig/secrets.py +138 -0
  11. together/lib/cli/api/beta/jig/volumes.py +509 -0
  12. together/lib/cli/api/endpoints/create.py +7 -3
  13. together/lib/cli/api/endpoints/hardware.py +38 -7
  14. together/lib/cli/api/models/upload.py +5 -1
  15. together/resources/__init__.py +0 -28
  16. together/resources/beta/__init__.py +14 -0
  17. together/resources/beta/beta.py +32 -0
  18. together/resources/beta/clusters/clusters.py +12 -12
  19. together/resources/beta/clusters/storage.py +10 -10
  20. together/resources/beta/jig/__init__.py +61 -0
  21. together/resources/beta/jig/jig.py +1004 -0
  22. together/resources/beta/jig/queue.py +482 -0
  23. together/resources/beta/jig/secrets.py +548 -0
  24. together/resources/beta/jig/volumes.py +514 -0
  25. together/resources/chat/completions.py +10 -0
  26. together/resources/endpoints.py +103 -1
  27. together/resources/models/__init__.py +33 -0
  28. together/resources/{models.py → models/models.py} +41 -9
  29. together/resources/models/uploads.py +163 -0
  30. together/types/__init__.py +2 -4
  31. together/types/beta/__init__.py +6 -0
  32. together/types/beta/deployment.py +261 -0
  33. together/types/beta/deployment_logs.py +11 -0
  34. together/types/beta/jig/__init__.py +20 -0
  35. together/types/beta/jig/queue_cancel_params.py +13 -0
  36. together/types/beta/jig/queue_cancel_response.py +11 -0
  37. together/types/beta/jig/queue_metrics_params.py +12 -0
  38. together/types/beta/jig/queue_metrics_response.py +8 -0
  39. together/types/beta/jig/queue_retrieve_params.py +15 -0
  40. together/types/beta/jig/queue_retrieve_response.py +35 -0
  41. together/types/beta/jig/queue_submit_params.py +19 -0
  42. together/types/beta/jig/queue_submit_response.py +25 -0
  43. together/types/beta/jig/secret.py +33 -0
  44. together/types/beta/jig/secret_create_params.py +34 -0
  45. together/types/beta/jig/secret_list_response.py +16 -0
  46. together/types/beta/jig/secret_update_params.py +34 -0
  47. together/types/beta/jig/volume.py +47 -0
  48. together/types/beta/jig/volume_create_params.py +34 -0
  49. together/types/beta/jig/volume_list_response.py +16 -0
  50. together/types/beta/jig/volume_update_params.py +34 -0
  51. together/types/beta/jig_deploy_params.py +150 -0
  52. together/types/beta/jig_list_response.py +16 -0
  53. together/types/beta/jig_retrieve_logs_params.py +12 -0
  54. together/types/beta/jig_update_params.py +141 -0
  55. together/types/chat/completion_create_params.py +11 -0
  56. together/types/{hardware_list_params.py → endpoint_list_hardware_params.py} +2 -2
  57. together/types/{hardware_list_response.py → endpoint_list_hardware_response.py} +2 -2
  58. together/types/models/__init__.py +5 -0
  59. together/types/{job_retrieve_response.py → models/upload_status_response.py} +3 -3
  60. {together-2.0.0a17.dist-info → together-2.0.0a19.dist-info}/METADATA +15 -14
  61. {together-2.0.0a17.dist-info → together-2.0.0a19.dist-info}/RECORD +64 -30
  62. together/resources/hardware.py +0 -181
  63. together/resources/jobs.py +0 -214
  64. together/types/job_list_response.py +0 -47
  65. {together-2.0.0a17.dist-info → together-2.0.0a19.dist-info}/WHEEL +0 -0
  66. {together-2.0.0a17.dist-info → together-2.0.0a19.dist-info}/entry_points.txt +0 -0
  67. {together-2.0.0a17.dist-info → together-2.0.0a19.dist-info}/licenses/LICENSE +0 -0
together/_base_client.py CHANGED
@@ -86,6 +86,7 @@ from ._exceptions import (
86
86
  APIConnectionError,
87
87
  APIResponseValidationError,
88
88
  )
89
+ from ._utils._json import openapi_dumps
89
90
 
90
91
  log: logging.Logger = logging.getLogger(__name__)
91
92
 
@@ -554,8 +555,10 @@ class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]):
554
555
  kwargs["content"] = options.content
555
556
  elif isinstance(json_data, bytes):
556
557
  kwargs["content"] = json_data
557
- else:
558
- kwargs["json"] = json_data if is_given(json_data) else None
558
+ elif not files:
559
+ # Don't set content when JSON is sent as multipart/form-data,
560
+ # since httpx's content param overrides other body arguments
561
+ kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None
559
562
  kwargs["files"] = files
560
563
  else:
561
564
  headers.pop("Content-Type", None)
together/_client.py CHANGED
@@ -37,7 +37,6 @@ if TYPE_CHECKING:
37
37
  from .resources import (
38
38
  beta,
39
39
  chat,
40
- jobs,
41
40
  audio,
42
41
  evals,
43
42
  files,
@@ -46,22 +45,18 @@ if TYPE_CHECKING:
46
45
  rerank,
47
46
  videos,
48
47
  batches,
49
- hardware,
50
48
  endpoints,
51
49
  embeddings,
52
50
  completions,
53
51
  fine_tuning,
54
52
  code_interpreter,
55
53
  )
56
- from .resources.jobs import JobsResource, AsyncJobsResource
57
54
  from .resources.evals import EvalsResource, AsyncEvalsResource
58
55
  from .resources.files import FilesResource, AsyncFilesResource
59
56
  from .resources.images import ImagesResource, AsyncImagesResource
60
- from .resources.models import ModelsResource, AsyncModelsResource
61
57
  from .resources.rerank import RerankResource, AsyncRerankResource
62
58
  from .resources.videos import VideosResource, AsyncVideosResource
63
59
  from .resources.batches import BatchesResource, AsyncBatchesResource
64
- from .resources.hardware import HardwareResource, AsyncHardwareResource
65
60
  from .resources.beta.beta import BetaResource, AsyncBetaResource
66
61
  from .resources.chat.chat import ChatResource, AsyncChatResource
67
62
  from .resources.endpoints import EndpointsResource, AsyncEndpointsResource
@@ -69,6 +64,7 @@ if TYPE_CHECKING:
69
64
  from .resources.audio.audio import AudioResource, AsyncAudioResource
70
65
  from .resources.completions import CompletionsResource, AsyncCompletionsResource
71
66
  from .resources.fine_tuning import FineTuningResource, AsyncFineTuningResource
67
+ from .resources.models.models import ModelsResource, AsyncModelsResource
72
68
  from .resources.code_interpreter.code_interpreter import CodeInterpreterResource, AsyncCodeInterpreterResource
73
69
 
74
70
  __all__ = [
@@ -209,24 +205,12 @@ class Together(SyncAPIClient):
209
205
 
210
206
  return ModelsResource(self)
211
207
 
212
- @cached_property
213
- def jobs(self) -> JobsResource:
214
- from .resources.jobs import JobsResource
215
-
216
- return JobsResource(self)
217
-
218
208
  @cached_property
219
209
  def endpoints(self) -> EndpointsResource:
220
210
  from .resources.endpoints import EndpointsResource
221
211
 
222
212
  return EndpointsResource(self)
223
213
 
224
- @cached_property
225
- def hardware(self) -> HardwareResource:
226
- from .resources.hardware import HardwareResource
227
-
228
- return HardwareResource(self)
229
-
230
214
  @cached_property
231
215
  def rerank(self) -> RerankResource:
232
216
  from .resources.rerank import RerankResource
@@ -486,24 +470,12 @@ class AsyncTogether(AsyncAPIClient):
486
470
 
487
471
  return AsyncModelsResource(self)
488
472
 
489
- @cached_property
490
- def jobs(self) -> AsyncJobsResource:
491
- from .resources.jobs import AsyncJobsResource
492
-
493
- return AsyncJobsResource(self)
494
-
495
473
  @cached_property
496
474
  def endpoints(self) -> AsyncEndpointsResource:
497
475
  from .resources.endpoints import AsyncEndpointsResource
498
476
 
499
477
  return AsyncEndpointsResource(self)
500
478
 
501
- @cached_property
502
- def hardware(self) -> AsyncHardwareResource:
503
- from .resources.hardware import AsyncHardwareResource
504
-
505
- return AsyncHardwareResource(self)
506
-
507
479
  @cached_property
508
480
  def rerank(self) -> AsyncRerankResource:
509
481
  from .resources.rerank import AsyncRerankResource
@@ -709,24 +681,12 @@ class TogetherWithRawResponse:
709
681
 
710
682
  return ModelsResourceWithRawResponse(self._client.models)
711
683
 
712
- @cached_property
713
- def jobs(self) -> jobs.JobsResourceWithRawResponse:
714
- from .resources.jobs import JobsResourceWithRawResponse
715
-
716
- return JobsResourceWithRawResponse(self._client.jobs)
717
-
718
684
  @cached_property
719
685
  def endpoints(self) -> endpoints.EndpointsResourceWithRawResponse:
720
686
  from .resources.endpoints import EndpointsResourceWithRawResponse
721
687
 
722
688
  return EndpointsResourceWithRawResponse(self._client.endpoints)
723
689
 
724
- @cached_property
725
- def hardware(self) -> hardware.HardwareResourceWithRawResponse:
726
- from .resources.hardware import HardwareResourceWithRawResponse
727
-
728
- return HardwareResourceWithRawResponse(self._client.hardware)
729
-
730
690
  @cached_property
731
691
  def rerank(self) -> rerank.RerankResourceWithRawResponse:
732
692
  from .resources.rerank import RerankResourceWithRawResponse
@@ -818,24 +778,12 @@ class AsyncTogetherWithRawResponse:
818
778
 
819
779
  return AsyncModelsResourceWithRawResponse(self._client.models)
820
780
 
821
- @cached_property
822
- def jobs(self) -> jobs.AsyncJobsResourceWithRawResponse:
823
- from .resources.jobs import AsyncJobsResourceWithRawResponse
824
-
825
- return AsyncJobsResourceWithRawResponse(self._client.jobs)
826
-
827
781
  @cached_property
828
782
  def endpoints(self) -> endpoints.AsyncEndpointsResourceWithRawResponse:
829
783
  from .resources.endpoints import AsyncEndpointsResourceWithRawResponse
830
784
 
831
785
  return AsyncEndpointsResourceWithRawResponse(self._client.endpoints)
832
786
 
833
- @cached_property
834
- def hardware(self) -> hardware.AsyncHardwareResourceWithRawResponse:
835
- from .resources.hardware import AsyncHardwareResourceWithRawResponse
836
-
837
- return AsyncHardwareResourceWithRawResponse(self._client.hardware)
838
-
839
787
  @cached_property
840
788
  def rerank(self) -> rerank.AsyncRerankResourceWithRawResponse:
841
789
  from .resources.rerank import AsyncRerankResourceWithRawResponse
@@ -927,24 +875,12 @@ class TogetherWithStreamedResponse:
927
875
 
928
876
  return ModelsResourceWithStreamingResponse(self._client.models)
929
877
 
930
- @cached_property
931
- def jobs(self) -> jobs.JobsResourceWithStreamingResponse:
932
- from .resources.jobs import JobsResourceWithStreamingResponse
933
-
934
- return JobsResourceWithStreamingResponse(self._client.jobs)
935
-
936
878
  @cached_property
937
879
  def endpoints(self) -> endpoints.EndpointsResourceWithStreamingResponse:
938
880
  from .resources.endpoints import EndpointsResourceWithStreamingResponse
939
881
 
940
882
  return EndpointsResourceWithStreamingResponse(self._client.endpoints)
941
883
 
942
- @cached_property
943
- def hardware(self) -> hardware.HardwareResourceWithStreamingResponse:
944
- from .resources.hardware import HardwareResourceWithStreamingResponse
945
-
946
- return HardwareResourceWithStreamingResponse(self._client.hardware)
947
-
948
884
  @cached_property
949
885
  def rerank(self) -> rerank.RerankResourceWithStreamingResponse:
950
886
  from .resources.rerank import RerankResourceWithStreamingResponse
@@ -1036,24 +972,12 @@ class AsyncTogetherWithStreamedResponse:
1036
972
 
1037
973
  return AsyncModelsResourceWithStreamingResponse(self._client.models)
1038
974
 
1039
- @cached_property
1040
- def jobs(self) -> jobs.AsyncJobsResourceWithStreamingResponse:
1041
- from .resources.jobs import AsyncJobsResourceWithStreamingResponse
1042
-
1043
- return AsyncJobsResourceWithStreamingResponse(self._client.jobs)
1044
-
1045
975
  @cached_property
1046
976
  def endpoints(self) -> endpoints.AsyncEndpointsResourceWithStreamingResponse:
1047
977
  from .resources.endpoints import AsyncEndpointsResourceWithStreamingResponse
1048
978
 
1049
979
  return AsyncEndpointsResourceWithStreamingResponse(self._client.endpoints)
1050
980
 
1051
- @cached_property
1052
- def hardware(self) -> hardware.AsyncHardwareResourceWithStreamingResponse:
1053
- from .resources.hardware import AsyncHardwareResourceWithStreamingResponse
1054
-
1055
- return AsyncHardwareResourceWithStreamingResponse(self._client.hardware)
1056
-
1057
981
  @cached_property
1058
982
  def rerank(self) -> rerank.AsyncRerankResourceWithStreamingResponse:
1059
983
  from .resources.rerank import AsyncRerankResourceWithStreamingResponse
together/_compat.py CHANGED
@@ -139,6 +139,7 @@ def model_dump(
139
139
  exclude_defaults: bool = False,
140
140
  warnings: bool = True,
141
141
  mode: Literal["json", "python"] = "python",
142
+ by_alias: bool | None = None,
142
143
  ) -> dict[str, Any]:
143
144
  if (not PYDANTIC_V1) or hasattr(model, "model_dump"):
144
145
  return model.model_dump(
@@ -148,13 +149,12 @@ def model_dump(
148
149
  exclude_defaults=exclude_defaults,
149
150
  # warnings are not supported in Pydantic v1
150
151
  warnings=True if PYDANTIC_V1 else warnings,
152
+ by_alias=by_alias,
151
153
  )
152
154
  return cast(
153
155
  "dict[str, Any]",
154
156
  model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
155
- exclude=exclude,
156
- exclude_unset=exclude_unset,
157
- exclude_defaults=exclude_defaults,
157
+ exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias)
158
158
  ),
159
159
  )
160
160
 
@@ -0,0 +1,35 @@
1
+ import json
2
+ from typing import Any
3
+ from datetime import datetime
4
+ from typing_extensions import override
5
+
6
+ import pydantic
7
+
8
+ from .._compat import model_dump
9
+
10
+
11
+ def openapi_dumps(obj: Any) -> bytes:
12
+ """
13
+ Serialize an object to UTF-8 encoded JSON bytes.
14
+
15
+ Extends the standard json.dumps with support for additional types
16
+ commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc.
17
+ """
18
+ return json.dumps(
19
+ obj,
20
+ cls=_CustomEncoder,
21
+ # Uses the same defaults as httpx's JSON serialization
22
+ ensure_ascii=False,
23
+ separators=(",", ":"),
24
+ allow_nan=False,
25
+ ).encode()
26
+
27
+
28
+ class _CustomEncoder(json.JSONEncoder):
29
+ @override
30
+ def default(self, o: Any) -> Any:
31
+ if isinstance(o, datetime):
32
+ return o.isoformat()
33
+ if isinstance(o, pydantic.BaseModel):
34
+ return model_dump(o, exclude_unset=True, mode="json", by_alias=True)
35
+ return super().default(o)
together/_version.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2
2
 
3
3
  __title__ = "together"
4
- __version__ = "2.0.0-alpha.17" # x-release-please-version
4
+ __version__ = "2.0.0-alpha.19" # x-release-please-version
@@ -1,5 +1,6 @@
1
1
  import click
2
2
 
3
+ from together.lib.cli.api.beta.jig import jig
3
4
  from together.lib.cli.api.beta.clusters import clusters
4
5
 
5
6
 
@@ -10,3 +11,4 @@ def beta() -> None:
10
11
 
11
12
 
12
13
  beta.add_command(clusters)
14
+ beta.add_command(jig)
@@ -0,0 +1,52 @@
1
+ """Jig CLI - deployment tool for Together AI."""
2
+
3
+ import click
4
+
5
+ from together.lib.cli.api.beta.jig.jig import (
6
+ init,
7
+ logs,
8
+ push,
9
+ build,
10
+ deploy,
11
+ status,
12
+ endpoint,
13
+ submit,
14
+ destroy,
15
+ dockerfile,
16
+ job_status,
17
+ queue_status,
18
+ list_deployments,
19
+ )
20
+ from together.lib.cli.api.beta.jig.secrets import secrets
21
+ from together.lib.cli.api.beta.jig.volumes import volumes
22
+
23
+
24
+ @click.group()
25
+ @click.pass_context
26
+ def jig(ctx: click.Context) -> None:
27
+ """Jig commands - deploy and manage containers"""
28
+ pass
29
+
30
+
31
+ def add_commands(parent: click.Group):
32
+ # Add subgroups
33
+ parent.add_command(secrets)
34
+ parent.add_command(volumes)
35
+
36
+ # Add main commands
37
+ parent.add_command(init)
38
+ parent.add_command(dockerfile)
39
+ parent.add_command(build)
40
+ parent.add_command(push)
41
+ parent.add_command(deploy)
42
+ parent.add_command(status)
43
+ parent.add_command(endpoint)
44
+ parent.add_command(logs)
45
+ parent.add_command(destroy)
46
+ parent.add_command(submit)
47
+ parent.add_command(job_status)
48
+ parent.add_command(queue_status)
49
+ parent.add_command(list_deployments)
50
+
51
+
52
+ add_commands(jig)
@@ -0,0 +1,170 @@
1
+ """Configuration and state management for jig CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ import json
8
+ from typing import TYPE_CHECKING, Any, Optional
9
+ from pathlib import Path
10
+ from dataclasses import field, asdict, dataclass
11
+
12
+ import click
13
+
14
+ if TYPE_CHECKING:
15
+ import tomli as tomllib
16
+ else:
17
+ try:
18
+ import tomllib
19
+ except ImportError:
20
+ import tomli as tomllib
21
+
22
+ # --- Environment Configuration ---
23
+
24
+ DEBUG = os.getenv("TOGETHER_DEBUG", "").strip()[:1] in ("y", "1", "t")
25
+
26
+ UPLOAD_CONCURRENCY_LIMIT = int(os.getenv("TOGETHER_UPLOAD_CONCURRENCY", "15"))
27
+ MULTIPART_CHUNK_SIZE_MB = int(os.getenv("TOGETHER_MULTIPART_CHUNK_SIZE_MB", "20"))
28
+ MULTIPART_THRESHOLD_MB = int(os.getenv("TOGETHER_MULTIPART_THRESHOLD_MB", "100"))
29
+ MAX_UPLOAD_RETRIES = 3
30
+
31
+ # Warmup configuration (for torch compile cache)
32
+ WARMUP_ENV_NAME = os.getenv("WARMUP_ENV_NAME", "TORCHINDUCTOR_CACHE_DIR")
33
+ WARMUP_DEST = os.getenv("WARMUP_DEST", "torch_cache")
34
+
35
+
36
+ # --- Configuration Dataclasses ---
37
+
38
+
39
+ @dataclass
40
+ class ImageConfig:
41
+ """Container image configuration from pyproject.toml"""
42
+
43
+ python_version: str = "3.11"
44
+ system_packages: list[str] = field(default_factory=list[str])
45
+ environment: dict[str, str] = field(default_factory=dict[str, str])
46
+ run: list[str] = field(default_factory=list[str])
47
+ cmd: str = "python app.py"
48
+ copy: list[str] = field(default_factory=list[str])
49
+ auto_include_git: bool = False
50
+
51
+ @classmethod
52
+ def from_dict(cls, data: dict[str, Any]) -> ImageConfig:
53
+ return cls(**{k: v for k, v in data.items() if k in cls.__annotations__})
54
+
55
+
56
+ @dataclass
57
+ class DeployConfig:
58
+ """Deployment configuration"""
59
+
60
+ description: str = ""
61
+ gpu_type: str = "h100-80gb"
62
+ gpu_count: int = 1
63
+ cpu: float = 1
64
+ memory: float = 8
65
+ storage: int = 100
66
+ min_replicas: int = 1
67
+ max_replicas: int = 1
68
+ port: int = 8000
69
+ environment_variables: dict[str, str] = field(default_factory=dict[str, str])
70
+ command: Optional[list[str]] = None
71
+ autoscaling: dict[str, str] = field(default_factory=dict[str, str])
72
+ health_check_path: str = "/health"
73
+ termination_grace_period_seconds: int = 300
74
+
75
+ @classmethod
76
+ def from_dict(cls, data: dict[str, Any]) -> DeployConfig:
77
+ return cls(**{k: v for k, v in data.items() if k in cls.__annotations__})
78
+
79
+
80
+ @dataclass
81
+ class Config:
82
+ """Main configuration from jig.toml or pyproject.toml"""
83
+
84
+ model_name: str = ""
85
+ dockerfile: str = "Dockerfile"
86
+ image: ImageConfig = field(default_factory=ImageConfig)
87
+ deploy: DeployConfig = field(default_factory=DeployConfig)
88
+ _path: Path = field(default_factory=lambda: Path("pyproject.toml"))
89
+
90
+ @classmethod
91
+ def find(cls, config_path: Optional[str] = None, init: bool = False) -> Config:
92
+ """Find specified config_path, pyproject.toml, or jig.toml"""
93
+ if config_path:
94
+ found_path = Path(config_path)
95
+ if not found_path.exists():
96
+ click.echo(f"ERROR: Configuration file not found: {config_path}", err=True)
97
+ sys.exit(1)
98
+ return cls.load(tomllib.load(found_path.open("rb")), found_path)
99
+
100
+ if (jigfile := Path("jig.toml")).exists():
101
+ return cls.load(tomllib.load(jigfile.open("rb")), jigfile)
102
+
103
+ if (pyproject_path := Path("pyproject.toml")).exists():
104
+ data = tomllib.load(pyproject_path.open("rb"))
105
+ if "tool" in data and "jig" in data["tool"]:
106
+ return cls.load(data, pyproject_path)
107
+
108
+ if init:
109
+ return cls()
110
+ click.echo(
111
+ "ERROR: No pyproject.toml or jig.toml found, use --config to specify a config path.",
112
+ err=True,
113
+ )
114
+ sys.exit(1)
115
+
116
+ @classmethod
117
+ def load(cls, data: dict[str, Any], path: Path) -> Config:
118
+ """Load configuration from parsed TOML data"""
119
+ is_pyproject = path.name == "pyproject.toml"
120
+
121
+ jig_config = data.get("tool", {}).get("jig", {}) if is_pyproject else data
122
+
123
+ name = jig_config.get("name")
124
+ if name is None:
125
+ if is_pyproject:
126
+ name = data.get("project", {}).get("name", "")
127
+ else:
128
+ name = path.resolve().parent.name
129
+ click.echo(f"\N{PACKAGE} Name not set in config file or pyproject.toml - defaulting to {name}")
130
+
131
+ if autoscaling := jig_config.get("autoscaling", {}):
132
+ autoscaling["model"] = name
133
+ jig_config["deploy"]["autoscaling"] = autoscaling
134
+
135
+ return cls(
136
+ image=ImageConfig.from_dict(jig_config.get("image", {})),
137
+ deploy=DeployConfig.from_dict(jig_config.get("deploy", {})),
138
+ dockerfile=jig_config.get("dockerfile", "Dockerfile"),
139
+ model_name=name,
140
+ _path=path,
141
+ )
142
+
143
+
144
+ # --- State Management ---
145
+
146
+
147
+ @dataclass
148
+ class State:
149
+ """Persistent state stored in .jig.json"""
150
+
151
+ _config_dir: Path
152
+ registry_base_path: str = ""
153
+ secrets: dict[str, str] = field(default_factory=dict[str, str])
154
+ volumes: dict[str, str] = field(default_factory=dict[str, str])
155
+
156
+ @classmethod
157
+ def load(cls, config_dir: Path) -> State:
158
+ path = config_dir / ".jig.json"
159
+ try:
160
+ with open(path) as f:
161
+ data = {k: v for k, v in json.load(f).items() if k in cls.__annotations__ and not k.startswith("_")}
162
+ return cls(_config_dir=config_dir, **data)
163
+ except FileNotFoundError:
164
+ return cls(_config_dir=config_dir)
165
+
166
+ def save(self) -> None:
167
+ path = self._config_dir / ".jig.json"
168
+ data = {k: v for k, v in asdict(self).items() if not k.startswith("_")}
169
+ with open(path, "w") as f:
170
+ json.dump(data, f, indent=2)