From 324a2d73d9b83b273b624ad813c886a5f49756b3 Mon Sep 17 00:00:00 2001 From: Michael Stolarz <146425971+SystemSculpt@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:50:11 -0700 Subject: [PATCH 1/5] Add sandbox image build sizes to the management API spec Carries build_memory and build_volume on the image push request, and upload_headers on its response, from the server's spec change, and regenerates the client. The spec is patched rather than downloaded, since the live spec does not have the change yet. --- baseten/client/managementapi/_models.py | 24 +++++++++++++++++++ scripts/apigen/specs/management.json | 31 ++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/baseten/client/managementapi/_models.py b/baseten/client/managementapi/_models.py index e662f5e..93161e0 100644 --- a/baseten/client/managementapi/_models.py +++ b/baseten/client/managementapi/_models.py @@ -9411,6 +9411,24 @@ class PushSandboxImageRequest(BaseModel): ], ), ] = None + build_memory: Annotated[ + int | None, + Field( + description="Memory in megabytes of the temporary environment that builds the source archive or imports the registry image. Omit it to use the default size. Does not change the memory of sandboxes created from the image.", + examples=[16384], + ge=1, + le=32768, + ), + ] = None + build_volume: Annotated[ + int | None, + Field( + description="Scratch disk in megabytes attached to the temporary build environment for intermediate layers. Set it for large images that run out of space while building or importing. 0 builds in memory, which is the default. Teams with HIPAA compliance enabled accept only 0.", + examples=[32768], + ge=0, + le=131072, + ), + ] = None class PushSandboxImageResponse(BaseModel): @@ -9433,6 +9451,12 @@ class PushSandboxImageResponse(BaseModel): ], ), ] = None + upload_headers: Annotated[ + dict[str, str] | None, + Field( + description="Headers the source archive upload must send, in addition to Content-Type, with exactly these names and values. Present only with upload_url, when build_memory or build_volume was set. upload_url is signed for these headers, so storage rejects an upload that omits or changes them. Treat names and values as opaque." + ), + ] = None image: Annotated[ str | None, Field( diff --git a/scripts/apigen/specs/management.json b/scripts/apigen/specs/management.json index a7ea40e..f28ea1a 100644 --- a/scripts/apigen/specs/management.json +++ b/scripts/apigen/specs/management.json @@ -11342,7 +11342,7 @@ ], "operationId": "PushImage", "summary": "Push a sandbox image", - "description": "With image supplied, import the registry image asynchronously. Otherwise return an upload URL for a ZIP source archive containing its Dockerfile and build context. The uploaded ZIP archive must not exceed 5 GB. Processing starts after upload. No sandbox is created. Poll the image until BUILT or FAILED.", + "description": "With image supplied, import the registry image asynchronously. Otherwise return an upload URL for a ZIP source archive containing its Dockerfile and build context. The uploaded ZIP archive must not exceed 5 GB. Processing starts after upload. No sandbox is created. Poll the image until BUILT or FAILED. Set build_memory and build_volume to size the temporary environment that builds or imports the image; for a source archive, upload with the returned upload_headers.", "tags": [ "Images" ], @@ -37054,6 +37054,20 @@ "description": "Optional serialized registry authentication configuration for importing a private image. Used only when image is supplied; never returned.", "writeOnly": true, "example": "{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"YjEwLXJldmlldzpkZW1vLW5vdC1hLXZhbGlkLXJlZ2lzdHJ5LXRva2Vu\"}}}" + }, + "build_memory": { + "type": "integer", + "description": "Memory in megabytes of the temporary environment that builds the source archive or imports the registry image. Omit it to use the default size. Does not change the memory of sandboxes created from the image.", + "minimum": 1, + "maximum": 32768, + "example": 16384 + }, + "build_volume": { + "type": "integer", + "description": "Scratch disk in megabytes attached to the temporary build environment for intermediate layers. Set it for large images that run out of space while building or importing. 0 builds in memory, which is the default. Teams with HIPAA compliance enabled accept only 0.", + "minimum": 0, + "maximum": 131072, + "example": 32768 } }, "required": [ @@ -37062,7 +37076,9 @@ "example": { "name": "base-image", "image": "docker.io/b10/base-image:latest", - "docker_config": "{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"YjEwLXJldmlldzpkZW1vLW5vdC1hLXZhbGlkLXJlZ2lzdHJ5LXRva2Vu\"}}}" + "docker_config": "{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"YjEwLXJldmlldzpkZW1vLW5vdC1hLXZhbGlkLXJlZ2lzdHJ5LXRva2Vu\"}}}", + "build_memory": 16384, + "build_volume": 32768 } }, "PushSandboxImageResponseV1": { @@ -37089,6 +37105,13 @@ "format": "uri", "example": "https://uploads.b10.run/images/base-image/20260916212658/source.zip?expires=2026-09-16T22%3A26%3A58Z&signature=demo-not-a-valid-upload-signature" }, + "upload_headers": { + "type": "object", + "description": "Headers the source archive upload must send, in addition to Content-Type, with exactly these names and values. Present only with upload_url, when build_memory or build_volume was set. upload_url is signed for these headers, so storage rejects an upload that omits or changes them. Treat names and values as opaque.", + "additionalProperties": { + "type": "string" + } + }, "image": { "type": "string", "description": "Registered image reference including its tag, when available. Tags are assigned by the service; GET /sandboxes/images/{image_name} returns the available tags once processing completes.", @@ -38415,7 +38438,9 @@ "value": { "name": "base-image", "image": "docker.io/b10/base-image:latest", - "docker_config": "{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"YjEwLXJldmlldzpkZW1vLW5vdC1hLXZhbGlkLXJlZ2lzdHJ5LXRva2Vu\"}}}" + "docker_config": "{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"YjEwLXJldmlldzpkZW1vLW5vdC1hLXZhbGlkLXJlZ2lzdHJ5LXRva2Vu\"}}}", + "build_memory": 16384, + "build_volume": 32768 }, "description": "Shows every request field. docker_config is serialized registry configuration with a deliberately invalid demonstration credential." }, From bc5438bae6cb76bf27d815edea114375ccd04a11 Mon Sep 17 00:00:00 2001 From: Michael Stolarz <146425971+SystemSculpt@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:52:36 -0700 Subject: [PATCH 2/5] Add build sizes to sandbox image pushes ImageClient.push and AsyncImageClient.push take build_memory and build_volume, in megabytes, for every source, and send them only when set, so a build_volume of 0 is sent. The build context upload sends the upload_headers a push returns, which its upload URL is signed for, and nothing more when there are none. --- baseten/sandbox/_images.py | 121 +++++++++++++++++++++++++---- tests/sandbox/test_images.py | 145 ++++++++++++++++++++++++++++++++++- 2 files changed, 251 insertions(+), 15 deletions(-) diff --git a/baseten/sandbox/_images.py b/baseten/sandbox/_images.py index 1bf5cc2..c7f56b1 100644 --- a/baseten/sandbox/_images.py +++ b/baseten/sandbox/_images.py @@ -291,6 +291,8 @@ def push( name: str, builder: ImageBuilder, temp_file: bool | None = ..., + build_memory: int | None = ..., + build_volume: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -303,6 +305,8 @@ def push( name: str, registry_image: str, docker_config: str | None = ..., + build_memory: int | None = ..., + build_volume: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -314,6 +318,8 @@ def push( *, name: str, zip: bytes, + build_memory: int | None = ..., + build_volume: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -328,6 +334,8 @@ def push( ignore_file_processor: ImageIgnoreFileProcessor | None = ..., default_ignore_file: ImageIgnoreFileFunc | None = ..., temp_file: bool | None = ..., + build_memory: int | None = ..., + build_volume: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -339,6 +347,8 @@ def push( *, name: str, files: Mapping[str, str | bytes], + build_memory: int | None = ..., + build_volume: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -357,6 +367,8 @@ def push( default_ignore_file: ImageIgnoreFileFunc | None = None, files: Mapping[str, str | bytes] | None = None, temp_file: bool | None = None, + build_memory: int | None = None, + build_volume: int | None = None, wait: bool = True, timeout: timedelta | None = _DEFAULT_WAIT_TIMEOUT, poll_interval: timedelta = _DEFAULT_POLL_INTERVAL, @@ -408,6 +420,15 @@ def push( temp_file: Whether to zip a *directory* or a builder with local files into a temporary file, rather than in memory. Defaults to true. + build_memory: Memory in megabytes, from 1 to 32768, of the + temporary environment that builds the build context or + imports *registry_image*. ``None`` uses the default size. Does + not change the memory of sandboxes created from the image. + build_volume: Scratch disk in megabytes, from 0 to 131072, + attached to the build environment for intermediate layers. Set + it for a large image that runs out of space while building or + importing. 0, the default, builds in memory. Teams with HIPAA + compliance enabled accept only 0. wait: Whether to return only once the image is ready to use. timeout: How long to wait for the image. ``None`` waits with no limit. Processing continues regardless. @@ -435,10 +456,21 @@ def push( params=baseten.client.managementapi.PushImageParams( **set_fields(team_id=self._context.team_id) ), - request=_push_request(name, registry_image, docker_config), + request=_push_request( + name, + registry_image=registry_image, + docker_config=docker_config, + build_memory=build_memory, + build_volume=build_volume, + ), ) if archive is not None: - self._upload(name, _upload_url(name, response), archive) + self._upload( + name, + _upload_url(name, response), + response.upload_headers, + archive, + ) finally: if archive is not None: archive.file.close() @@ -690,7 +722,13 @@ def _source_archive( return _zip_archive(files_zip_entries(files), False) return None - def _upload(self, name: str, url: str, archive: _Archive) -> None: + def _upload( + self, + name: str, + url: str, + signed_headers: Mapping[str, str] | None, + archive: _Archive, + ) -> None: """Put a build context to the signed storage URL a push returned.""" # The URL is signed for storage, so none of the API's or the HTTP # client's headers, cookies, or credentials go with it: the request is @@ -698,7 +736,10 @@ def _upload(self, name: str, url: str, archive: _Archive) -> None: # length. response = self._context.http_client.send( httpx.Request( - "PUT", url, content=archive.file, headers=_upload_headers(archive) + "PUT", + url, + content=archive.file, + headers=_upload_headers(archive, signed_headers), ), auth=None, ) @@ -767,6 +808,8 @@ async def push( name: str, builder: ImageBuilder, temp_file: bool | None = ..., + build_memory: int | None = ..., + build_volume: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -779,6 +822,8 @@ async def push( name: str, registry_image: str, docker_config: str | None = ..., + build_memory: int | None = ..., + build_volume: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -790,6 +835,8 @@ async def push( *, name: str, zip: bytes, + build_memory: int | None = ..., + build_volume: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -804,6 +851,8 @@ async def push( ignore_file_processor: AsyncImageIgnoreFileProcessor | None = ..., default_ignore_file: AsyncImageIgnoreFileFunc | None = ..., temp_file: bool | None = ..., + build_memory: int | None = ..., + build_volume: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -815,6 +864,8 @@ async def push( *, name: str, files: Mapping[str, str | bytes], + build_memory: int | None = ..., + build_volume: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -833,6 +884,8 @@ async def push( default_ignore_file: AsyncImageIgnoreFileFunc | None = None, files: Mapping[str, str | bytes] | None = None, temp_file: bool | None = None, + build_memory: int | None = None, + build_volume: int | None = None, wait: bool = True, timeout: timedelta | None = _DEFAULT_WAIT_TIMEOUT, poll_interval: timedelta = _DEFAULT_POLL_INTERVAL, @@ -859,10 +912,21 @@ async def push( params=baseten.client.managementapi.PushImageParams( **set_fields(team_id=self._context.team_id) ), - request=_push_request(name, registry_image, docker_config), + request=_push_request( + name, + registry_image=registry_image, + docker_config=docker_config, + build_memory=build_memory, + build_volume=build_volume, + ), ) if archive is not None: - await self._upload(name, _upload_url(name, response), archive) + await self._upload( + name, + _upload_url(name, response), + response.upload_headers, + archive, + ) finally: if archive is not None: archive.file.close() @@ -1068,7 +1132,13 @@ async def _source_archive( ) return None - async def _upload(self, name: str, url: str, archive: _Archive) -> None: + async def _upload( + self, + name: str, + url: str, + signed_headers: Mapping[str, str] | None, + archive: _Archive, + ) -> None: """Put a build context to the signed storage URL a push returned.""" # Read on a worker thread, since the archive may be a file on disk. @@ -1081,7 +1151,10 @@ async def chunks() -> AsyncIterator[bytes]: # Built apart from the HTTP client, as in ImageClient._upload. response = await self._context.http_client.send( httpx.Request( - "PUT", url, content=chunks(), headers=_upload_headers(archive) + "PUT", + url, + content=chunks(), + headers=_upload_headers(archive, signed_headers), ), auth=None, ) @@ -1198,12 +1271,23 @@ def _zip_archive(entries: list[ZipEntry], to_file: bool) -> _Archive: def _push_request( - name: str, registry_image: str | None, docker_config: str | None + name: str, + *, + registry_image: str | None, + docker_config: str | None, + build_memory: int | None, + build_volume: int | None, ) -> baseten.client.managementapi.PushSandboxImageRequest: """Build the request that pushes an image.""" + # A build_volume of 0 is sent, since only None is left out. return baseten.client.managementapi.PushSandboxImageRequest( name=name, - **set_fields(image=registry_image, docker_config=docker_config), + **set_fields( + image=registry_image, + docker_config=docker_config, + build_memory=build_memory, + build_volume=build_volume, + ), ) @@ -1216,9 +1300,20 @@ def _upload_url( return str(response.upload_url) -def _upload_headers(archive: _Archive) -> dict[str, str]: - """Give the headers of a build context upload.""" - return {"Content-Type": "application/zip", "Content-Length": str(archive.size)} +def _upload_headers( + archive: _Archive, signed_headers: Mapping[str, str] | None +) -> dict[str, str]: + """Give the headers of a build context upload. + + ``signed_headers`` are the ones a push returned with its upload URL, which + is signed for them, so storage rejects an upload that leaves one out or + changes it. They go as given, and are opaque. + """ + return { + "Content-Type": "application/zip", + "Content-Length": str(archive.size), + **(signed_headers or {}), + } def _pushed_info( diff --git a/tests/sandbox/test_images.py b/tests/sandbox/test_images.py index 287209f..023c629 100644 --- a/tests/sandbox/test_images.py +++ b/tests/sandbox/test_images.py @@ -36,6 +36,9 @@ _UPLOAD_URL = "https://storage.test/upload?signature=abc" +# Opaque headers a push returns for the upload URL it signed for them. +_SIGNED_HEADERS = {"X-Signed-Memory": "2048", "x-signed-volume": "8192 MB"} + _NO_WAIT = timedelta(0) _Reply = httpx.Response | Exception | dict[str, Any] @@ -124,12 +127,18 @@ def _image(status: str = "BUILT", **fields: Any) -> dict[str, Any]: } -def _pushing(api: _Api, *statuses: str) -> None: +def _pushing(api: _Api, *statuses: str, **response: Any) -> None: api.route( "POST", "/v1/sandboxes/images", httpx.Response( - 202, json={"name": "img", "status": "UPLOADING", "upload_url": _UPLOAD_URL} + 202, + json={ + "name": "img", + "status": "UPLOADING", + "upload_url": _UPLOAD_URL, + **response, + }, ), ) api.route("PUT", "/upload", httpx.Response(200)) @@ -143,6 +152,11 @@ def _zip_names(request: httpx.Request) -> dict[str, bytes]: return {name: archive.read(name) for name in archive.namelist()} +def _upload_header_names(request: httpx.Request) -> set[str]: + # Host is set by httpx for every request. + return set(request.headers.keys()) - {"host"} + + def _error(status: int) -> httpx.Response: return httpx.Response(status, json={"error": "NOPE", "message": "nope"}) @@ -340,6 +354,67 @@ def test_pushes_a_builder_as_a_zip_of_its_dockerfile_and_files(self) -> None: "hello.txt": b"hello", } + def test_sends_build_sizes_and_uploads_with_the_returned_upload_headers( + self, + ) -> None: + api = _Api() + _pushing(api, "BUILT", upload_headers=_SIGNED_HEADERS) + api.client().images.push( + name="img", + builder=ImageBuilder.from_registry("debian:bookworm-slim"), + build_memory=2048, + build_volume=8192, + ) + assert json.loads(api.requests[0].content) == { + "name": "img", + "build_memory": 2048, + "build_volume": 8192, + } + assert _upload_header_names(api.upload()) == { + "content-type", + "content-length", + *(name.lower() for name in _SIGNED_HEADERS), + } + for name, value in _SIGNED_HEADERS.items(): + assert api.upload().headers[name] == value + + def test_sends_a_build_volume_of_0_and_uploads_only_its_own_headers( + self, + ) -> None: + api = _Api() + _pushing(api, "BUILT") + api.client().images.push(name="img", files={"Dockerfile": "x"}, build_volume=0) + assert json.loads(api.requests[0].content) == { + "name": "img", + "build_volume": 0, + } + assert _upload_header_names(api.upload()) == { + "content-type", + "content-length", + } + + def test_imports_a_registry_image_with_build_sizes(self) -> None: + api = _Api() + api.route( + "POST", + "/v1/sandboxes/images", + httpx.Response(202, json={"name": "img", "status": "BUILDING"}), + ) + api.route("GET", "/v1/sandboxes/images/img", _image("BUILT")) + api.client().images.push( + name="img", + registry_image="docker.io/library/python:3", + build_memory=2048, + build_volume=8192, + ) + assert json.loads(api.requests[0].content) == { + "name": "img", + "image": "docker.io/library/python:3", + "build_memory": 2048, + "build_volume": 8192, + } + assert api.paths()[1:] == ["GET /v1/sandboxes/images/img"] + class TestImageClientPushDirectory: def _directory(self, root: Path) -> None: @@ -718,6 +793,72 @@ async def ignore(candidate: ImageIgnoreFileOptions) -> bool: assert upload.headers["Content-Length"] == str(len(upload.content)) assert "Authorization" not in upload.headers + @pytest.mark.asyncio + async def test_sends_build_sizes_and_uploads_with_the_returned_upload_headers( + self, + ) -> None: + api = _Api() + _pushing(api, "BUILT", upload_headers=_SIGNED_HEADERS) + await api.async_client().images.push( + name="img", + files={"Dockerfile": "x"}, + build_memory=2048, + build_volume=8192, + ) + assert json.loads(api.requests[0].content) == { + "name": "img", + "build_memory": 2048, + "build_volume": 8192, + } + assert _upload_header_names(api.upload()) == { + "content-type", + "content-length", + *(name.lower() for name in _SIGNED_HEADERS), + } + for name, value in _SIGNED_HEADERS.items(): + assert api.upload().headers[name] == value + + @pytest.mark.asyncio + async def test_sends_a_build_volume_of_0_and_uploads_only_its_own_headers( + self, + ) -> None: + api = _Api() + _pushing(api, "BUILT") + await api.async_client().images.push( + name="img", files={"Dockerfile": "x"}, build_volume=0 + ) + assert json.loads(api.requests[0].content) == { + "name": "img", + "build_volume": 0, + } + assert _upload_header_names(api.upload()) == { + "content-type", + "content-length", + } + + @pytest.mark.asyncio + async def test_imports_a_registry_image_with_build_sizes(self) -> None: + api = _Api() + api.route( + "POST", + "/v1/sandboxes/images", + httpx.Response(202, json={"name": "img", "status": "BUILDING"}), + ) + api.route("GET", "/v1/sandboxes/images/img", _image("BUILT")) + await api.async_client().images.push( + name="img", + registry_image="docker.io/library/python:3", + build_memory=2048, + build_volume=8192, + ) + assert json.loads(api.requests[0].content) == { + "name": "img", + "image": "docker.io/library/python:3", + "build_memory": 2048, + "build_volume": 8192, + } + assert api.paths()[1:] == ["GET /v1/sandboxes/images/img"] + @pytest.mark.asyncio async def test_times_out_waiting_as_an_image_build_error(self) -> None: api = _Api() From 46d222dfd35fd3039e22756e40898e2ff8ac4e8f Mon Sep 17 00:00:00 2001 From: Michael Stolarz <146425971+SystemSculpt@users.noreply.github.com> Date: Tue, 13 Oct 2026 16:21:44 -0700 Subject: [PATCH 3/5] Name the image push build sizes memory_mb and volume_mb Follows the server's rename: the push endpoint only builds, so no build prefix, and the unit is in the name. --- baseten/client/managementapi/_models.py | 6 +-- baseten/sandbox/_images.py | 70 ++++++++++++------------- scripts/apigen/specs/management.json | 16 +++--- tests/sandbox/test_images.py | 44 ++++++++-------- 4 files changed, 68 insertions(+), 68 deletions(-) diff --git a/baseten/client/managementapi/_models.py b/baseten/client/managementapi/_models.py index 93161e0..8ad48f2 100644 --- a/baseten/client/managementapi/_models.py +++ b/baseten/client/managementapi/_models.py @@ -9411,7 +9411,7 @@ class PushSandboxImageRequest(BaseModel): ], ), ] = None - build_memory: Annotated[ + memory_mb: Annotated[ int | None, Field( description="Memory in megabytes of the temporary environment that builds the source archive or imports the registry image. Omit it to use the default size. Does not change the memory of sandboxes created from the image.", @@ -9420,7 +9420,7 @@ class PushSandboxImageRequest(BaseModel): le=32768, ), ] = None - build_volume: Annotated[ + volume_mb: Annotated[ int | None, Field( description="Scratch disk in megabytes attached to the temporary build environment for intermediate layers. Set it for large images that run out of space while building or importing. 0 builds in memory, which is the default. Teams with HIPAA compliance enabled accept only 0.", @@ -9454,7 +9454,7 @@ class PushSandboxImageResponse(BaseModel): upload_headers: Annotated[ dict[str, str] | None, Field( - description="Headers the source archive upload must send, in addition to Content-Type, with exactly these names and values. Present only with upload_url, when build_memory or build_volume was set. upload_url is signed for these headers, so storage rejects an upload that omits or changes them. Treat names and values as opaque." + description="Headers the source archive upload must send, in addition to Content-Type, with exactly these names and values. Present only with upload_url, when memory_mb or volume_mb was set. upload_url is signed for these headers, so storage rejects an upload that omits or changes them. Treat names and values as opaque." ), ] = None image: Annotated[ diff --git a/baseten/sandbox/_images.py b/baseten/sandbox/_images.py index c7f56b1..aaf4a57 100644 --- a/baseten/sandbox/_images.py +++ b/baseten/sandbox/_images.py @@ -291,8 +291,8 @@ def push( name: str, builder: ImageBuilder, temp_file: bool | None = ..., - build_memory: int | None = ..., - build_volume: int | None = ..., + memory_mb: int | None = ..., + volume_mb: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -305,8 +305,8 @@ def push( name: str, registry_image: str, docker_config: str | None = ..., - build_memory: int | None = ..., - build_volume: int | None = ..., + memory_mb: int | None = ..., + volume_mb: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -318,8 +318,8 @@ def push( *, name: str, zip: bytes, - build_memory: int | None = ..., - build_volume: int | None = ..., + memory_mb: int | None = ..., + volume_mb: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -334,8 +334,8 @@ def push( ignore_file_processor: ImageIgnoreFileProcessor | None = ..., default_ignore_file: ImageIgnoreFileFunc | None = ..., temp_file: bool | None = ..., - build_memory: int | None = ..., - build_volume: int | None = ..., + memory_mb: int | None = ..., + volume_mb: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -347,8 +347,8 @@ def push( *, name: str, files: Mapping[str, str | bytes], - build_memory: int | None = ..., - build_volume: int | None = ..., + memory_mb: int | None = ..., + volume_mb: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -367,8 +367,8 @@ def push( default_ignore_file: ImageIgnoreFileFunc | None = None, files: Mapping[str, str | bytes] | None = None, temp_file: bool | None = None, - build_memory: int | None = None, - build_volume: int | None = None, + memory_mb: int | None = None, + volume_mb: int | None = None, wait: bool = True, timeout: timedelta | None = _DEFAULT_WAIT_TIMEOUT, poll_interval: timedelta = _DEFAULT_POLL_INTERVAL, @@ -420,11 +420,11 @@ def push( temp_file: Whether to zip a *directory* or a builder with local files into a temporary file, rather than in memory. Defaults to true. - build_memory: Memory in megabytes, from 1 to 32768, of the + memory_mb: Memory in megabytes, from 1 to 32768, of the temporary environment that builds the build context or imports *registry_image*. ``None`` uses the default size. Does not change the memory of sandboxes created from the image. - build_volume: Scratch disk in megabytes, from 0 to 131072, + volume_mb: Scratch disk in megabytes, from 0 to 131072, attached to the build environment for intermediate layers. Set it for a large image that runs out of space while building or importing. 0, the default, builds in memory. Teams with HIPAA @@ -460,8 +460,8 @@ def push( name, registry_image=registry_image, docker_config=docker_config, - build_memory=build_memory, - build_volume=build_volume, + memory_mb=memory_mb, + volume_mb=volume_mb, ), ) if archive is not None: @@ -808,8 +808,8 @@ async def push( name: str, builder: ImageBuilder, temp_file: bool | None = ..., - build_memory: int | None = ..., - build_volume: int | None = ..., + memory_mb: int | None = ..., + volume_mb: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -822,8 +822,8 @@ async def push( name: str, registry_image: str, docker_config: str | None = ..., - build_memory: int | None = ..., - build_volume: int | None = ..., + memory_mb: int | None = ..., + volume_mb: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -835,8 +835,8 @@ async def push( *, name: str, zip: bytes, - build_memory: int | None = ..., - build_volume: int | None = ..., + memory_mb: int | None = ..., + volume_mb: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -851,8 +851,8 @@ async def push( ignore_file_processor: AsyncImageIgnoreFileProcessor | None = ..., default_ignore_file: AsyncImageIgnoreFileFunc | None = ..., temp_file: bool | None = ..., - build_memory: int | None = ..., - build_volume: int | None = ..., + memory_mb: int | None = ..., + volume_mb: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -864,8 +864,8 @@ async def push( *, name: str, files: Mapping[str, str | bytes], - build_memory: int | None = ..., - build_volume: int | None = ..., + memory_mb: int | None = ..., + volume_mb: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -884,8 +884,8 @@ async def push( default_ignore_file: AsyncImageIgnoreFileFunc | None = None, files: Mapping[str, str | bytes] | None = None, temp_file: bool | None = None, - build_memory: int | None = None, - build_volume: int | None = None, + memory_mb: int | None = None, + volume_mb: int | None = None, wait: bool = True, timeout: timedelta | None = _DEFAULT_WAIT_TIMEOUT, poll_interval: timedelta = _DEFAULT_POLL_INTERVAL, @@ -916,8 +916,8 @@ async def push( name, registry_image=registry_image, docker_config=docker_config, - build_memory=build_memory, - build_volume=build_volume, + memory_mb=memory_mb, + volume_mb=volume_mb, ), ) if archive is not None: @@ -1275,18 +1275,18 @@ def _push_request( *, registry_image: str | None, docker_config: str | None, - build_memory: int | None, - build_volume: int | None, + memory_mb: int | None, + volume_mb: int | None, ) -> baseten.client.managementapi.PushSandboxImageRequest: """Build the request that pushes an image.""" - # A build_volume of 0 is sent, since only None is left out. + # A volume_mb of 0 is sent, since only None is left out. return baseten.client.managementapi.PushSandboxImageRequest( name=name, **set_fields( image=registry_image, docker_config=docker_config, - build_memory=build_memory, - build_volume=build_volume, + memory_mb=memory_mb, + volume_mb=volume_mb, ), ) diff --git a/scripts/apigen/specs/management.json b/scripts/apigen/specs/management.json index f28ea1a..be206d3 100644 --- a/scripts/apigen/specs/management.json +++ b/scripts/apigen/specs/management.json @@ -11342,7 +11342,7 @@ ], "operationId": "PushImage", "summary": "Push a sandbox image", - "description": "With image supplied, import the registry image asynchronously. Otherwise return an upload URL for a ZIP source archive containing its Dockerfile and build context. The uploaded ZIP archive must not exceed 5 GB. Processing starts after upload. No sandbox is created. Poll the image until BUILT or FAILED. Set build_memory and build_volume to size the temporary environment that builds or imports the image; for a source archive, upload with the returned upload_headers.", + "description": "With image supplied, import the registry image asynchronously. Otherwise return an upload URL for a ZIP source archive containing its Dockerfile and build context. The uploaded ZIP archive must not exceed 5 GB. Processing starts after upload. No sandbox is created. Poll the image until BUILT or FAILED. Set memory_mb and volume_mb to size the temporary environment that builds or imports the image; for a source archive, upload with the returned upload_headers.", "tags": [ "Images" ], @@ -37055,14 +37055,14 @@ "writeOnly": true, "example": "{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"YjEwLXJldmlldzpkZW1vLW5vdC1hLXZhbGlkLXJlZ2lzdHJ5LXRva2Vu\"}}}" }, - "build_memory": { + "memory_mb": { "type": "integer", "description": "Memory in megabytes of the temporary environment that builds the source archive or imports the registry image. Omit it to use the default size. Does not change the memory of sandboxes created from the image.", "minimum": 1, "maximum": 32768, "example": 16384 }, - "build_volume": { + "volume_mb": { "type": "integer", "description": "Scratch disk in megabytes attached to the temporary build environment for intermediate layers. Set it for large images that run out of space while building or importing. 0 builds in memory, which is the default. Teams with HIPAA compliance enabled accept only 0.", "minimum": 0, @@ -37077,8 +37077,8 @@ "name": "base-image", "image": "docker.io/b10/base-image:latest", "docker_config": "{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"YjEwLXJldmlldzpkZW1vLW5vdC1hLXZhbGlkLXJlZ2lzdHJ5LXRva2Vu\"}}}", - "build_memory": 16384, - "build_volume": 32768 + "memory_mb": 16384, + "volume_mb": 32768 } }, "PushSandboxImageResponseV1": { @@ -37107,7 +37107,7 @@ }, "upload_headers": { "type": "object", - "description": "Headers the source archive upload must send, in addition to Content-Type, with exactly these names and values. Present only with upload_url, when build_memory or build_volume was set. upload_url is signed for these headers, so storage rejects an upload that omits or changes them. Treat names and values as opaque.", + "description": "Headers the source archive upload must send, in addition to Content-Type, with exactly these names and values. Present only with upload_url, when memory_mb or volume_mb was set. upload_url is signed for these headers, so storage rejects an upload that omits or changes them. Treat names and values as opaque.", "additionalProperties": { "type": "string" } @@ -38439,8 +38439,8 @@ "name": "base-image", "image": "docker.io/b10/base-image:latest", "docker_config": "{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"YjEwLXJldmlldzpkZW1vLW5vdC1hLXZhbGlkLXJlZ2lzdHJ5LXRva2Vu\"}}}", - "build_memory": 16384, - "build_volume": 32768 + "memory_mb": 16384, + "volume_mb": 32768 }, "description": "Shows every request field. docker_config is serialized registry configuration with a deliberately invalid demonstration credential." }, diff --git a/tests/sandbox/test_images.py b/tests/sandbox/test_images.py index 023c629..66cf649 100644 --- a/tests/sandbox/test_images.py +++ b/tests/sandbox/test_images.py @@ -362,13 +362,13 @@ def test_sends_build_sizes_and_uploads_with_the_returned_upload_headers( api.client().images.push( name="img", builder=ImageBuilder.from_registry("debian:bookworm-slim"), - build_memory=2048, - build_volume=8192, + memory_mb=2048, + volume_mb=8192, ) assert json.loads(api.requests[0].content) == { "name": "img", - "build_memory": 2048, - "build_volume": 8192, + "memory_mb": 2048, + "volume_mb": 8192, } assert _upload_header_names(api.upload()) == { "content-type", @@ -378,15 +378,15 @@ def test_sends_build_sizes_and_uploads_with_the_returned_upload_headers( for name, value in _SIGNED_HEADERS.items(): assert api.upload().headers[name] == value - def test_sends_a_build_volume_of_0_and_uploads_only_its_own_headers( + def test_sends_a_volume_mb_of_0_and_uploads_only_its_own_headers( self, ) -> None: api = _Api() _pushing(api, "BUILT") - api.client().images.push(name="img", files={"Dockerfile": "x"}, build_volume=0) + api.client().images.push(name="img", files={"Dockerfile": "x"}, volume_mb=0) assert json.loads(api.requests[0].content) == { "name": "img", - "build_volume": 0, + "volume_mb": 0, } assert _upload_header_names(api.upload()) == { "content-type", @@ -404,14 +404,14 @@ def test_imports_a_registry_image_with_build_sizes(self) -> None: api.client().images.push( name="img", registry_image="docker.io/library/python:3", - build_memory=2048, - build_volume=8192, + memory_mb=2048, + volume_mb=8192, ) assert json.loads(api.requests[0].content) == { "name": "img", "image": "docker.io/library/python:3", - "build_memory": 2048, - "build_volume": 8192, + "memory_mb": 2048, + "volume_mb": 8192, } assert api.paths()[1:] == ["GET /v1/sandboxes/images/img"] @@ -802,13 +802,13 @@ async def test_sends_build_sizes_and_uploads_with_the_returned_upload_headers( await api.async_client().images.push( name="img", files={"Dockerfile": "x"}, - build_memory=2048, - build_volume=8192, + memory_mb=2048, + volume_mb=8192, ) assert json.loads(api.requests[0].content) == { "name": "img", - "build_memory": 2048, - "build_volume": 8192, + "memory_mb": 2048, + "volume_mb": 8192, } assert _upload_header_names(api.upload()) == { "content-type", @@ -819,17 +819,17 @@ async def test_sends_build_sizes_and_uploads_with_the_returned_upload_headers( assert api.upload().headers[name] == value @pytest.mark.asyncio - async def test_sends_a_build_volume_of_0_and_uploads_only_its_own_headers( + async def test_sends_a_volume_mb_of_0_and_uploads_only_its_own_headers( self, ) -> None: api = _Api() _pushing(api, "BUILT") await api.async_client().images.push( - name="img", files={"Dockerfile": "x"}, build_volume=0 + name="img", files={"Dockerfile": "x"}, volume_mb=0 ) assert json.loads(api.requests[0].content) == { "name": "img", - "build_volume": 0, + "volume_mb": 0, } assert _upload_header_names(api.upload()) == { "content-type", @@ -848,14 +848,14 @@ async def test_imports_a_registry_image_with_build_sizes(self) -> None: await api.async_client().images.push( name="img", registry_image="docker.io/library/python:3", - build_memory=2048, - build_volume=8192, + memory_mb=2048, + volume_mb=8192, ) assert json.loads(api.requests[0].content) == { "name": "img", "image": "docker.io/library/python:3", - "build_memory": 2048, - "build_volume": 8192, + "memory_mb": 2048, + "volume_mb": 8192, } assert api.paths()[1:] == ["GET /v1/sandboxes/images/img"] From 31ca63206267ae861fda62c9289da5a686337f54 Mon Sep 17 00:00:00 2001 From: Michael Stolarz <146425971+SystemSculpt@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:13:56 -0700 Subject: [PATCH 4/5] Name the image push build sizes memory and volume Matches the unitless memory field, in megabytes, that the other sandbox options and API schemas use. images.push's memory_mb and volume_mb become memory and volume, sent as memory and volume. Values stay integers in MB. --- baseten/client/managementapi/_models.py | 6 +-- baseten/sandbox/_images.py | 70 ++++++++++++------------- scripts/apigen/specs/management.json | 16 +++--- tests/sandbox/test_images.py | 44 ++++++++-------- 4 files changed, 68 insertions(+), 68 deletions(-) diff --git a/baseten/client/managementapi/_models.py b/baseten/client/managementapi/_models.py index 8ad48f2..299a298 100644 --- a/baseten/client/managementapi/_models.py +++ b/baseten/client/managementapi/_models.py @@ -9411,7 +9411,7 @@ class PushSandboxImageRequest(BaseModel): ], ), ] = None - memory_mb: Annotated[ + memory: Annotated[ int | None, Field( description="Memory in megabytes of the temporary environment that builds the source archive or imports the registry image. Omit it to use the default size. Does not change the memory of sandboxes created from the image.", @@ -9420,7 +9420,7 @@ class PushSandboxImageRequest(BaseModel): le=32768, ), ] = None - volume_mb: Annotated[ + volume: Annotated[ int | None, Field( description="Scratch disk in megabytes attached to the temporary build environment for intermediate layers. Set it for large images that run out of space while building or importing. 0 builds in memory, which is the default. Teams with HIPAA compliance enabled accept only 0.", @@ -9454,7 +9454,7 @@ class PushSandboxImageResponse(BaseModel): upload_headers: Annotated[ dict[str, str] | None, Field( - description="Headers the source archive upload must send, in addition to Content-Type, with exactly these names and values. Present only with upload_url, when memory_mb or volume_mb was set. upload_url is signed for these headers, so storage rejects an upload that omits or changes them. Treat names and values as opaque." + description="Headers the source archive upload must send, in addition to Content-Type, with exactly these names and values. Present only with upload_url, when memory or volume was set. upload_url is signed for these headers, so storage rejects an upload that omits or changes them. Treat names and values as opaque." ), ] = None image: Annotated[ diff --git a/baseten/sandbox/_images.py b/baseten/sandbox/_images.py index aaf4a57..83f87c0 100644 --- a/baseten/sandbox/_images.py +++ b/baseten/sandbox/_images.py @@ -291,8 +291,8 @@ def push( name: str, builder: ImageBuilder, temp_file: bool | None = ..., - memory_mb: int | None = ..., - volume_mb: int | None = ..., + memory: int | None = ..., + volume: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -305,8 +305,8 @@ def push( name: str, registry_image: str, docker_config: str | None = ..., - memory_mb: int | None = ..., - volume_mb: int | None = ..., + memory: int | None = ..., + volume: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -318,8 +318,8 @@ def push( *, name: str, zip: bytes, - memory_mb: int | None = ..., - volume_mb: int | None = ..., + memory: int | None = ..., + volume: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -334,8 +334,8 @@ def push( ignore_file_processor: ImageIgnoreFileProcessor | None = ..., default_ignore_file: ImageIgnoreFileFunc | None = ..., temp_file: bool | None = ..., - memory_mb: int | None = ..., - volume_mb: int | None = ..., + memory: int | None = ..., + volume: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -347,8 +347,8 @@ def push( *, name: str, files: Mapping[str, str | bytes], - memory_mb: int | None = ..., - volume_mb: int | None = ..., + memory: int | None = ..., + volume: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -367,8 +367,8 @@ def push( default_ignore_file: ImageIgnoreFileFunc | None = None, files: Mapping[str, str | bytes] | None = None, temp_file: bool | None = None, - memory_mb: int | None = None, - volume_mb: int | None = None, + memory: int | None = None, + volume: int | None = None, wait: bool = True, timeout: timedelta | None = _DEFAULT_WAIT_TIMEOUT, poll_interval: timedelta = _DEFAULT_POLL_INTERVAL, @@ -420,11 +420,11 @@ def push( temp_file: Whether to zip a *directory* or a builder with local files into a temporary file, rather than in memory. Defaults to true. - memory_mb: Memory in megabytes, from 1 to 32768, of the + memory: Memory in megabytes, from 1 to 32768, of the temporary environment that builds the build context or imports *registry_image*. ``None`` uses the default size. Does not change the memory of sandboxes created from the image. - volume_mb: Scratch disk in megabytes, from 0 to 131072, + volume: Scratch disk in megabytes, from 0 to 131072, attached to the build environment for intermediate layers. Set it for a large image that runs out of space while building or importing. 0, the default, builds in memory. Teams with HIPAA @@ -460,8 +460,8 @@ def push( name, registry_image=registry_image, docker_config=docker_config, - memory_mb=memory_mb, - volume_mb=volume_mb, + memory=memory, + volume=volume, ), ) if archive is not None: @@ -808,8 +808,8 @@ async def push( name: str, builder: ImageBuilder, temp_file: bool | None = ..., - memory_mb: int | None = ..., - volume_mb: int | None = ..., + memory: int | None = ..., + volume: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -822,8 +822,8 @@ async def push( name: str, registry_image: str, docker_config: str | None = ..., - memory_mb: int | None = ..., - volume_mb: int | None = ..., + memory: int | None = ..., + volume: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -835,8 +835,8 @@ async def push( *, name: str, zip: bytes, - memory_mb: int | None = ..., - volume_mb: int | None = ..., + memory: int | None = ..., + volume: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -851,8 +851,8 @@ async def push( ignore_file_processor: AsyncImageIgnoreFileProcessor | None = ..., default_ignore_file: AsyncImageIgnoreFileFunc | None = ..., temp_file: bool | None = ..., - memory_mb: int | None = ..., - volume_mb: int | None = ..., + memory: int | None = ..., + volume: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -864,8 +864,8 @@ async def push( *, name: str, files: Mapping[str, str | bytes], - memory_mb: int | None = ..., - volume_mb: int | None = ..., + memory: int | None = ..., + volume: int | None = ..., wait: bool = ..., timeout: timedelta | None = ..., poll_interval: timedelta = ..., @@ -884,8 +884,8 @@ async def push( default_ignore_file: AsyncImageIgnoreFileFunc | None = None, files: Mapping[str, str | bytes] | None = None, temp_file: bool | None = None, - memory_mb: int | None = None, - volume_mb: int | None = None, + memory: int | None = None, + volume: int | None = None, wait: bool = True, timeout: timedelta | None = _DEFAULT_WAIT_TIMEOUT, poll_interval: timedelta = _DEFAULT_POLL_INTERVAL, @@ -916,8 +916,8 @@ async def push( name, registry_image=registry_image, docker_config=docker_config, - memory_mb=memory_mb, - volume_mb=volume_mb, + memory=memory, + volume=volume, ), ) if archive is not None: @@ -1275,18 +1275,18 @@ def _push_request( *, registry_image: str | None, docker_config: str | None, - memory_mb: int | None, - volume_mb: int | None, + memory: int | None, + volume: int | None, ) -> baseten.client.managementapi.PushSandboxImageRequest: """Build the request that pushes an image.""" - # A volume_mb of 0 is sent, since only None is left out. + # A volume of 0 is sent, since only None is left out. return baseten.client.managementapi.PushSandboxImageRequest( name=name, **set_fields( image=registry_image, docker_config=docker_config, - memory_mb=memory_mb, - volume_mb=volume_mb, + memory=memory, + volume=volume, ), ) diff --git a/scripts/apigen/specs/management.json b/scripts/apigen/specs/management.json index be206d3..8c3efbd 100644 --- a/scripts/apigen/specs/management.json +++ b/scripts/apigen/specs/management.json @@ -11342,7 +11342,7 @@ ], "operationId": "PushImage", "summary": "Push a sandbox image", - "description": "With image supplied, import the registry image asynchronously. Otherwise return an upload URL for a ZIP source archive containing its Dockerfile and build context. The uploaded ZIP archive must not exceed 5 GB. Processing starts after upload. No sandbox is created. Poll the image until BUILT or FAILED. Set memory_mb and volume_mb to size the temporary environment that builds or imports the image; for a source archive, upload with the returned upload_headers.", + "description": "With image supplied, import the registry image asynchronously. Otherwise return an upload URL for a ZIP source archive containing its Dockerfile and build context. The uploaded ZIP archive must not exceed 5 GB. Processing starts after upload. No sandbox is created. Poll the image until BUILT or FAILED. Set memory and volume to size the temporary environment that builds or imports the image; for a source archive, upload with the returned upload_headers.", "tags": [ "Images" ], @@ -37055,14 +37055,14 @@ "writeOnly": true, "example": "{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"YjEwLXJldmlldzpkZW1vLW5vdC1hLXZhbGlkLXJlZ2lzdHJ5LXRva2Vu\"}}}" }, - "memory_mb": { + "memory": { "type": "integer", "description": "Memory in megabytes of the temporary environment that builds the source archive or imports the registry image. Omit it to use the default size. Does not change the memory of sandboxes created from the image.", "minimum": 1, "maximum": 32768, "example": 16384 }, - "volume_mb": { + "volume": { "type": "integer", "description": "Scratch disk in megabytes attached to the temporary build environment for intermediate layers. Set it for large images that run out of space while building or importing. 0 builds in memory, which is the default. Teams with HIPAA compliance enabled accept only 0.", "minimum": 0, @@ -37077,8 +37077,8 @@ "name": "base-image", "image": "docker.io/b10/base-image:latest", "docker_config": "{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"YjEwLXJldmlldzpkZW1vLW5vdC1hLXZhbGlkLXJlZ2lzdHJ5LXRva2Vu\"}}}", - "memory_mb": 16384, - "volume_mb": 32768 + "memory": 16384, + "volume": 32768 } }, "PushSandboxImageResponseV1": { @@ -37107,7 +37107,7 @@ }, "upload_headers": { "type": "object", - "description": "Headers the source archive upload must send, in addition to Content-Type, with exactly these names and values. Present only with upload_url, when memory_mb or volume_mb was set. upload_url is signed for these headers, so storage rejects an upload that omits or changes them. Treat names and values as opaque.", + "description": "Headers the source archive upload must send, in addition to Content-Type, with exactly these names and values. Present only with upload_url, when memory or volume was set. upload_url is signed for these headers, so storage rejects an upload that omits or changes them. Treat names and values as opaque.", "additionalProperties": { "type": "string" } @@ -38439,8 +38439,8 @@ "name": "base-image", "image": "docker.io/b10/base-image:latest", "docker_config": "{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"YjEwLXJldmlldzpkZW1vLW5vdC1hLXZhbGlkLXJlZ2lzdHJ5LXRva2Vu\"}}}", - "memory_mb": 16384, - "volume_mb": 32768 + "memory": 16384, + "volume": 32768 }, "description": "Shows every request field. docker_config is serialized registry configuration with a deliberately invalid demonstration credential." }, diff --git a/tests/sandbox/test_images.py b/tests/sandbox/test_images.py index 66cf649..a28cb5a 100644 --- a/tests/sandbox/test_images.py +++ b/tests/sandbox/test_images.py @@ -362,13 +362,13 @@ def test_sends_build_sizes_and_uploads_with_the_returned_upload_headers( api.client().images.push( name="img", builder=ImageBuilder.from_registry("debian:bookworm-slim"), - memory_mb=2048, - volume_mb=8192, + memory=2048, + volume=8192, ) assert json.loads(api.requests[0].content) == { "name": "img", - "memory_mb": 2048, - "volume_mb": 8192, + "memory": 2048, + "volume": 8192, } assert _upload_header_names(api.upload()) == { "content-type", @@ -378,15 +378,15 @@ def test_sends_build_sizes_and_uploads_with_the_returned_upload_headers( for name, value in _SIGNED_HEADERS.items(): assert api.upload().headers[name] == value - def test_sends_a_volume_mb_of_0_and_uploads_only_its_own_headers( + def test_sends_a_volume_of_0_and_uploads_only_its_own_headers( self, ) -> None: api = _Api() _pushing(api, "BUILT") - api.client().images.push(name="img", files={"Dockerfile": "x"}, volume_mb=0) + api.client().images.push(name="img", files={"Dockerfile": "x"}, volume=0) assert json.loads(api.requests[0].content) == { "name": "img", - "volume_mb": 0, + "volume": 0, } assert _upload_header_names(api.upload()) == { "content-type", @@ -404,14 +404,14 @@ def test_imports_a_registry_image_with_build_sizes(self) -> None: api.client().images.push( name="img", registry_image="docker.io/library/python:3", - memory_mb=2048, - volume_mb=8192, + memory=2048, + volume=8192, ) assert json.loads(api.requests[0].content) == { "name": "img", "image": "docker.io/library/python:3", - "memory_mb": 2048, - "volume_mb": 8192, + "memory": 2048, + "volume": 8192, } assert api.paths()[1:] == ["GET /v1/sandboxes/images/img"] @@ -802,13 +802,13 @@ async def test_sends_build_sizes_and_uploads_with_the_returned_upload_headers( await api.async_client().images.push( name="img", files={"Dockerfile": "x"}, - memory_mb=2048, - volume_mb=8192, + memory=2048, + volume=8192, ) assert json.loads(api.requests[0].content) == { "name": "img", - "memory_mb": 2048, - "volume_mb": 8192, + "memory": 2048, + "volume": 8192, } assert _upload_header_names(api.upload()) == { "content-type", @@ -819,17 +819,17 @@ async def test_sends_build_sizes_and_uploads_with_the_returned_upload_headers( assert api.upload().headers[name] == value @pytest.mark.asyncio - async def test_sends_a_volume_mb_of_0_and_uploads_only_its_own_headers( + async def test_sends_a_volume_of_0_and_uploads_only_its_own_headers( self, ) -> None: api = _Api() _pushing(api, "BUILT") await api.async_client().images.push( - name="img", files={"Dockerfile": "x"}, volume_mb=0 + name="img", files={"Dockerfile": "x"}, volume=0 ) assert json.loads(api.requests[0].content) == { "name": "img", - "volume_mb": 0, + "volume": 0, } assert _upload_header_names(api.upload()) == { "content-type", @@ -848,14 +848,14 @@ async def test_imports_a_registry_image_with_build_sizes(self) -> None: await api.async_client().images.push( name="img", registry_image="docker.io/library/python:3", - memory_mb=2048, - volume_mb=8192, + memory=2048, + volume=8192, ) assert json.loads(api.requests[0].content) == { "name": "img", "image": "docker.io/library/python:3", - "memory_mb": 2048, - "volume_mb": 8192, + "memory": 2048, + "volume": 8192, } assert api.paths()[1:] == ["GET /v1/sandboxes/images/img"] From f37ee0594954d81902eae1d675e9ce367b5c8a8b Mon Sep 17 00:00:00 2001 From: Chad Retz Date: Thu, 15 Oct 2026 07:44:44 -0500 Subject: [PATCH 5/5] Regenerate from production specs and refine image push build sizes --- baseten/client/managementapi/__init__.py | 120 +- baseten/client/managementapi/_client.py | 416 ++- baseten/client/managementapi/_models.py | 1822 ++++++++---- baseten/client/modelconfig/_models.py | 14 + baseten/sandbox/_images.py | 25 +- scripts/apigen/specs/config.schema.json | 16 + scripts/apigen/specs/management.json | 3326 +++++++++++++++++----- tests/sandbox/test_images.py | 1 - tests/sandbox/test_parity.py | 6 - 9 files changed, 4445 insertions(+), 1301 deletions(-) diff --git a/baseten/client/managementapi/__init__.py b/baseten/client/managementapi/__init__.py index 7273e32..096442a 100644 --- a/baseten/client/managementapi/__init__.py +++ b/baseten/client/managementapi/__init__.py @@ -70,6 +70,9 @@ AuditLogEventUserRoleUpdated, AuditLogEventUserTeamRoleUpdated, AuditLogEventVolumeDeleted, + AuditLogEventVolumeTagDeleted, + AuditLogEventVolumeTagSet, + AuditLogEventVolumeUpdated, AuditLogEventVolumeVersionDeleted, AuditLogEventVolumeVersionRestored, AuditLogEventWebhookSigningSecretCreated, @@ -176,6 +179,8 @@ DeleteSandboxParams, DeleteVolumeRequest, DeleteVolumeResponse, + DeleteVolumeTagRequest, + DeleteVolumeTagResponse, DeleteVolumeVersionRequest, DeleteVolumeVersionResponse, DeployLoopsCheckpointRequest, @@ -204,6 +209,7 @@ EffectiveModelConfig, EffectiveRateLimit, EffectiveUsageLimit, + EgressRestrictions, EmbeddingBenchmarkMetrics, EnablementDetails, Endpoint, @@ -219,7 +225,12 @@ EnvironmentGroupUser, Environments, EnvironmentTombstone, - ErrorRate, + ExpectedSequence, + ExpectedSequence1, + ExpectedSequence2, + ExpectedSequence3, + ExpectedSequence4, + ExpectedSequence5, ExploreCost, ExploreCostValues, ExploreMetadata, @@ -273,8 +284,12 @@ GetModelsDeploymentsLogsParams, GetModelsDeploymentsMetricsParams, GetModelsDeploymentsParams, + GetModelsDeploymentsStartupsParams, + GetModelsDeploymentsStartupsSummaryParams, GetModelsEnvironmentsLogsParams, GetModelsEnvironmentsMetricsParams, + GetModelsEnvironmentsStartupsParams, + GetModelsEnvironmentsStartupsSummaryParams, GetModelsParams, GetRoutesConnectionsParams, GetRoutesParams, @@ -361,6 +376,7 @@ LoopsDeploymentMetrics, LoopsDeploymentNodeMetrics, LoopsDeploymentStatus, + LoopsLoRAConfig, LoopsRun, LoopsRunStatus, LoopsSampler, @@ -370,6 +386,7 @@ MaxScaleDownRate, MaxScaleDownRate1, MaxSeqLen, + MinDurationMs, Model, ModelAPI, ModelApiItem, @@ -404,6 +421,8 @@ PatchLoopsUserConfigResponse, PatchTeamTrainingGpuCapacityRequest, PatchTeamTrainingGpuCapacityResponse, + PatchVolumeRequest, + PatchVolumeResponse, PendingJobAheadAtSubmit, PrepareModelUploadRequest, PrepareModelUploadResponse, @@ -434,14 +453,11 @@ RollingDeployConfig, Route, RouteConnection, - RouteConnectionAnthropic, RouteConnectionConfigAnthropic, RouteConnectionConfigOpenAI, RouteConnectionConfigXAI, - RouteConnectionOpenAI, RouteConnectionsResponse, RouteConnectionTombstone, - RouteConnectionXAI, RouteEffectiveSpendLimit, RouteHarnessDefaults, RouteHarnessModel, @@ -452,20 +468,15 @@ RoutesUsageBucket, RoutesUsageResponse, RoutesUsageResult, - RouteTargetAnthropic, RouteTargetBasetenModelAPI, RouteTargetClassifierModelBased, - RouteTargetConfigAnthropic, - RouteTargetConfigBasetenModelAPI, - RouteTargetConfigClassifierModelBased, - RouteTargetConfigOpenAI, - RouteTargetConfigXAI, - RouteTargetOpenAI, - RouteTargetXAI, + RouteTargetConnection, RouteTeamSettings, RouteTeamSpendLimitSetting, + RouteTeamUserSettings, + RouteTeamUserSettingsList, RouteTombstone, - RouteUserSettings, + RouteUserTeamSettingsList, SamplerNumReplicas, Sandbox, SandboxApiPagination, @@ -475,7 +486,6 @@ SandboxImage, SandboxImageBuildLog, SandboxImageBuildLogsResponse, - SandboxImageSummary, SandboxImageTag, SandboxLibraryImage, SandboxLibraryImageCreationOptions, @@ -483,8 +493,9 @@ SandboxLifecycle, SandboxLogEntry, SandboxLogs, + SandboxMetricDescriptor, SandboxMetrics, - SandboxMetricsPoint, + SandboxMetricValueSet, SandboxNetwork, SandboxPort, SandboxProxyConfig, @@ -496,13 +507,20 @@ SearchTrainingJobsRequest, SearchTrainingJobsResponse, Secret, - SecretId, SecretReference, Secrets, SecretTombstone, + SetVolumeTagRequest, + SetVolumeTagResponse, SignalPromotionResponse, SignSSHCertificateRequest, SignSSHCertificateResponse, + StartupAttempt, + StartupAttempts, + StartupPercentiles, + StartupPhasePercentiles, + StartupPhaseSummary, + StartupSummary, StatusItem, StopTrainingJobRequest, StopTrainingJobResponse, @@ -570,9 +588,6 @@ UpdatePromotionSettings, UpdateRequestBackpressureSettings, UpdateRollingDeployConfig, - UpdateRouteConnectionConfigAnthropic, - UpdateRouteConnectionConfigOpenAI, - UpdateRouteConnectionConfigXAI, UpdateRouteConnectionRequest, UpdateRouteHarnessDefaults, UpdateRouteRequest, @@ -584,6 +599,12 @@ UpdateSandboxRequest, UpdateTrainingJobRequest, UpdateTrainingJobResponse, + UpsertRouteConnectionConfigAnthropic, + UpsertRouteConnectionConfigOpenAI, + UpsertRouteConnectionConfigXAI, + UpsertRouteTargetBasetenModelAPI, + UpsertRouteTargetClassifierModelBased, + UpsertRouteTargetConnection, UpsertSecretRequest, UpsertTrainingProject, UpsertTrainingProjectRequest, @@ -595,6 +616,7 @@ UsersResponse, ValidateLoopsCheckpointRequest, ValidateLoopsCheckpointResponse, + Value, VertexTargetConfig, Volume, VolumeSync, @@ -679,6 +701,9 @@ "AuditLogEventUserRoleUpdated", "AuditLogEventUserTeamRoleUpdated", "AuditLogEventVolumeDeleted", + "AuditLogEventVolumeTagDeleted", + "AuditLogEventVolumeTagSet", + "AuditLogEventVolumeUpdated", "AuditLogEventVolumeVersionDeleted", "AuditLogEventVolumeVersionRestored", "AuditLogEventWebhookSigningSecretCreated", @@ -784,6 +809,8 @@ "DeleteSandboxParams", "DeleteVolumeRequest", "DeleteVolumeResponse", + "DeleteVolumeTagRequest", + "DeleteVolumeTagResponse", "DeleteVolumeVersionRequest", "DeleteVolumeVersionResponse", "DeployLoopsCheckpointRequest", @@ -812,6 +839,7 @@ "EffectiveModelConfig", "EffectiveRateLimit", "EffectiveUsageLimit", + "EgressRestrictions", "EmbeddingBenchmarkMetrics", "EnablementDetails", "Endpoint", @@ -827,7 +855,12 @@ "EnvironmentGroups", "EnvironmentTombstone", "Environments", - "ErrorRate", + "ExpectedSequence", + "ExpectedSequence1", + "ExpectedSequence2", + "ExpectedSequence3", + "ExpectedSequence4", + "ExpectedSequence5", "ExploreCost", "ExploreCostValues", "ExploreMetadata", @@ -881,8 +914,12 @@ "GetModelsDeploymentsLogsParams", "GetModelsDeploymentsMetricsParams", "GetModelsDeploymentsParams", + "GetModelsDeploymentsStartupsParams", + "GetModelsDeploymentsStartupsSummaryParams", "GetModelsEnvironmentsLogsParams", "GetModelsEnvironmentsMetricsParams", + "GetModelsEnvironmentsStartupsParams", + "GetModelsEnvironmentsStartupsSummaryParams", "GetModelsParams", "GetRoutesConnectionsParams", "GetRoutesParams", @@ -969,6 +1006,7 @@ "LoopsDeploymentMetrics", "LoopsDeploymentNodeMetrics", "LoopsDeploymentStatus", + "LoopsLoRAConfig", "LoopsRun", "LoopsRunStatus", "LoopsSampler", @@ -978,6 +1016,7 @@ "MaxScaleDownRate", "MaxScaleDownRate1", "MaxSeqLen", + "MinDurationMs", "Model", "ModelAPI", "ModelAPIOrgDetails", @@ -1012,6 +1051,8 @@ "PatchLoopsUserConfigResponse", "PatchTeamTrainingGpuCapacityRequest", "PatchTeamTrainingGpuCapacityResponse", + "PatchVolumeRequest", + "PatchVolumeResponse", "PendingJobAheadAtSubmit", "PrepareModelUploadRequest", "PrepareModelUploadResponse", @@ -1043,13 +1084,10 @@ "RollingDeployConfig", "Route", "RouteConnection", - "RouteConnectionAnthropic", "RouteConnectionConfigAnthropic", "RouteConnectionConfigOpenAI", "RouteConnectionConfigXAI", - "RouteConnectionOpenAI", "RouteConnectionTombstone", - "RouteConnectionXAI", "RouteConnectionsResponse", "RouteEffectiveSpendLimit", "RouteHarnessDefaults", @@ -1057,20 +1095,15 @@ "RouteRef", "RouteSpendLimitSetting", "RouteSpendLimitTeamDefault", - "RouteTargetAnthropic", "RouteTargetBasetenModelAPI", "RouteTargetClassifierModelBased", - "RouteTargetConfigAnthropic", - "RouteTargetConfigBasetenModelAPI", - "RouteTargetConfigClassifierModelBased", - "RouteTargetConfigOpenAI", - "RouteTargetConfigXAI", - "RouteTargetOpenAI", - "RouteTargetXAI", + "RouteTargetConnection", "RouteTeamSettings", "RouteTeamSpendLimitSetting", + "RouteTeamUserSettings", + "RouteTeamUserSettingsList", "RouteTombstone", - "RouteUserSettings", + "RouteUserTeamSettingsList", "RoutesResponse", "RoutesUsageBucket", "RoutesUsageResponse", @@ -1084,7 +1117,6 @@ "SandboxImage", "SandboxImageBuildLog", "SandboxImageBuildLogsResponse", - "SandboxImageSummary", "SandboxImageTag", "SandboxLibraryImage", "SandboxLibraryImageCreationOptions", @@ -1092,8 +1124,9 @@ "SandboxLifecycle", "SandboxLogEntry", "SandboxLogs", + "SandboxMetricDescriptor", + "SandboxMetricValueSet", "SandboxMetrics", - "SandboxMetricsPoint", "SandboxNetwork", "SandboxPort", "SandboxProxyConfig", @@ -1105,13 +1138,20 @@ "SearchTrainingJobsRequest", "SearchTrainingJobsResponse", "Secret", - "SecretId", "SecretReference", "SecretTombstone", "Secrets", + "SetVolumeTagRequest", + "SetVolumeTagResponse", "SignSSHCertificateRequest", "SignSSHCertificateResponse", "SignalPromotionResponse", + "StartupAttempt", + "StartupAttempts", + "StartupPercentiles", + "StartupPhasePercentiles", + "StartupPhaseSummary", + "StartupSummary", "StatusItem", "StopTrainingJobRequest", "StopTrainingJobResponse", @@ -1179,9 +1219,6 @@ "UpdatePromotionSettings", "UpdateRequestBackpressureSettings", "UpdateRollingDeployConfig", - "UpdateRouteConnectionConfigAnthropic", - "UpdateRouteConnectionConfigOpenAI", - "UpdateRouteConnectionConfigXAI", "UpdateRouteConnectionRequest", "UpdateRouteHarnessDefaults", "UpdateRouteRequest", @@ -1193,6 +1230,12 @@ "UpdateSandboxRequest", "UpdateTrainingJobRequest", "UpdateTrainingJobResponse", + "UpsertRouteConnectionConfigAnthropic", + "UpsertRouteConnectionConfigOpenAI", + "UpsertRouteConnectionConfigXAI", + "UpsertRouteTargetBasetenModelAPI", + "UpsertRouteTargetClassifierModelBased", + "UpsertRouteTargetConnection", "UpsertSecretRequest", "UpsertTrainingProject", "UpsertTrainingProjectRequest", @@ -1204,6 +1247,7 @@ "UsersResponse", "ValidateLoopsCheckpointRequest", "ValidateLoopsCheckpointResponse", + "Value", "VertexTargetConfig", "Volume", "VolumeSync", diff --git a/baseten/client/managementapi/_client.py b/baseten/client/managementapi/_client.py index e295dd8..13acdd1 100644 --- a/baseten/client/managementapi/_client.py +++ b/baseten/client/managementapi/_client.py @@ -66,6 +66,8 @@ DeleteSandboxParams, DeleteVolumeRequest, DeleteVolumeResponse, + DeleteVolumeTagRequest, + DeleteVolumeTagResponse, DeleteVolumeVersionRequest, DeleteVolumeVersionResponse, DeployLoopsCheckpointRequest, @@ -128,8 +130,12 @@ GetModelsDeploymentsLogsParams, GetModelsDeploymentsMetricsParams, GetModelsDeploymentsParams, + GetModelsDeploymentsStartupsParams, + GetModelsDeploymentsStartupsSummaryParams, GetModelsEnvironmentsLogsParams, GetModelsEnvironmentsMetricsParams, + GetModelsEnvironmentsStartupsParams, + GetModelsEnvironmentsStartupsSummaryParams, GetModelsParams, GetRoutesConnectionsParams, GetRoutesParams, @@ -205,6 +211,8 @@ PatchLoopsUserConfigResponse, PatchTeamTrainingGpuCapacityRequest, PatchTeamTrainingGpuCapacityResponse, + PatchVolumeRequest, + PatchVolumeResponse, PrepareModelUploadRequest, PrepareModelUploadResponse, PromoteRequest, @@ -228,8 +236,10 @@ RoutesResponse, RoutesUsageResponse, RouteTeamSettings, + RouteTeamUserSettings, + RouteTeamUserSettingsList, RouteTombstone, - RouteUserSettings, + RouteUserTeamSettingsList, Sandbox, SandboxImage, SandboxImageBuildLogsResponse, @@ -240,9 +250,13 @@ Secret, Secrets, SecretTombstone, + SetVolumeTagRequest, + SetVolumeTagResponse, SignalPromotionResponse, SignSSHCertificateRequest, SignSSHCertificateResponse, + StartupAttempts, + StartupSummary, StopTrainingJobRequest, StopTrainingJobResponse, SyncDeploymentPatchesRequest, @@ -723,6 +737,28 @@ def delete_volumes( ), ) + def delete_volumes_tags( + self, + *, + volume_namespace: str, + volume_name: str, + volume_tag: str, + request: DeleteVolumeTagRequest, + ) -> DeleteVolumeTagResponse: + """Deletes a volume tag""" + return self._do_json( + DeleteVolumeTagResponse, + _ApiRequest( + method="DELETE", + path_fmt="/v1/volumes/{}/{}/tags/{}", + path_args=[volume_namespace, volume_name, volume_tag], + body=request, + query=None, + success_codes=[200], + error_codes=None, + ), + ) + def delete_volumes_versions( self, *, @@ -1757,6 +1793,48 @@ def get_models_deployments_production(self, *, model_id: str) -> Deployment: ), ) + def get_models_deployments_startups( + self, + *, + model_id: str, + deployment_id: str, + params: GetModelsDeploymentsStartupsParams, + ) -> StartupAttempts: + """Lists the startups of a model deployment""" + return self._do_json( + StartupAttempts, + _ApiRequest( + method="GET", + path_fmt="/v1/models/{}/deployments/{}/startups", + path_args=[model_id, deployment_id], + body=None, + query=params, + success_codes=[200], + error_codes=None, + ), + ) + + def get_models_deployments_startups_summary( + self, + *, + model_id: str, + deployment_id: str, + params: GetModelsDeploymentsStartupsSummaryParams, + ) -> StartupSummary: + """Gets the startup summary for a model deployment""" + return self._do_json( + StartupSummary, + _ApiRequest( + method="GET", + path_fmt="/v1/models/{}/deployments/{}/startups/summary", + path_args=[model_id, deployment_id], + body=None, + query=params, + success_codes=[200], + error_codes=None, + ), + ) + def get_models_environments(self, *, model_id: str) -> Environments: """Gets all environments""" return self._do_json( @@ -1831,6 +1909,48 @@ def get_models_environments_metrics( ), ) + def get_models_environments_startups( + self, + *, + model_id: str, + env_name: str, + params: GetModelsEnvironmentsStartupsParams, + ) -> StartupAttempts: + """Lists the startups of a model environment""" + return self._do_json( + StartupAttempts, + _ApiRequest( + method="GET", + path_fmt="/v1/models/{}/environments/{}/startups", + path_args=[model_id, env_name], + body=None, + query=params, + success_codes=[200], + error_codes=None, + ), + ) + + def get_models_environments_startups_summary( + self, + *, + model_id: str, + env_name: str, + params: GetModelsEnvironmentsStartupsSummaryParams, + ) -> StartupSummary: + """Gets the startup summary for a model environment""" + return self._do_json( + StartupSummary, + _ApiRequest( + method="GET", + path_fmt="/v1/models/{}/environments/{}/startups/summary", + path_args=[model_id, env_name], + body=None, + query=params, + success_codes=[200], + error_codes=None, + ), + ) + def get_models_model_id(self, *, model_id: str) -> Model: """Gets a model by ID""" return self._do_json( @@ -1938,13 +2058,49 @@ def get_routes_settings_teams(self, *, team_id: str) -> RouteTeamSettings: ), ) - def get_routes_settings_users(self, *, user_id: str) -> RouteUserSettings: - """Gets a user's route settings""" + def get_routes_settings_teams_users( + self, *, team_id: str + ) -> RouteTeamUserSettingsList: + """Lists users' route settings in a team""" + return self._do_json( + RouteTeamUserSettingsList, + _ApiRequest( + method="GET", + path_fmt="/v1/routes/settings/teams/{}/users", + path_args=[team_id], + body=None, + query=None, + success_codes=[200], + error_codes=None, + ), + ) + + def get_routes_settings_teams_users_user_id( + self, *, team_id: str, user_id: str + ) -> RouteTeamUserSettings: + """Gets a user's route settings in a team""" + return self._do_json( + RouteTeamUserSettings, + _ApiRequest( + method="GET", + path_fmt="/v1/routes/settings/teams/{}/users/{}", + path_args=[team_id, user_id], + body=None, + query=None, + success_codes=[200], + error_codes=None, + ), + ) + + def get_routes_settings_users_teams( + self, *, user_id: str + ) -> RouteUserTeamSettingsList: + """Lists a user's route settings in each of their teams""" return self._do_json( - RouteUserSettings, + RouteUserTeamSettingsList, _ApiRequest( method="GET", - path_fmt="/v1/routes/settings/users/{}", + path_fmt="/v1/routes/settings/users/{}/teams", path_args=[user_id], body=None, query=None, @@ -2020,7 +2176,7 @@ def get_sandbox_logs( ) def get_sandbox_metrics( - self, *, sandbox_name: str, params: GetSandboxMetricsParams + self, *, sandbox_name: str, params: GetSandboxMetricsParams | None = None ) -> SandboxMetrics: """Get sandbox metrics""" return self._do_json( @@ -2967,16 +3123,16 @@ def patch_routes_settings_teams( ), ) - def patch_routes_settings_users( - self, *, user_id: str, request: UpdateRouteUserSettingsRequest - ) -> RouteUserSettings: - """Updates a user's route settings""" + def patch_routes_settings_teams_users( + self, *, team_id: str, user_id: str, request: UpdateRouteUserSettingsRequest + ) -> RouteTeamUserSettings: + """Updates a user's route settings in a team""" return self._do_json( - RouteUserSettings, + RouteTeamUserSettings, _ApiRequest( method="PATCH", - path_fmt="/v1/routes/settings/users/{}", - path_args=[user_id], + path_fmt="/v1/routes/settings/teams/{}/users/{}", + path_args=[team_id, user_id], body=request, query=None, success_codes=[200], @@ -3061,6 +3217,23 @@ def patch_training_projects_jobs_interactive_sessions( ), ) + def patch_volumes( + self, *, volume_namespace: str, volume_name: str, request: PatchVolumeRequest + ) -> PatchVolumeResponse: + """Updates a volume""" + return self._do_json( + PatchVolumeResponse, + _ApiRequest( + method="PATCH", + path_fmt="/v1/volumes/{}/{}", + path_args=[volume_namespace, volume_name], + body=request, + query=None, + success_codes=[200], + error_codes=None, + ), + ) + def post_api_keys(self, *, request: CreateAPIKeyRequest) -> APIKey: """Creates an API key""" return self._do_json( @@ -4307,6 +4480,23 @@ def post_volumes_syncs_cancel(self, *, volume_sync_id: str) -> VolumeSync: ), ) + def post_volumes_tags( + self, *, volume_namespace: str, volume_name: str, request: SetVolumeTagRequest + ) -> SetVolumeTagResponse: + """Sets a volume tag""" + return self._do_json( + SetVolumeTagResponse, + _ApiRequest( + method="POST", + path_fmt="/v1/volumes/{}/{}/tags", + path_args=[volume_namespace, volume_name], + body=request, + query=None, + success_codes=[200], + error_codes=None, + ), + ) + def post_volumes_token( self, *, request: CreateVolumeTokenRequest ) -> CreateVolumeTokenResponse: @@ -4860,6 +5050,28 @@ async def delete_volumes( ), ) + async def delete_volumes_tags( + self, + *, + volume_namespace: str, + volume_name: str, + volume_tag: str, + request: DeleteVolumeTagRequest, + ) -> DeleteVolumeTagResponse: + """Deletes a volume tag""" + return await self._do_json( + DeleteVolumeTagResponse, + _ApiRequest( + method="DELETE", + path_fmt="/v1/volumes/{}/{}/tags/{}", + path_args=[volume_namespace, volume_name, volume_tag], + body=request, + query=None, + success_codes=[200], + error_codes=None, + ), + ) + async def delete_volumes_versions( self, *, @@ -5898,6 +6110,48 @@ async def get_models_deployments_production(self, *, model_id: str) -> Deploymen ), ) + async def get_models_deployments_startups( + self, + *, + model_id: str, + deployment_id: str, + params: GetModelsDeploymentsStartupsParams, + ) -> StartupAttempts: + """Lists the startups of a model deployment""" + return await self._do_json( + StartupAttempts, + _ApiRequest( + method="GET", + path_fmt="/v1/models/{}/deployments/{}/startups", + path_args=[model_id, deployment_id], + body=None, + query=params, + success_codes=[200], + error_codes=None, + ), + ) + + async def get_models_deployments_startups_summary( + self, + *, + model_id: str, + deployment_id: str, + params: GetModelsDeploymentsStartupsSummaryParams, + ) -> StartupSummary: + """Gets the startup summary for a model deployment""" + return await self._do_json( + StartupSummary, + _ApiRequest( + method="GET", + path_fmt="/v1/models/{}/deployments/{}/startups/summary", + path_args=[model_id, deployment_id], + body=None, + query=params, + success_codes=[200], + error_codes=None, + ), + ) + async def get_models_environments(self, *, model_id: str) -> Environments: """Gets all environments""" return await self._do_json( @@ -5972,6 +6226,48 @@ async def get_models_environments_metrics( ), ) + async def get_models_environments_startups( + self, + *, + model_id: str, + env_name: str, + params: GetModelsEnvironmentsStartupsParams, + ) -> StartupAttempts: + """Lists the startups of a model environment""" + return await self._do_json( + StartupAttempts, + _ApiRequest( + method="GET", + path_fmt="/v1/models/{}/environments/{}/startups", + path_args=[model_id, env_name], + body=None, + query=params, + success_codes=[200], + error_codes=None, + ), + ) + + async def get_models_environments_startups_summary( + self, + *, + model_id: str, + env_name: str, + params: GetModelsEnvironmentsStartupsSummaryParams, + ) -> StartupSummary: + """Gets the startup summary for a model environment""" + return await self._do_json( + StartupSummary, + _ApiRequest( + method="GET", + path_fmt="/v1/models/{}/environments/{}/startups/summary", + path_args=[model_id, env_name], + body=None, + query=params, + success_codes=[200], + error_codes=None, + ), + ) + async def get_models_model_id(self, *, model_id: str) -> Model: """Gets a model by ID""" return await self._do_json( @@ -6081,13 +6377,49 @@ async def get_routes_settings_teams(self, *, team_id: str) -> RouteTeamSettings: ), ) - async def get_routes_settings_users(self, *, user_id: str) -> RouteUserSettings: - """Gets a user's route settings""" + async def get_routes_settings_teams_users( + self, *, team_id: str + ) -> RouteTeamUserSettingsList: + """Lists users' route settings in a team""" + return await self._do_json( + RouteTeamUserSettingsList, + _ApiRequest( + method="GET", + path_fmt="/v1/routes/settings/teams/{}/users", + path_args=[team_id], + body=None, + query=None, + success_codes=[200], + error_codes=None, + ), + ) + + async def get_routes_settings_teams_users_user_id( + self, *, team_id: str, user_id: str + ) -> RouteTeamUserSettings: + """Gets a user's route settings in a team""" + return await self._do_json( + RouteTeamUserSettings, + _ApiRequest( + method="GET", + path_fmt="/v1/routes/settings/teams/{}/users/{}", + path_args=[team_id, user_id], + body=None, + query=None, + success_codes=[200], + error_codes=None, + ), + ) + + async def get_routes_settings_users_teams( + self, *, user_id: str + ) -> RouteUserTeamSettingsList: + """Lists a user's route settings in each of their teams""" return await self._do_json( - RouteUserSettings, + RouteUserTeamSettingsList, _ApiRequest( method="GET", - path_fmt="/v1/routes/settings/users/{}", + path_fmt="/v1/routes/settings/users/{}/teams", path_args=[user_id], body=None, query=None, @@ -6163,7 +6495,7 @@ async def get_sandbox_logs( ) async def get_sandbox_metrics( - self, *, sandbox_name: str, params: GetSandboxMetricsParams + self, *, sandbox_name: str, params: GetSandboxMetricsParams | None = None ) -> SandboxMetrics: """Get sandbox metrics""" return await self._do_json( @@ -7116,16 +7448,16 @@ async def patch_routes_settings_teams( ), ) - async def patch_routes_settings_users( - self, *, user_id: str, request: UpdateRouteUserSettingsRequest - ) -> RouteUserSettings: - """Updates a user's route settings""" + async def patch_routes_settings_teams_users( + self, *, team_id: str, user_id: str, request: UpdateRouteUserSettingsRequest + ) -> RouteTeamUserSettings: + """Updates a user's route settings in a team""" return await self._do_json( - RouteUserSettings, + RouteTeamUserSettings, _ApiRequest( method="PATCH", - path_fmt="/v1/routes/settings/users/{}", - path_args=[user_id], + path_fmt="/v1/routes/settings/teams/{}/users/{}", + path_args=[team_id, user_id], body=request, query=None, success_codes=[200], @@ -7210,6 +7542,23 @@ async def patch_training_projects_jobs_interactive_sessions( ), ) + async def patch_volumes( + self, *, volume_namespace: str, volume_name: str, request: PatchVolumeRequest + ) -> PatchVolumeResponse: + """Updates a volume""" + return await self._do_json( + PatchVolumeResponse, + _ApiRequest( + method="PATCH", + path_fmt="/v1/volumes/{}/{}", + path_args=[volume_namespace, volume_name], + body=request, + query=None, + success_codes=[200], + error_codes=None, + ), + ) + async def post_api_keys(self, *, request: CreateAPIKeyRequest) -> APIKey: """Creates an API key""" return await self._do_json( @@ -8468,6 +8817,23 @@ async def post_volumes_syncs_cancel(self, *, volume_sync_id: str) -> VolumeSync: ), ) + async def post_volumes_tags( + self, *, volume_namespace: str, volume_name: str, request: SetVolumeTagRequest + ) -> SetVolumeTagResponse: + """Sets a volume tag""" + return await self._do_json( + SetVolumeTagResponse, + _ApiRequest( + method="POST", + path_fmt="/v1/volumes/{}/{}/tags", + path_args=[volume_namespace, volume_name], + body=request, + query=None, + success_codes=[200], + error_codes=None, + ), + ) + async def post_volumes_token( self, *, request: CreateVolumeTokenRequest ) -> CreateVolumeTokenResponse: diff --git a/baseten/client/managementapi/_models.py b/baseten/client/managementapi/_models.py index 299a298..316a47f 100644 --- a/baseten/client/managementapi/_models.py +++ b/baseten/client/managementapi/_models.py @@ -381,6 +381,65 @@ class SearchPattern(RootModel[str]): ] = None +class MinDurationMs(RootModel[int]): + root: Annotated[ + int | None, + Field( + description="Only include startups that lasted at least this many milliseconds.", + ge=0, + title="Min Duration Ms", + ), + ] = None + + +class GetModelsDeploymentsStartupsSummaryParams(BaseModel): + start_epoch_millis: Annotated[ + int, + Field( + description="Start of the query window, in epoch milliseconds.", + title="Start Epoch Millis", + ), + ] + end_epoch_millis: Annotated[ + int, + Field( + description="End of the query window, in epoch milliseconds. Must be after the start.", + title="End Epoch Millis", + ), + ] + min_duration_ms: Annotated[ + MinDurationMs | None, + Field( + description="Only include startups that lasted at least this many milliseconds.", + title="Min Duration Ms", + ), + ] = None + + +class GetModelsEnvironmentsStartupsSummaryParams(BaseModel): + start_epoch_millis: Annotated[ + int, + Field( + description="Start of the query window, in epoch milliseconds.", + title="Start Epoch Millis", + ), + ] + end_epoch_millis: Annotated[ + int, + Field( + description="End of the query window, in epoch milliseconds. Must be after the start.", + title="End Epoch Millis", + ), + ] + min_duration_ms: Annotated[ + MinDurationMs | None, + Field( + description="Only include startups that lasted at least this many milliseconds.", + title="Min Duration Ms", + ), + ] = None + + class GetTrainingProjectsJobsMetricsParams(BaseModel): end_epoch_millis: Annotated[ int | None, @@ -827,19 +886,6 @@ class GetRoutesConnectionsParams(BaseModel): ] = None -class GetSandboxMetricsParams(BaseModel): - team_id: Annotated[ - str | None, - Field( - description="Optional team ID. Must match X-Team-Id when both are supplied. If neither selector is supplied, defaults to the caller's only accessible team. Callers with multiple accessible teams must select a team. Requests without access to any team are forbidden.", - min_length=1, - ), - ] = None - start_time: AwareDatetime - end_time: AwareDatetime - interval_seconds: Literal[10, 30, 60, 300, 900, 3600] | str - - class GetSandboxLogsParams(BaseModel): team_id: Annotated[ str | None, @@ -1699,9 +1745,23 @@ class CreateVolumeSyncRequest(BaseModel): ] +class ExpectedSequence(RootModel[int]): + root: Annotated[ + int | None, + Field( + description="Revision the volume is expected to be at. When set, the delete fails with a conflict if the volume has changed since, so it cannot act on a volume someone else has pushed to. Take the value from a volume's sequence, or from volume_sequence.", + ge=0, + title="Expected Sequence", + ), + ] = None + + class DeleteVolumeRequest(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) expected_sequence: Annotated[ - int | None, + ExpectedSequence | None, Field( description="Revision the volume is expected to be at. When set, the delete fails with a conflict if the volume has changed since, so it cannot act on a volume someone else has pushed to. Take the value from a volume's sequence, or from volume_sequence.", title="Expected Sequence", @@ -1736,6 +1796,65 @@ class DeleteVolumeResponse(BaseModel): ] +class ExpectedSequence1(RootModel[int]): + root: Annotated[ + int | None, + Field( + description="Revision the volume is expected to be at. When set, the update fails with a conflict if the volume has changed since. Take the value from a volume's sequence, or from volume_sequence.", + ge=0, + title="Expected Sequence", + ), + ] = None + + +class PatchVolumeRequest(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + expires_at: Annotated[ + AwareDatetime | None, + Field( + description="Time at which the volume expires, in ISO 8601 format, with a UTC offset. It must be in the future and at most ten years ahead. Set it to null to clear the expiration time. A body that sets no field is rejected. When the volume expires, all live versions are deleted, all tags are removed, and the volume disappears from listings. Clearing the expiration time afterward does not restore deleted versions. Each deleted version can be restored until its delete_after time. Setting or clearing the expiration time requires the same permission as deleting the volume.", + examples=["2030-01-01T00:00:00Z"], + title="Expires At", + ), + ] = None + expected_sequence: Annotated[ + ExpectedSequence1 | None, + Field( + description="Revision the volume is expected to be at. When set, the update fails with a conflict if the volume has changed since. Take the value from a volume's sequence, or from volume_sequence.", + title="Expected Sequence", + ), + ] = None + + +class PatchVolumeResponse(BaseModel): + namespace: Annotated[ + str, + Field( + description="Namespace the volume belongs to, in lowercase.", + title="Namespace", + ), + ] + volume: Annotated[ + str, Field(description="Name of the volume, in lowercase.", title="Volume") + ] + expires_at: Annotated[ + AwareDatetime | None, + Field( + description="Volume expiration time in ISO 8601 format. Null when no expiration time is set.", + title="Expires At", + ), + ] + volume_sequence: Annotated[ + int, + Field( + description="Revision of the volume after the update.", + title="Volume Sequence", + ), + ] + + class VolumeVersion(BaseModel): namespace: Annotated[ str, @@ -1839,9 +1958,23 @@ class ListVolumeVersionsResponse(BaseModel): ] +class ExpectedSequence2(RootModel[int]): + root: Annotated[ + int | None, + Field( + description="Revision the volume is expected to be at. When set, the delete fails with a conflict if the volume has changed since, so a read followed by a delete cannot act on a version a tag has since been moved off. Take the value from volume_sequence.", + ge=0, + title="Expected Sequence", + ), + ] = None + + class DeleteVolumeVersionRequest(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) expected_sequence: Annotated[ - int | None, + ExpectedSequence2 | None, Field( description="Revision the volume is expected to be at. When set, the delete fails with a conflict if the volume has changed since, so a read followed by a delete cannot act on a version a tag has since been moved off. Take the value from volume_sequence.", title="Expected Sequence", @@ -2000,9 +2133,23 @@ class VolumeVersionDetail(BaseModel): ] +class ExpectedSequence3(RootModel[int]): + root: Annotated[ + int | None, + Field( + description="Revision the volume is expected to be at. When set, the restore fails with a conflict if the volume has changed since. Take the value from volume_sequence.", + ge=0, + title="Expected Sequence", + ), + ] = None + + class RestoreVolumeVersionRequest(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) expected_sequence: Annotated[ - int | None, + ExpectedSequence3 | None, Field( description="Revision the volume is expected to be at. When set, the restore fails with a conflict if the volume has changed since. Take the value from volume_sequence.", title="Expected Sequence", @@ -2051,99 +2198,253 @@ class RestoreVolumeVersionResponse(BaseModel): ] -class CreateTokenRequest(BaseModel): - scopes: Annotated[ - list[Literal["sandboxes"] | str], +class ExpectedSequence4(RootModel[int]): + root: Annotated[ + int | None, Field( - description="What the token should grant access to. Only `sandboxes` is supported today; the token then authenticates against the sandbox API.", - examples=[["sandboxes"]], - min_length=1, - title="Scopes", + description="Revision the volume is expected to be at. When set, the request fails with a conflict if the volume has changed since. Take the value from volume_sequence.", + ge=0, + title="Expected Sequence", ), - ] + ] = None -class Token(BaseModel): - token: Annotated[ +class SetVolumeTagRequest(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + tag: Annotated[ str, Field( - description="Short-lived bearer token for the sandbox API. Send it as Authorization: Bearer .", - title="Token", + description="Name of the tag to set. Tags are case-sensitive. Setting a tag that already exists overwrites it, and the version it pointed at before stays reachable by digest.", + examples=["nightly"], + pattern="^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$", + title="Tag", + ), + ] + version: Annotated[ + str, + Field( + description="Version to set the tag on. Address it with `:` for a tag, `@` for a full content digest or a prefix of at least 12 hexadecimal characters, or `head` for the version a reference with no tag or digest resolves to.", + examples=["head"], + title="Version", ), ] expires_at: Annotated[ - AwareDatetime, + AwareDatetime | None, Field( - description="Token expiry in ISO 8601 format. Tokens cannot be renewed; request a new one.", + description="Time at which the tag expires, in ISO 8601 format, with a UTC offset. It must be in the future and at most ten years ahead. Omit it for a tag with no expiration time of its own. Omitting it also clears the expiration time of an existing tag. Version or volume expiration still deletes the tag. Do not specify it when setting `head`.", title="Expires At", ), - ] + ] = None + expected_sequence: Annotated[ + ExpectedSequence4 | None, + Field( + description="Revision the volume is expected to be at. When set, the request fails with a conflict if the volume has changed since. Take the value from volume_sequence.", + title="Expected Sequence", + ), + ] = None -class Secret(BaseModel): - id: Annotated[ +class SetVolumeTagResponse(BaseModel): + namespace: Annotated[ str, Field( - description="Stable identifier for the secret. Unchanged across rotation.", - examples=["3kZ9xqd"], - title="Id", + description="Namespace the volume belongs to, in lowercase.", + title="Namespace", ), ] - created_at: Annotated[ - AwareDatetime, + volume: Annotated[ + str, Field(description="Name of the volume, in lowercase.", title="Volume") + ] + tag: Annotated[ + str, Field( - description="Time the secret was created in ISO 8601 format", - title="Created At", + description="Name of the tag that was set, with its original capitalization.", + title="Tag", ), ] - name: Annotated[str, Field(description="Name of the secret", title="Name")] - team_name: Annotated[ + digest: Annotated[ str, - Field(description="Name of the team the secret belongs to", title="Team Name"), + Field( + description="Content digest of the version the tag points at, as `b3:`.", + title="Digest", + ), ] - - -class Secrets(BaseModel): - secrets: Annotated[list[Secret], Field(title="Secrets")] - - -class UpsertSecretRequest(BaseModel): - name: Annotated[ + version_ref: Annotated[ str, Field( - description="Name of the new or existing secret", - examples=["my_secret"], - title="Name", + description="Full address of the tagged version, as `bdn:/@`. Paste it into a config.yaml to pin this version, even if the tag is later set on another version.", + title="Version Ref", ), ] - value: Annotated[ - str, + volume_sequence: Annotated[ + int, Field( - description="Value of the secret", - examples=["my_secret_value"], - title="Value", + description="Revision of the volume after the tag was set.", + title="Volume Sequence", + ), + ] + expires_at: Annotated[ + AwareDatetime | None, + Field( + description="Tag expiration time in ISO 8601 format. Null when the tag has no expiration time of its own.", + title="Expires At", ), ] -class SecretTombstone(BaseModel): - name: Annotated[str, Field(description="Name of the deleted secret", title="Name")] +class ExpectedSequence5(RootModel[int]): + root: Annotated[ + int | None, + Field( + description="Revision the volume is expected to be at. When set, the delete fails with a conflict if the volume has changed since, so it cannot delete a tag someone else has just set on another version. Take the value from volume_sequence.", + ge=0, + title="Expected Sequence", + ), + ] = None -class EnvironmentGroupUser(BaseModel): - user_id: Annotated[ - str, Field(description="Unique identifier for the user.", title="User Id") - ] - email: Annotated[ - str | None, Field(description="Email address of the user.", title="Email") - ] = None - name: Annotated[ - str | None, Field(description="Display name of the user.", title="Name") +class DeleteVolumeTagRequest(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + expected_sequence: Annotated[ + ExpectedSequence5 | None, + Field( + description="Revision the volume is expected to be at. When set, the delete fails with a conflict if the volume has changed since, so it cannot delete a tag someone else has just set on another version. Take the value from volume_sequence.", + title="Expected Sequence", + ), ] = None -class UpdateEnvironmentGroupManageAccess(BaseModel): - is_restricted: Annotated[ +class DeleteVolumeTagResponse(BaseModel): + namespace: Annotated[ + str, + Field( + description="Namespace the volume belongs to, in lowercase.", + title="Namespace", + ), + ] + volume: Annotated[ + str, Field(description="Name of the volume, in lowercase.", title="Volume") + ] + tag: Annotated[ + str, + Field( + description="Tag name from the request, with its original capitalization.", + title="Tag", + ), + ] + deleted: Annotated[ + bool, + Field( + description="Whether the tag existed and was deleted. False when the volume had no tag by that name, which is not an error.", + title="Deleted", + ), + ] + volume_sequence: Annotated[ + int, + Field( + description="Revision of the volume after the delete.", + title="Volume Sequence", + ), + ] + + +class CreateTokenRequest(BaseModel): + scopes: Annotated[ + list[Literal["sandboxes"] | str], + Field( + description="What the token should grant access to. Only `sandboxes` is supported today; the token then authenticates against the sandbox API.", + examples=[["sandboxes"]], + min_length=1, + title="Scopes", + ), + ] + + +class Token(BaseModel): + token: Annotated[ + str, + Field( + description="Short-lived bearer token for the sandbox API. Send it as Authorization: Bearer .", + title="Token", + ), + ] + expires_at: Annotated[ + AwareDatetime, + Field( + description="Token expiry in ISO 8601 format. Tokens cannot be renewed; request a new one.", + title="Expires At", + ), + ] + + +class Secret(BaseModel): + id: Annotated[ + str, + Field( + description="Stable identifier for the secret. Unchanged across rotation.", + examples=["3kZ9xqd"], + title="Id", + ), + ] + created_at: Annotated[ + AwareDatetime, + Field( + description="Time the secret was created in ISO 8601 format", + title="Created At", + ), + ] + name: Annotated[str, Field(description="Name of the secret", title="Name")] + team_name: Annotated[ + str, + Field(description="Name of the team the secret belongs to", title="Team Name"), + ] + + +class Secrets(BaseModel): + secrets: Annotated[list[Secret], Field(title="Secrets")] + + +class UpsertSecretRequest(BaseModel): + name: Annotated[ + str, + Field( + description="Name of the new or existing secret", + examples=["my_secret"], + title="Name", + ), + ] + value: Annotated[ + str, + Field( + description="Value of the secret", + examples=["my_secret_value"], + title="Value", + ), + ] + + +class SecretTombstone(BaseModel): + name: Annotated[str, Field(description="Name of the deleted secret", title="Name")] + + +class EnvironmentGroupUser(BaseModel): + user_id: Annotated[ + str, Field(description="Unique identifier for the user.", title="User Id") + ] + email: Annotated[ + str | None, Field(description="Email address of the user.", title="Email") + ] = None + name: Annotated[ + str | None, Field(description="Display name of the user.", title="Name") + ] = None + + +class UpdateEnvironmentGroupManageAccess(BaseModel): + is_restricted: Annotated[ bool, Field( description="Whether to restrict this environment to a specific set of users.", @@ -3072,6 +3373,39 @@ class AuditLogEventVolumeDeleted(BaseModel): versions_deleted: Annotated[int, Field(title="Versions Deleted")] +class AuditLogEventVolumeTagDeleted(BaseModel): + event_type: Annotated[Literal["VOLUME_TAG_DELETED"], Field(title="Event Type")] + volume_ref: Annotated[str, Field(title="Volume Ref")] + tag: Annotated[str, Field(title="Tag")] + deleted: Annotated[bool, Field(title="Deleted")] + + +class AuditLogEventVolumeTagSet(BaseModel): + event_type: Annotated[Literal["VOLUME_TAG_SET"], Field(title="Event Type")] + volume_ref: Annotated[str, Field(title="Volume Ref")] + tag: Annotated[str, Field(title="Tag")] + digest: Annotated[str, Field(title="Digest")] + + +class AuditLogEventVolumeUpdated(BaseModel): + event_type: Annotated[Literal["VOLUME_UPDATED"], Field(title="Event Type")] + volume_ref: Annotated[str, Field(title="Volume Ref")] + expires_at: Annotated[ + str | None, + Field( + description="The volume's expiration time after the update, as an ISO 8601 string. Null when the update cleared it.", + title="Expires At", + ), + ] + previous_expires_at: Annotated[ + str | None, + Field( + description="The expiration time the update replaced, as an ISO 8601 string. Null when the volume had none.", + title="Previous Expires At", + ), + ] + + class AuditLogEventVolumeVersionDeleted(BaseModel): event_type: Annotated[Literal["VOLUME_VERSION_DELETED"], Field(title="Event Type")] volume_ref: Annotated[str, Field(title="Volume Ref")] @@ -3983,6 +4317,77 @@ class ModelMetricValueSet(BaseModel): ] +class StartupPercentiles(BaseModel): + sample_count: Annotated[ + int, + Field( + description="Number of startups in the distribution, including those that took 0 ms. A phase that finishes within a second can record 0 ms.", + title="Sample Count", + ), + ] + mean_ms: Annotated[ + int | None, + Field(description="Mean duration, in milliseconds.", title="Mean Ms"), + ] + p50_ms: Annotated[ + int | None, + Field(description="50th percentile duration, in milliseconds.", title="P50 Ms"), + ] + p90_ms: Annotated[ + int | None, + Field(description="90th percentile duration, in milliseconds.", title="P90 Ms"), + ] + p95_ms: Annotated[ + int | None, + Field(description="95th percentile duration, in milliseconds.", title="P95 Ms"), + ] + p99_ms: Annotated[ + int | None, + Field(description="99th percentile duration, in milliseconds.", title="P99 Ms"), + ] + + +class StartupPhasePercentiles(BaseModel): + phase: Annotated[ + Literal[ + "SCHEDULING", + "IMAGE_PULL", + "WEIGHT_DOWNLOAD", + "CONTAINER_CREATE", + "CONTAINER_START", + "READINESS", + ] + | str, + Field(description="The phase.", title="StartupPhaseV1"), + ] + duration_ms: Annotated[ + StartupPercentiles, Field(description="Durations of the phase.") + ] + + +class StartupSummary(BaseModel): + start_epoch_millis: Annotated[ + int, + Field(description="Start of the returned window.", title="Start Epoch Millis"), + ] + end_epoch_millis: Annotated[ + int, Field(description="End of the returned window.", title="End Epoch Millis") + ] + total_duration_ms: Annotated[ + StartupPercentiles, + Field( + description="Durations of whole startups, sampled from the 1,000 most recent in the window." + ), + ] + phase_duration_ms: Annotated[ + list[StartupPhasePercentiles], + Field( + description="Durations of each phase that was recorded in the window.", + title="Phase Duration Ms", + ), + ] + + class TerminateReplicaResponse(BaseModel): success: Annotated[ bool, @@ -5573,16 +5978,19 @@ class LoopsSampler(BaseModel): ), ] model_id: Annotated[ - str, - Field(description="Hashid of the underlying Baseten model.", title="Model Id"), - ] + str | None, + Field( + description="Hashid of the underlying Baseten model. Null for a serverless sampler.", + title="Model Id", + ), + ] = None deployment_id: Annotated[ - str, + str | None, Field( - description="Hashid of the specific model deployment (version).", + description="Hashid of the specific model deployment (version). Null for a serverless sampler.", title="Deployment Id", ), - ] + ] = None status: Annotated[ LoopsSamplerStatus, Field(description="The sampler's current status.") ] @@ -5898,64 +6306,27 @@ class GetLoopsSamplerResponse(BaseModel): sampler: Annotated[LoopsSampler, Field(description="The Loops sampler.")] -class LoopsCheckpoint(BaseModel): - checkpoint_id: Annotated[ - str, Field(description="The ID of the checkpoint.", title="Checkpoint Id") - ] - created_at: Annotated[ - AwareDatetime, +class LoopsLoRAConfig(BaseModel): + config_version: Annotated[ + str, Field( - description="The timestamp of the checkpoint in ISO 8601 format.", - title="Created At", + description="The config version, unique per base model.", + title="Config Version", ), ] - checkpoint_type: Annotated[ - str, Field(description="The type of checkpoint.", title="Checkpoint Type") - ] - base_model: Annotated[ - str | None, - Field(description="The base model of the checkpoint.", title="Base Model"), - ] - lora_adapter_config: Annotated[ - dict[str, Any] | None, + targets: Annotated[ + list[str], Field( - description="The adapter config of the checkpoint.", - title="Lora Adapter Config", + description="The module names the LoRA adapter targets.", title="Targets" ), ] - size_bytes: Annotated[ - int, - Field(description="The size of the checkpoint in bytes.", title="Size Bytes"), - ] - sync_status: Annotated[ - str | None, + expert_layout: Annotated[ + Literal["per_expert", "shared_outer", "shared_adapter"] | str | None, Field( - description="Sync state of the checkpoint: SYNCING or COMPLETE.", - title="Sync Status", + description="How routed-expert LoRA weights are laid out. Null when no experts are targeted.", + title="Expert Layout", ), ] = None - id: Annotated[str, Field(description="The checkpoint ID.", title="Id")] - run_id: Annotated[ - str, - Field( - description="The ID of the run that produced the checkpoint.", - title="Run Id", - ), - ] - target: Annotated[ - Literal["sampler", "trainer"] | str, - Field( - description="Whether this checkpoint is loadable by the sampler or by the run.", - title="TrainerCheckpointTarget", - ), - ] - - -class ListLoopsCheckpointsResponse(BaseModel): - checkpoints: Annotated[ - list[LoopsCheckpoint], - Field(description="Matching checkpoints.", title="Checkpoints"), - ] class ValidateLoopsCheckpointRequest(BaseModel): @@ -5967,6 +6338,14 @@ class ValidateLoopsCheckpointRequest(BaseModel): title="Checkpoint Path", ), ] + run_id: Annotated[ + str | None, + Field( + description="Loops run whose sampler will load the checkpoint. The checkpoint must use the run's LoRA config.", + examples=["k4q95w5"], + title="Run Id", + ), + ] = None class ValidateLoopsCheckpointResponse(BaseModel): @@ -6782,6 +7161,13 @@ class ModelApisUsageResult(BaseModel): title="Model", ), ] = None + team_id: Annotated[ + str | None, + Field( + description="Team the usage is attributed to. Null when not grouping by team or when team attribution is unavailable.", + title="Team Id", + ), + ] = None input_tokens: Annotated[ int, Field( @@ -6811,6 +7197,25 @@ class ModelApisUsageResult(BaseModel): ] +class EgressRestrictions(BaseModel): + fqdn_allow_list: Annotated[ + list[str] | None, + Field( + description="Allowed outbound FQDNs; '*' wildcards are supported.", + examples=[["*.example.com"]], + title="Fqdn Allow List", + ), + ] = None + ip_allow_list: Annotated[ + list[str] | None, + Field( + description="Allowed outbound IPv4 addresses or CIDR ranges.", + examples=[["1.1.1.1/32"]], + title="Ip Allow List", + ), + ] = None + + class CreateLLMModelRequest(BaseModel): resources: Annotated[ dict[str, Any], @@ -6893,6 +7298,12 @@ class CreateLLMModelRequest(BaseModel): title="Weights", ), ] = None + egress_restrictions: Annotated[ + EgressRestrictions | None, + Field( + description="Restricts this deployment's egress to the specified FQDNs and IP addresses; an empty block allows none. Requires the organization to have egress restrictions enabled." + ), + ] = None name: Annotated[str, Field(description="Name of the model", title="Name")] @@ -6998,6 +7409,12 @@ class CreateLLMModelVersionRequest(BaseModel): title="Weights", ), ] = None + egress_restrictions: Annotated[ + EgressRestrictions | None, + Field( + description="Restricts this deployment's egress to the specified FQDNs and IP addresses; an empty block allows none. Requires the organization to have egress restrictions enabled." + ), + ] = None class LibraryListingMetadata(BaseModel): @@ -7038,6 +7455,13 @@ class LibraryListing(BaseModel): description="Whether the listing is publicly accessible", title="Is Public" ), ] + is_listed: Annotated[ + bool, + Field( + description="Whether the listing appears in catalogs; unlisted listings still deploy from a direct link", + title="Is Listed", + ), + ] trending: Annotated[ bool | None, Field(description="Whether the listing is trending", title="Trending"), @@ -7090,6 +7514,13 @@ class CreateLibraryListingRequest(BaseModel): description="Whether the listing is publicly accessible", title="Is Public" ), ] = False + is_listed: Annotated[ + bool, + Field( + description="Whether the listing appears in catalogs; unlisted listings still deploy from a direct link", + title="Is Listed", + ), + ] = True closed_source: Annotated[ bool, Field( @@ -7130,6 +7561,13 @@ class UpdateLibraryListingRequest(BaseModel): bool | None, Field(description="Whether the listing is trending", title="Trending"), ] = None + is_listed: Annotated[ + bool | None, + Field( + description="Whether the listing appears in catalogs; unlisted listings still deploy from a direct link", + title="Is Listed", + ), + ] = None metadata: Annotated[ LibraryListingMetadata | None, Field( @@ -7220,6 +7658,20 @@ class CreateLibraryListingVersionRequest(BaseModel): str, Field(description="Human-readable tag for this version", title="Version Tag"), ] + is_listed: Annotated[ + bool, + Field( + description="Whether the listing appears in catalogs; unlisted listings still deploy from a direct link. Only used when creating a new listing.", + title="Is Listed", + ), + ] = True + replace_model: Annotated[ + bool, + Field( + description="Allow publishing from a different model than the listing's current one. The listing then points at the new model; earlier versions and existing deployments are unaffected. Fails if the new model already backs another listing.", + title="Replace Model", + ), + ] = False class LibraryListingVersionTombstone(BaseModel): @@ -7794,27 +8246,6 @@ class RouteRef(BaseModel): ] -class RouteTargetAnthropic(BaseModel): - type: Annotated[ - Literal["ANTHROPIC"], - Field(description="Target kind for Anthropic.", title="Type"), - ] - model: Annotated[ - str, - Field( - description="Model name sent to the provider.", min_length=1, title="Model" - ), - ] - secret_name: Annotated[ - str, - Field( - description="Name of a credential secret owned by the route's team.", - min_length=1, - title="Secret Name", - ), - ] - - class RouteTargetBasetenModelAPI(BaseModel): type: Annotated[ Literal["BASETEN_MODEL_API"], @@ -7861,71 +8292,30 @@ class RouteTargetClassifierModelBased(BaseModel): ] -class RouteTargetOpenAI(BaseModel): - type: Annotated[ - Literal["OPENAI"], Field(description="Target kind for OpenAI.", title="Type") - ] - model: Annotated[ - str, - Field( - description="Model name sent to the provider.", min_length=1, title="Model" - ), - ] - secret_name: Annotated[ - str, - Field( - description="Name of a credential secret owned by the route's team.", - min_length=1, - title="Secret Name", - ), - ] - - -class RouteTargetXAI(BaseModel): +class RouteTargetConnection(BaseModel): type: Annotated[ - Literal["XAI"], Field(description="Target kind for xAI.", title="Type") - ] - model: Annotated[ - str, + Literal["CONNECTION"], Field( - description="Model name sent to the provider.", min_length=1, title="Model" - ), - ] - secret_name: Annotated[ - str, - Field( - description="Name of a credential secret owned by the route's team.", - min_length=1, - title="Secret Name", + description="Target kind for a provider reached through a team connection.", + title="Type", ), ] - - -class RouteTargetConfigAnthropic(BaseModel): - model_config = ConfigDict( - extra="forbid", - ) - type: Annotated[ - Literal["ANTHROPIC"], - Field(description="Target kind for Anthropic.", title="Type"), - ] model: Annotated[ str, Field( description="Model name sent to the provider.", min_length=1, title="Model" ), ] - secret_name: Annotated[ + connection_id: Annotated[ str, Field( - description="Name of a credential secret owned by the route's team.", - min_length=1, - title="Secret Name", + description="Identifier of the team connection authenticating with the provider.", + title="Connection Id", ), ] -class RouteTargetConfigBasetenModelAPI(BaseModel): +class UpsertRouteTargetBasetenModelAPI(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -7951,7 +8341,7 @@ class ClassifierEnvironmentName(RootModel[str]): ] = None -class RouteTargetConfigClassifierModelBased(BaseModel): +class UpsertRouteTargetClassifierModelBased(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -7997,48 +8387,30 @@ class RouteTargetConfigClassifierModelBased(BaseModel): ] -class RouteTargetConfigOpenAI(BaseModel): +class UpsertRouteTargetConnection(BaseModel): model_config = ConfigDict( extra="forbid", ) type: Annotated[ - Literal["OPENAI"], Field(description="Target kind for OpenAI.", title="Type") - ] - model: Annotated[ - str, - Field( - description="Model name sent to the provider.", min_length=1, title="Model" - ), - ] - secret_name: Annotated[ - str, + Literal["CONNECTION"], Field( - description="Name of a credential secret owned by the route's team.", - min_length=1, - title="Secret Name", + description="Target kind for a provider reached through a team connection.", + title="Type", ), ] - - -class RouteTargetConfigXAI(BaseModel): - model_config = ConfigDict( - extra="forbid", - ) - type: Annotated[ - Literal["XAI"], Field(description="Target kind for xAI.", title="Type") - ] model: Annotated[ str, Field( description="Model name sent to the provider.", min_length=1, title="Model" ), ] - secret_name: Annotated[ + connection_id: Annotated[ str, Field( - description="Name of a credential secret owned by the route's team.", + description="Identifier of a connection owned by the route's team. Its provider serves the route.", + examples=["abc1234"], min_length=1, - title="Secret Name", + title="Connection Id", ), ] @@ -8102,13 +8474,11 @@ class CreateRouteRequest(BaseModel): ] = None target: Annotated[ Annotated[ - RouteTargetConfigBasetenModelAPI - | RouteTargetConfigAnthropic - | RouteTargetConfigOpenAI - | RouteTargetConfigXAI - | RouteTargetConfigClassifierModelBased, + UpsertRouteTargetBasetenModelAPI + | UpsertRouteTargetConnection + | UpsertRouteTargetClassifierModelBased, Field( - description="Upstream target for the route.", + description="Upstream target for the route. CONNECTION targets use the provider and credentials of a connection owned by the route's team.", discriminator="type", examples=[{"model": "zai-org/GLM-5.3", "type": "BASETEN_MODEL_API"}], title="Target", @@ -8135,6 +8505,13 @@ class RoutesUsageResult(BaseModel): title="User Id", ), ] = None + team_id: Annotated[ + str | None, + Field( + description="ID of the team the usage was metered in. Null when not grouping by TEAM, and for usage metered before it was attributed to teams.", + title="Team Id", + ), + ] = None model: Annotated[ str | None, Field( @@ -8157,6 +8534,13 @@ class RoutesUsageResult(BaseModel): description="Provider that served the requests. Null when not grouping by PROVIDER." ), ] = None + router_id: Annotated[ + str | None, + Field( + description="Slug of the intelligent router, matching its name in `GET /v1/routes`. Null when not grouping by ROUTER.", + title="Router Id", + ), + ] = None cost_usd: Annotated[ str, Field( @@ -8189,13 +8573,20 @@ class RoutesUsageResult(BaseModel): output_tokens: Annotated[ int, Field(description="Output tokens.", title="Output Tokens") ] + requests: Annotated[ + int, + Field( + description="Number of requests; with ROUTER, the requests sent through that router. Usage recorded before requests were counted adds zero.", + title="Requests", + ), + ] class RouteSpendLimitTeamDefault(BaseModel): team_id: Annotated[ str, Field( - description="ID of the team the user's active Code key belongs to.", + description="ID of the team whose per-member limit this is.", examples=["abc1234"], title="Team Id", ), @@ -8210,41 +8601,23 @@ class RouteSpendLimitTeamDefault(BaseModel): ] -class UpdateRouteSpendLimitSetting(BaseModel): - model_config = ConfigDict( - extra="forbid", - ) - user_monthly_limit_usd: Annotated[ - str | None, - Field( - description="Standing spend limit in USD for each UTC calendar month. Send null to remove the limit; omit to leave it unchanged.", - examples=["200"], - title="User Monthly Limit Usd", - ), - ] = None - - -class UpdateRouteUserSettingsRequest(BaseModel): - model_config = ConfigDict( - extra="forbid", - ) - spend_limit: Annotated[ - UpdateRouteSpendLimitSetting | None, - Field( - description="Spend limit fields to change. Pass null to remove the user's limit; omit to leave it unchanged." - ), - ] = None - - class RouteTeamSpendLimitSetting(BaseModel): per_member_monthly_limit_usd: Annotated[ str | None, Field( - description="Spend limit in USD for each UTC calendar month that applies to each member whose active Code key belongs to this team, unless the member has a limit of their own. Returned as an exact decimal string. Null when the team has no per-member limit.", + description="Spend limit in USD for each UTC calendar month that applies to each member's Code spend in this team, unless the member has a limit of their own. Returned as an exact decimal string. Null when the team has no per-member limit.", examples=["200"], title="Per Member Monthly Limit Usd", ), ] + team_wide_monthly_limit_usd: Annotated[ + str | None, + Field( + description="Spend limit in USD for each UTC calendar month on the Code spend of all members in this team together. Once it is reached, every member's requests in the team are rejected. Returned as an exact decimal string. Null when the team has no team-wide limit.", + examples=["5000"], + title="Team Wide Monthly Limit Usd", + ), + ] class Primary(RootModel[str]): @@ -8344,11 +8717,19 @@ class UpdateRouteTeamSpendLimitSetting(BaseModel): per_member_monthly_limit_usd: Annotated[ str | None, Field( - description="Spend limit in USD for each UTC calendar month that applies to each member whose active Code key belongs to this team, unless the member has a limit of their own. Send null to remove it; omit to leave it unchanged.", + description="Spend limit in USD for each UTC calendar month that applies to each member's Code spend in this team, unless the member has a limit of their own. Send null to remove it; omit to leave it unchanged.", examples=["200"], title="Per Member Monthly Limit Usd", ), ] = None + team_wide_monthly_limit_usd: Annotated[ + str | None, + Field( + description="Spend limit in USD for each UTC calendar month on the Code spend of all members in this team together. Send null to remove it; omit to leave it unchanged.", + examples=["5000"], + title="Team Wide Monthly Limit Usd", + ), + ] = None class UpdateRouteTeamSettingsRequest(BaseModel): @@ -8364,12 +8745,46 @@ class UpdateRouteTeamSettingsRequest(BaseModel): spend_limit: Annotated[ UpdateRouteTeamSpendLimitSetting | None, Field( - description="Spend limit fields to change. Pass null to remove the team's per-member limit; omit to leave it unchanged." + description="Spend limit fields to change. Pass null to remove the team's limits; omit to leave them unchanged." + ), + ] = None + + +class UpdateRouteSpendLimitSetting(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + user_monthly_limit_usd: Annotated[ + str | None, + Field( + description="Standing spend limit in USD for each UTC calendar month. Send null to remove the limit; omit to leave it unchanged.", + examples=["200"], + title="User Monthly Limit Usd", + ), + ] = None + month_override_usd: Annotated[ + str | None, + Field( + description="Spend limit in USD for the current UTC calendar month, which supersedes the standing limit. Send null to remove it; omit to leave it unchanged.", + examples=["300"], + title="Month Override Usd", ), ] = None -class RouteConnectionAnthropic(BaseModel): +class UpdateRouteUserSettingsRequest(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + spend_limit: Annotated[ + UpdateRouteSpendLimitSetting | None, + Field( + description="Spend limit fields to change. Pass null to remove the user's limit; omit to leave it unchanged." + ), + ] = None + + +class RouteConnectionConfigAnthropic(BaseModel): provider: Annotated[ Literal["ANTHROPIC"], Field(description="Provider kind for Anthropic.", title="Provider"), @@ -8390,7 +8805,7 @@ class RouteConnectionAnthropic(BaseModel): ] -class RouteConnectionOpenAI(BaseModel): +class RouteConnectionConfigOpenAI(BaseModel): provider: Annotated[ Literal["OPENAI"], Field(description="Provider kind for OpenAI.", title="Provider"), @@ -8411,7 +8826,7 @@ class RouteConnectionOpenAI(BaseModel): ] -class RouteConnectionXAI(BaseModel): +class RouteConnectionConfigXAI(BaseModel): provider: Annotated[ Literal["XAI"], Field(description="Provider kind for xAI.", title="Provider") ] @@ -8431,7 +8846,48 @@ class RouteConnectionXAI(BaseModel): ] -class RouteConnectionConfigAnthropic(BaseModel): +class RouteConnection(BaseModel): + id: Annotated[str, Field(description="Stable connection identifier.", title="Id")] + team_id: Annotated[ + str, + Field( + description="Identifier of the team that owns the connection.", + title="Team Id", + ), + ] + config: Annotated[ + Annotated[ + RouteConnectionConfigAnthropic + | RouteConnectionConfigOpenAI + | RouteConnectionConfigXAI, + Field( + description="Provider the connection authenticates with, and the team secret holding its API key.", + discriminator="provider", + title="Config", + ), + ] + | dict[str, Any], + Field(union_mode="left_to_right"), + ] + created_at: Annotated[ + AwareDatetime, Field(description="Creation time, ISO 8601.", title="Created At") + ] + updated_at: Annotated[ + AwareDatetime, + Field(description="Last update time, ISO 8601.", title="Updated At"), + ] + + +class RouteConnectionsResponse(BaseModel): + items: Annotated[ + list[RouteConnection], Field(description="Items in this page.", title="Items") + ] + pagination: Annotated[ + PaginationResponse, Field(description="Pagination metadata for the page.") + ] + + +class UpsertRouteConnectionConfigAnthropic(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -8449,7 +8905,7 @@ class RouteConnectionConfigAnthropic(BaseModel): ] -class RouteConnectionConfigOpenAI(BaseModel): +class UpsertRouteConnectionConfigOpenAI(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -8467,7 +8923,7 @@ class RouteConnectionConfigOpenAI(BaseModel): ] -class RouteConnectionConfigXAI(BaseModel): +class UpsertRouteConnectionConfigXAI(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -8496,101 +8952,40 @@ class TeamId2(RootModel[str]): ] = None -class CreateRouteConnectionRequest(BaseModel): - model_config = ConfigDict( - extra="forbid", - ) - team_id: Annotated[ - TeamId2 | None, - Field( - description="Identifier of the team that owns the connection.", - examples=["abc1234"], - title="Team Id", - ), - ] = None - config: Annotated[ - Annotated[ - RouteConnectionConfigAnthropic - | RouteConnectionConfigOpenAI - | RouteConnectionConfigXAI, - Field( - description="Provider the connection authenticates with, and the team secret holding its API key.", - discriminator="provider", - examples=[{"provider": "ANTHROPIC", "secret_id": "abc1234"}], - title="Config", - ), - ] - | dict[str, Any], - Field(union_mode="left_to_right"), - ] - - -class RouteConnectionTombstone(BaseModel): - id: Annotated[ - str, - Field(description="Stable identifier of the deleted connection.", title="Id"), - ] - - -class SecretId(RootModel[str]): - root: Annotated[ - str | None, - Field( - description="Identifier of the new secret, owned by the connection's team, that holds the provider API key. Omit to keep the current secret.", - min_length=1, - title="Secret Id", - ), - ] = None - - -class UpdateRouteConnectionConfigAnthropic(BaseModel): - model_config = ConfigDict( - extra="forbid", - ) - provider: Annotated[ - Literal["ANTHROPIC"], - Field(description="Provider kind for Anthropic.", title="Provider"), - ] - secret_id: Annotated[ - SecretId | None, - Field( - description="Identifier of the new secret, owned by the connection's team, that holds the provider API key. Omit to keep the current secret.", - title="Secret Id", - ), - ] = None - - -class UpdateRouteConnectionConfigOpenAI(BaseModel): +class CreateRouteConnectionRequest(BaseModel): model_config = ConfigDict( extra="forbid", ) - provider: Annotated[ - Literal["OPENAI"], - Field(description="Provider kind for OpenAI.", title="Provider"), - ] - secret_id: Annotated[ - SecretId | None, + team_id: Annotated[ + TeamId2 | None, Field( - description="Identifier of the new secret, owned by the connection's team, that holds the provider API key. Omit to keep the current secret.", - title="Secret Id", + description="Identifier of the team that owns the connection.", + examples=["abc1234"], + title="Team Id", ), ] = None + config: Annotated[ + Annotated[ + UpsertRouteConnectionConfigAnthropic + | UpsertRouteConnectionConfigOpenAI + | UpsertRouteConnectionConfigXAI, + Field( + description="Provider the connection authenticates with, and the team secret holding its API key.", + discriminator="provider", + examples=[{"provider": "ANTHROPIC", "secret_id": "abc1234"}], + title="Config", + ), + ] + | dict[str, Any], + Field(union_mode="left_to_right"), + ] -class UpdateRouteConnectionConfigXAI(BaseModel): - model_config = ConfigDict( - extra="forbid", - ) - provider: Annotated[ - Literal["XAI"], Field(description="Provider kind for xAI.", title="Provider") +class RouteConnectionTombstone(BaseModel): + id: Annotated[ + str, + Field(description="Stable identifier of the deleted connection.", title="Id"), ] - secret_id: Annotated[ - SecretId | None, - Field( - description="Identifier of the new secret, owned by the connection's team, that holds the provider API key. Omit to keep the current secret.", - title="Secret Id", - ), - ] = None class UpdateRouteConnectionRequest(BaseModel): @@ -8599,11 +8994,11 @@ class UpdateRouteConnectionRequest(BaseModel): ) config: Annotated[ Annotated[ - UpdateRouteConnectionConfigAnthropic - | UpdateRouteConnectionConfigOpenAI - | UpdateRouteConnectionConfigXAI, + UpsertRouteConnectionConfigAnthropic + | UpsertRouteConnectionConfigOpenAI + | UpsertRouteConnectionConfigXAI, Field( - description="Connection fields to change. The provider must match the connection and is immutable.", + description="Complete new connection configuration, replacing the current one. The provider must match the connection and is immutable.", discriminator="provider", examples=[{"provider": "ANTHROPIC", "secret_id": "abc1234"}], title="Config", @@ -8667,11 +9062,9 @@ class UpdateRouteRequest(BaseModel): ), ] = None target: Annotated[ - RouteTargetConfigBasetenModelAPI - | RouteTargetConfigAnthropic - | RouteTargetConfigOpenAI - | RouteTargetConfigXAI - | RouteTargetConfigClassifierModelBased + UpsertRouteTargetBasetenModelAPI + | UpsertRouteTargetConnection + | UpsertRouteTargetClassifierModelBased | None, Field( description="Complete new target configuration. Omit to keep the current configuration; null is not accepted. Only the CLASSIFIER_MODEL_BASED configuration of a router route is mutable.", @@ -9099,27 +9492,6 @@ class RegisterAPIKeyResponse(BaseModel): ] -class SandboxRegion(BaseModel): - name: Annotated[ - str, - Field( - description="Public region identifier to use when creating a sandbox.", - examples=["us-was-1"], - ), - ] - country: Annotated[str, Field(description="Country code.", examples=["us"])] - continent: Annotated[str, Field(description="Continent code.", examples=["na"])] - location: Annotated[ - str, Field(description="Region location.", examples=["Washington"]) - ] - info_generation: Annotated[ - Literal["CARBON"] | str, - Field( - description="Runtime generation supported by this region, using the public name CARBON. Actual runtime selection depends on the team and sandbox configuration." - ), - ] - - class SandboxLogEntry(BaseModel): timestamp: AwareDatetime message: str @@ -9136,41 +9508,69 @@ class SandboxLogEntry(BaseModel): ] = None -class ErrorRate(RootModel[float]): - root: Annotated[ - float, +class SandboxMetricDescriptor(BaseModel): + name: ( + Literal[ + "baseten_sandbox_requests_total", + "baseten_sandbox_cpu_usage_cores", + "baseten_sandbox_memory_bytes", + ] + | str + ) + unit_hint: Annotated[ + Literal["COUNT", "CORES", "BYTES"] | str, + Field( + description="Requests use COUNT, CPU uses CORES (1 means one fully used core), and memory uses BYTES." + ), + ] + kind: Literal["GAUGE", "COUNTER"] | str + label_sets: Annotated[ + list[dict[str, str]], Field( - description="Fraction of requests returning 4xx or 5xx. Null when there are no requests.", - ge=0.0, - le=1.0, + description="Requests use status=2xx,4xx,5xx; CPU and memory use stat=avg,max.\n", + max_length=3, ), ] -class SandboxMetricsPoint(BaseModel): - timestamp: Annotated[AwareDatetime, Field(description="Start of this interval.")] - requests: Annotated[ - float | None, +class Value(RootModel[list[float | None]]): + root: Annotated[list[float | None], Field(max_length=3)] + + +class SandboxMetricValueSet(BaseModel): + start_epoch_millis: Annotated[ + int, Field( - description="Request count. Zero without traffic; null for intervals entirely before creation." + description="Start of the SERIES interval or SUMMARY window; snapshot timestamp in CURRENT." ), ] - cpu_percent: Annotated[ - float | None, + values: Annotated[ + list[Value], Field( - description="Peak CPU usage in this interval, where 100 is one full CPU core. Null without a sample." + description="Values indexed first by metric_descriptors, then by that descriptor's label_sets. Null means no sample.", + max_length=3, + min_length=1, ), ] - memory_bytes: Annotated[ - float | None, + + +class SandboxRegion(BaseModel): + name: Annotated[ + str, Field( - description="Memory usage in bytes, averaged per series then maximum across series. Null without a sample." + description="Public region identifier to use when creating a sandbox.", + examples=["us-was-1"], ), ] - error_rate: Annotated[ - ErrorRate | None, + country: Annotated[str, Field(description="Country code.", examples=["us"])] + continent: Annotated[str, Field(description="Continent code.", examples=["na"])] + location: Annotated[ + str, Field(description="Region location.", examples=["Washington"]) + ] + info_generation: Annotated[ + Literal["CARBON"] | str, Field( - description="Fraction of requests returning 4xx or 5xx. Null when there are no requests." + description="Runtime generation supported by this region, using the public name CARBON. Actual runtime selection depends on the team and sandbox configuration." ), ] @@ -9257,7 +9657,10 @@ class SandboxProxyTarget(BaseModel): class SandboxEnv(BaseModel): name: Annotated[ str | None, - Field(description="Name of the environment variable", examples=["NODE_ENV"]), + Field( + description="Name of the environment variable. Must start with a letter or underscore and contain only letters, digits and underscores (^[A-Za-z_][A-Za-z0-9_]*$); create and update reject other names.", + examples=["NODE_ENV"], + ), ] = None secret: Annotated[ bool, @@ -9325,7 +9728,7 @@ class SandboxImageTag(BaseModel): ] = None -class SandboxImageSummary(BaseModel): +class SandboxImage(BaseModel): name: Annotated[ str, Field( @@ -9376,16 +9779,6 @@ class SandboxImageSummary(BaseModel): ] = None -class SandboxImage(SandboxImageSummary): - tags: Annotated[ - list[SandboxImageTag] | None, - Field( - description="Empty for get summary responses. Use GET /sandboxes/images/{image_name}/tags to retrieve paginated image versions.", - examples=[[]], - ), - ] = None - - class PushSandboxImageRequest(BaseModel): name: Annotated[ str, @@ -9460,7 +9853,7 @@ class PushSandboxImageResponse(BaseModel): image: Annotated[ str | None, Field( - description="Registered image reference including its tag, when available. Tags are assigned by the service; GET /sandboxes/images/{image_name} returns the available tags once processing completes.", + description="Registered image reference including its tag, when available. Tags are assigned by the service; GET /sandboxes/images/{image_name}/tags returns the available tags once processing completes.", examples=["b10/base-image:latest"], ), ] = None @@ -9884,6 +10277,83 @@ class GetModelsDeploymentsMetricsParams(BaseModel): ] = None +class GetModelsDeploymentsStartupsParams(BaseModel): + start_epoch_millis: Annotated[ + int, + Field( + description="Start of the query window, in epoch milliseconds.", + title="Start Epoch Millis", + ), + ] + end_epoch_millis: Annotated[ + int, + Field( + description="End of the query window, in epoch milliseconds. Must be after the start.", + title="End Epoch Millis", + ), + ] + min_duration_ms: Annotated[ + MinDurationMs | None, + Field( + description="Only include startups that lasted at least this many milliseconds.", + title="Min Duration Ms", + ), + ] = None + cursor: Annotated[ + str | None, + Field( + description="Opaque cursor returned by a previous page. Omit to fetch the first page. Pass the same filters and sort as the request that returned it.", + title="Cursor", + ), + ] = None + limit: Annotated[ + int, + Field( + description="Maximum number of attempts to return.", + ge=1, + le=100, + title="Limit", + ), + ] = 50 + outcomes: Annotated[ + list[Literal["SUCCESS", "FAILED", "STALLED"] | str] | None, + Field( + description="Only include attempts with one of these outcomes. Repeat to pass several.", + title="Outcomes", + ), + ] = None + phase: Annotated[ + Literal[ + "SCHEDULING", + "IMAGE_PULL", + "WEIGHT_DOWNLOAD", + "CONTAINER_CREATE", + "CONTAINER_START", + "READINESS", + ] + | str + | None, + Field( + description="Only include attempts that spent longer than 0 ms in this phase." + ), + ] = None + search: Annotated[ + str | None, + Field( + description="Only include attempts whose replica ID matches exactly, or whose deployment name contains this text (case-insensitive).", + title="Search", + ), + ] = None + sort_by: Annotated[ + Literal["OUTCOME", "STARTED_AT", "READY_AT", "DURATION"] | str | None, + Field(description="Field to sort attempts by. Defaults to `STARTED_AT`."), + ] = None + direction: Annotated[ + Literal["asc", "desc"] | str | None, + Field(description="Sort direction. Defaults to `desc`."), + ] = None + + class GetModelsEnvironmentsLogsParams(BaseModel): start_epoch_millis: Annotated[ int | None, @@ -9990,6 +10460,83 @@ class GetModelsEnvironmentsMetricsParams(BaseModel): ] = None +class GetModelsEnvironmentsStartupsParams(BaseModel): + start_epoch_millis: Annotated[ + int, + Field( + description="Start of the query window, in epoch milliseconds.", + title="Start Epoch Millis", + ), + ] + end_epoch_millis: Annotated[ + int, + Field( + description="End of the query window, in epoch milliseconds. Must be after the start.", + title="End Epoch Millis", + ), + ] + min_duration_ms: Annotated[ + MinDurationMs | None, + Field( + description="Only include startups that lasted at least this many milliseconds.", + title="Min Duration Ms", + ), + ] = None + cursor: Annotated[ + str | None, + Field( + description="Opaque cursor returned by a previous page. Omit to fetch the first page. Pass the same filters and sort as the request that returned it.", + title="Cursor", + ), + ] = None + limit: Annotated[ + int, + Field( + description="Maximum number of attempts to return.", + ge=1, + le=100, + title="Limit", + ), + ] = 50 + outcomes: Annotated[ + list[Literal["SUCCESS", "FAILED", "STALLED"] | str] | None, + Field( + description="Only include attempts with one of these outcomes. Repeat to pass several.", + title="Outcomes", + ), + ] = None + phase: Annotated[ + Literal[ + "SCHEDULING", + "IMAGE_PULL", + "WEIGHT_DOWNLOAD", + "CONTAINER_CREATE", + "CONTAINER_START", + "READINESS", + ] + | str + | None, + Field( + description="Only include attempts that spent longer than 0 ms in this phase." + ), + ] = None + search: Annotated[ + str | None, + Field( + description="Only include attempts whose replica ID matches exactly, or whose deployment name contains this text (case-insensitive).", + title="Search", + ), + ] = None + sort_by: Annotated[ + Literal["OUTCOME", "STARTED_AT", "READY_AT", "DURATION"] | str | None, + Field(description="Field to sort attempts by. Defaults to `STARTED_AT`."), + ] = None + direction: Annotated[ + Literal["asc", "desc"] | str | None, + Field(description="Sort direction. Defaults to `desc`."), + ] = None + + class GetChainsAuditLogsParams(BaseModel): cursor: Annotated[ str | None, @@ -10295,9 +10842,9 @@ class GetModelApisUsageParams(BaseModel): ), ] = "1d" group_by: Annotated[ - list[Literal["api_key", "user", "model"] | str] | None, + list[Literal["api_key", "user", "model", "team"] | str] | None, Field( - description="Dimensions to break usage down by, repeated once per dimension: api_key, user, model. Defaults to model.", + description="Dimensions to break usage down by, repeated once per dimension: api_key, user, model, or team. Defaults to model.", title="Group By", ), ] = None @@ -10322,6 +10869,13 @@ class GetModelApisUsageParams(BaseModel): title="Models", ), ] = None + team_ids: Annotated[ + list[str] | None, + Field( + description="Return only usage for these teams, repeated once per team. Defaults to all teams you can manage.", + title="Team Ids", + ), + ] = None limit: Annotated[ int | None, Field( @@ -10423,6 +10977,13 @@ class GetRoutesUsageParams(BaseModel): title="Limit", ), ] = 7 + team_id: Annotated[ + str | None, + Field( + description="Limit usage to a team you can access. Organization admins can view all usage in the team, team admins can view usage in teams they manage, and other members can view only their own usage. Without a team, organization admins see all organization usage, team admins see their managed teams, and other members see their own usage.", + title="Team Id", + ), + ] = None start_date: Annotated[ date_aliased | None, Field( @@ -10438,9 +10999,9 @@ class GetRoutesUsageParams(BaseModel): ), ] = None group_by: Annotated[ - list[Literal["USER", "MODEL", "PROVIDER"] | str] | None, + list[Literal["USER", "TEAM", "MODEL", "PROVIDER", "ROUTER"] | str] | None, Field( - description="Dimensions to break usage down by, repeated once per dimension: USER, MODEL, or PROVIDER. Each result represents one observed combination of the requested dimensions within that day, and results are sorted by those values. Combinations without usage are omitted, so result counts can differ between days. Defaults to MODEL.", + description="Dimensions to break usage down by, repeated once per dimension: USER, TEAM, MODEL, PROVIDER, or ROUTER. Each result represents one observed combination of the requested dimensions within that day, and results are sorted by those values. Combinations without usage are omitted, so result counts can differ between days. ROUTER returns only usage sent through an intelligent router, one row per router, or per router and model with MODEL. Router rows repeat usage already counted in other groupings, so don't add them to model totals. Defaults to MODEL.", title="Group By", ), ] = None @@ -10451,29 +11012,75 @@ class GetRoutesUsageParams(BaseModel): title="User Ids", ), ] = None - models: Annotated[ - list[str] | None, + models: Annotated[ + list[str] | None, + Field( + description="Return only usage for these exact model names, repeated once per model.", + title="Models", + ), + ] = None + providers: Annotated[ + list[ + Literal[ + "BASETEN_MODEL_API", + "OPENAI", + "ANTHROPIC", + "XAI", + "VERTEX", + "OPENAI_COMPATIBLE", + ] + | str + ] + | None, + Field( + description="Return only usage for these providers, repeated once per provider.", + title="Providers", + ), + ] = None + + +class GetSandboxMetricsParams(BaseModel): + team_id: Annotated[ + str | None, + Field( + description="Optional team ID. Must match X-Team-Id when both are supplied. If neither selector is supplied, defaults to the caller's only accessible team. Callers with multiple accessible teams must select a team. Requests without access to any team are forbidden.", + min_length=1, + ), + ] = None + mode: Annotated[ + Literal["CURRENT", "SUMMARY", "SERIES"] | str | None, + Field( + description="Defaults to CURRENT, which rejects start_epoch_millis and end_epoch_millis. SERIES uses a server-selected step of 30 seconds up to 2 hours, 60 seconds up to 1 day, 600 seconds up to 3 days, and 3600 seconds beyond 3 days." + ), + ] = None + start_epoch_millis: Annotated[ + int | None, + Field( + description="Unix milliseconds. Defaults to one hour before end; SERIES snaps outward.", + ge=0, + ), + ] = None + end_epoch_millis: Annotated[ + int | None, Field( - description="Return only usage for these exact model names, repeated once per model.", - title="Models", + description="Exclusive end in Unix milliseconds, default now. Maximum aligned window is 7 days.", + ge=0, ), ] = None - providers: Annotated[ + metrics: Annotated[ list[ Literal[ - "BASETEN_MODEL_API", - "OPENAI", - "ANTHROPIC", - "XAI", - "VERTEX", - "OPENAI_COMPATIBLE", + "baseten_sandbox_requests_total", + "baseten_sandbox_cpu_usage_cores", + "baseten_sandbox_memory_bytes", ] | str ] | None, Field( - description="Return only usage for these providers, repeated once per provider.", - title="Providers", + description="Metric names in response order. Omit to request all three signals.", + max_length=3, + min_length=1, ), ] = None @@ -10945,6 +11552,39 @@ class GetModelMetricsResponse(BaseModel): ] +class StartupPhaseSummary(BaseModel): + phase: Annotated[ + Literal[ + "SCHEDULING", + "IMAGE_PULL", + "WEIGHT_DOWNLOAD", + "CONTAINER_CREATE", + "CONTAINER_START", + "READINESS", + ] + | str, + Field(description="The phase.", title="StartupPhaseV1"), + ] + status: Annotated[ + Literal["COMPLETED", "FAILED"] | str, + Field(description="Outcome of the phase.", title="StartupPhaseStatusV1"), + ] + offset_ms: Annotated[ + int, + Field( + description="Time from the start of the attempt to the start of the phase, in milliseconds.", + title="Offset Ms", + ), + ] + duration_ms: Annotated[ + int | None, + Field( + description="Duration of the phase, in milliseconds. Can be 0 for a phase that finished within a second. Null if the phase did not finish.", + title="Duration Ms", + ), + ] + + class AutoscalingSchedule(BaseModel): id: Annotated[ str, Field(description="Stable unique identifier of the schedule", title="Id") @@ -11894,6 +12534,13 @@ class LoopsRun(BaseModel): title="Base Model", ), ] + lora_config_version: Annotated[ + str | None, + Field( + description="The run's immutable LoRA config version.", + title="Lora Config Version", + ), + ] = None base_url: Annotated[str, Field(description="The run's base URL.", title="Base Url")] created_at: Annotated[ AwareDatetime, @@ -11926,6 +12573,72 @@ class GetLoopsRunResponse(BaseModel): ] +class LoopsCheckpoint(BaseModel): + checkpoint_id: Annotated[ + str, Field(description="The ID of the checkpoint.", title="Checkpoint Id") + ] + created_at: Annotated[ + AwareDatetime, + Field( + description="The timestamp of the checkpoint in ISO 8601 format.", + title="Created At", + ), + ] + checkpoint_type: Annotated[ + str, Field(description="The type of checkpoint.", title="Checkpoint Type") + ] + base_model: Annotated[ + str | None, + Field(description="The base model of the checkpoint.", title="Base Model"), + ] + lora_adapter_config: Annotated[ + dict[str, Any] | None, + Field( + description="The adapter config of the checkpoint.", + title="Lora Adapter Config", + ), + ] + size_bytes: Annotated[ + int, + Field(description="The size of the checkpoint in bytes.", title="Size Bytes"), + ] + sync_status: Annotated[ + str | None, + Field( + description="Sync state of the checkpoint: SYNCING or COMPLETE.", + title="Sync Status", + ), + ] = None + id: Annotated[str, Field(description="The checkpoint ID.", title="Id")] + lora_config: Annotated[ + LoopsLoRAConfig | None, + Field( + description="The immutable LoRA config used to save this checkpoint. Null means the checkpoint is unsupported and cannot be loaded." + ), + ] + run_id: Annotated[ + str, + Field( + description="The ID of the run that produced the checkpoint.", + title="Run Id", + ), + ] + target: Annotated[ + Literal["sampler", "trainer"] | str, + Field( + description="Whether this checkpoint is loadable by the sampler or by the run.", + title="TrainerCheckpointTarget", + ), + ] + + +class ListLoopsCheckpointsResponse(BaseModel): + checkpoints: Annotated[ + list[LoopsCheckpoint], + Field(description="Matching checkpoints.", title="Checkpoints"), + ] + + class LoopsDeploymentNodeMetrics(BaseModel): node_id: Annotated[ str, Field(description="Identifier for the node.", title="Node Id") @@ -12595,9 +13308,7 @@ class Route(BaseModel): target: Annotated[ Annotated[ RouteTargetBasetenModelAPI - | RouteTargetAnthropic - | RouteTargetOpenAI - | RouteTargetXAI + | RouteTargetConnection | RouteTargetClassifierModelBased, Field( description="Configured upstream target.", @@ -12682,24 +13393,49 @@ class RouteSpendLimitSetting(BaseModel): user_monthly_limit_usd: Annotated[ str | None, Field( - description="Standing spend limit in USD for each UTC calendar month set on the user. Null when the user has no limit of their own.", + description="Standing spend limit in USD for each UTC calendar month set on the user in the team. Null when the user has no limit of their own there.", examples=["200"], title="User Monthly Limit Usd", ), ] + month_override_usd: Annotated[ + str | None, + Field( + description="Spend limit in USD for the current UTC calendar month set on the user in the team, which supersedes their standing limit. Null when there is none.", + examples=["300"], + title="Month Override Usd", + ), + ] team_default: Annotated[ RouteSpendLimitTeamDefault | None, Field( - description="Per-member limit of the team the user's active Code key belongs to. This limit applies when the user has no limit of their own. Null when there is none." + description="Per-member limit of the team. This limit applies when the user has no limit of their own. Null when there is none." ), ] effective: Annotated[ RouteEffectiveSpendLimit, Field(description="The effective limit enforced for the current month."), ] + team_wide_monthly_limit_usd: Annotated[ + str | None, + Field( + description="Team-wide limit of the team in USD for each UTC calendar month. Once all members' spend in the team reaches it, the user's requests in the team are rejected whatever their own limit. Null when there is none.", + examples=["5000"], + title="Team Wide Monthly Limit Usd", + ), + ] -class RouteUserSettings(BaseModel): +class RouteTeamUserSettings(BaseModel): + team_id: Annotated[ + str, Field(description="ID of the team.", examples=["abc1234"], title="Team Id") + ] + team_name: Annotated[ + str, + Field( + description="Name of the team.", examples=["Engineering"], title="Team Name" + ), + ] user_id: Annotated[ str, Field(description="ID of the user.", examples=["abc1234"], title="User Id") ] @@ -12714,7 +13450,38 @@ class RouteUserSettings(BaseModel): spend_limit: Annotated[ RouteSpendLimitSetting, Field( - description="Spend limit for Baseten Code. Applies to requests with Routes keys the user created; personal API keys are not limited." + description="Spend limit for Baseten Code in the team. Applies to requests with Routes keys the user created in the team; personal API keys are not limited." + ), + ] + month_spend_usd: Annotated[ + str, + Field( + description="The user's metered Code spend in USD in the team this UTC calendar month, returned as an exact decimal string.", + examples=["12.5"], + title="Month Spend Usd", + ), + ] + is_blocked: Annotated[ + bool, + Field( + description="Whether the user's spend this month reached their limit, or the team's spend reached its team-wide limit, so requests with Routes keys they created in the team are rejected.", + title="Is Blocked", + ), + ] + blocked_by: Annotated[ + Literal["member", "team"] | str | None, + Field( + description="`member` when the user's spend this month reached their limit, `team` when the team's spend reached its team-wide limit. Null when the user is not blocked." + ), + ] + + +class RouteUserTeamSettingsList(BaseModel): + teams: Annotated[ + list[RouteTeamUserSettings], + Field( + description="The user's settings in each team they have a role in and in any team they have Code spend in this month, alphabetical by team name.", + title="Teams", ), ] @@ -12730,43 +13497,14 @@ class RouteHarnessModel(BaseModel): route: Annotated[Route, Field(description="Route to use for this role.")] -class RouteConnection(BaseModel): - id: Annotated[str, Field(description="Stable connection identifier.", title="Id")] - team_id: Annotated[ - str, +class RouteTeamUserSettingsList(BaseModel): + users: Annotated[ + list[RouteTeamUserSettings], Field( - description="Identifier of the team that owns the connection.", - title="Team Id", + description="Settings of each user with a role in the team and of anyone with Code spend in the team this month.", + title="Users", ), ] - config: Annotated[ - Annotated[ - RouteConnectionAnthropic | RouteConnectionOpenAI | RouteConnectionXAI, - Field( - description="Provider the connection authenticates with, and the team secret holding its API key.", - discriminator="provider", - title="Config", - ), - ] - | dict[str, Any], - Field(union_mode="left_to_right"), - ] - created_at: Annotated[ - AwareDatetime, Field(description="Creation time, ISO 8601.", title="Created At") - ] - updated_at: Annotated[ - AwareDatetime, - Field(description="Last update time, ISO 8601.", title="Updated At"), - ] - - -class RouteConnectionsResponse(BaseModel): - items: Annotated[ - list[RouteConnection], Field(description="Items in this page.", title="Items") - ] - pagination: Annotated[ - PaginationResponse, Field(description="Pagination metadata for the page.") - ] class EndpointTarget(BaseModel): @@ -12949,15 +13687,6 @@ class UpdateGroupRequest(BaseModel): ] = None -class GetSandboxConfigurationResponse(BaseModel): - regions: Annotated[ - list[SandboxRegion], - Field( - description="Available Carbon-compatible regions, sorted by name. Empty when none are available." - ), - ] - - class SandboxLogs(BaseModel): sandbox_name: str start_time: AwareDatetime @@ -12972,11 +13701,36 @@ class SandboxLogs(BaseModel): class SandboxMetrics(BaseModel): - sandbox_name: str - start_time: AwareDatetime - end_time: AwareDatetime - interval_seconds: int - data: Annotated[list[SandboxMetricsPoint], Field(max_length=10000)] + start_epoch_millis: Annotated[ + int, + Field( + description="Returned window start, snapped outward in SERIES. Equal to end for CURRENT." + ), + ] + end_epoch_millis: Annotated[ + int, + Field( + description="Returned exclusive window end, snapped outward in SERIES. Snapshot timestamp for CURRENT." + ), + ] + mode: Literal["CURRENT", "SUMMARY", "SERIES"] | str + step_seconds: Annotated[ + int | None, + Field(description="Seconds per SERIES interval; null in CURRENT and SUMMARY."), + ] + metric_descriptors: Annotated[ + list[SandboxMetricDescriptor], Field(max_length=3, min_length=1) + ] + metric_values: Annotated[list[SandboxMetricValueSet], Field(max_length=10000)] + + +class GetSandboxConfigurationResponse(BaseModel): + regions: Annotated[ + list[SandboxRegion], + Field( + description="Available Carbon-compatible regions, sorted by name. Empty when none are available." + ), + ] class SandboxProxyConfig(BaseModel): @@ -13028,7 +13782,7 @@ class SandboxProxyConfig(BaseModel): class ListSandboxImagesResponse(BaseModel): items: Annotated[ - list[SandboxImageSummary], Field(description="Image repositories on this page.") + list[SandboxImage], Field(description="Image repositories on this page.") ] pagination: SandboxApiPagination @@ -13177,6 +13931,9 @@ class AuditLogEntry(BaseModel): "VOLUME_DELETED", "VOLUME_VERSION_DELETED", "VOLUME_VERSION_RESTORED", + "VOLUME_TAG_SET", + "VOLUME_TAG_DELETED", + "VOLUME_UPDATED", ] | str, Field( @@ -13234,7 +13991,10 @@ class AuditLogEntry(BaseModel): | AuditLogEventSshCertificateSigned | AuditLogEventVolumeDeleted | AuditLogEventVolumeVersionDeleted - | AuditLogEventVolumeVersionRestored, + | AuditLogEventVolumeVersionRestored + | AuditLogEventVolumeTagSet + | AuditLogEventVolumeTagDeleted + | AuditLogEventVolumeUpdated, Field( description="Structured details of the action, discriminated by `event_type`.", discriminator="event_type", @@ -13283,6 +14043,78 @@ class ListAuditLogsResponse(BaseModel): ] +class StartupAttempt(BaseModel): + model_id: Annotated[ + str, Field(description="Unique identifier of the model.", title="Model Id") + ] + deployment_id: Annotated[ + str | None, + Field( + description="Unique identifier of the deployment.", title="Deployment Id" + ), + ] + deployment_name: Annotated[ + str, Field(description="Name of the deployment.", title="Deployment Name") + ] + environment_name: Annotated[ + str | None, + Field( + description="Name of the environment. Null when querying a deployment.", + title="Environment Name", + ), + ] + replica_id: Annotated[ + str | None, + Field( + description="Identifier of the replica that started.", title="Replica Id" + ), + ] + started_at_epoch_millis: Annotated[ + int, + Field( + description="Time the attempt started, in epoch milliseconds.", + title="Started At Epoch Millis", + ), + ] + ready_at_epoch_millis: Annotated[ + int | None, + Field( + description="Time the replica became ready, in epoch milliseconds. Null if it never did.", + title="Ready At Epoch Millis", + ), + ] + duration_ms: Annotated[ + int, + Field( + description="Duration of the attempt, in milliseconds.", title="Duration Ms" + ), + ] + outcome: Annotated[ + Literal["SUCCESS", "FAILED", "STALLED"] | str, + Field(description="Outcome of the attempt.", title="StartupOutcomeV1"), + ] + phase_summary: Annotated[ + list[StartupPhaseSummary], + Field(description="Phases recorded during the attempt.", title="Phase Summary"), + ] + + +class StartupAttempts(BaseModel): + items: Annotated[ + list[StartupAttempt], Field(description="Items in this page.", title="Items") + ] + pagination: Annotated[ + PaginationResponse, Field(description="Pagination metadata for the page.") + ] + total_count: Annotated[ + int, + Field( + description="Number of attempts matching the filters, counted within the 1,000 most recent startups in the window.", + title="Total Count", + ), + ] + + class PromotionSettings(BaseModel): redeploy_on_promotion: Annotated[ bool | None, @@ -13546,7 +14378,7 @@ class RouteTeamSettings(BaseModel): spend_limit: Annotated[ RouteTeamSpendLimitSetting, Field( - description="Spend limits for Baseten Code for members whose active Code key belongs to this team." + description="Spend limits for Baseten Code for members' spend in this team." ), ] @@ -13756,9 +14588,10 @@ class SandboxConfiguration(BaseModel): memory: Annotated[ int | None, Field( - description="Memory allocation in megabytes. Also determines CPU allocation (CPU cores = memory in MB / 2048, e.g., 4096MB = 2 CPUs).", + description="Memory allocation in megabytes. Also determines CPU allocation (CPU cores = memory in MB / 2048, e.g., 4096MB = 2 CPUs). Must be between 1024 and 262144.", examples=[4096], - ge=1, + ge=512, + le=262144, ), ] = None ports: Annotated[ @@ -13885,9 +14718,10 @@ class CreateSandboxRequest(BaseModel): memory: Annotated[ int, Field( - description="Memory allocation in megabytes. Also determines CPU allocation (CPU cores = memory in MB / 2048, e.g., 4096MB = 2 CPUs). Defaults to 4096.", + description="Memory allocation in megabytes. Also determines CPU allocation (CPU cores = memory in MB / 2048, e.g., 4096MB = 2 CPUs). Defaults to 4096. Must be between 1024 and 262144.", examples=[4096], - ge=1, + ge=512, + le=262144, ), ] = 4096 ports: Annotated[ diff --git a/baseten/client/modelconfig/_models.py b/baseten/client/modelconfig/_models.py index 7a92f40..e9d87e3 100644 --- a/baseten/client/modelconfig/_models.py +++ b/baseten/client/modelconfig/_models.py @@ -55,6 +55,20 @@ class BDNVolumeMount(BaseModel): title="Path", ), ] + include: Annotated[ + list[str] | None, + Field( + description="Relative glob patterns selecting files to mount; empty includes all files.", + title="Include", + ), + ] = None + exclude: Annotated[ + list[str] | None, + Field( + description="Relative glob patterns excluding files from the mount; exclusions win.", + title="Exclude", + ), + ] = None class DockerServer(BaseModel): diff --git a/baseten/sandbox/_images.py b/baseten/sandbox/_images.py index 83f87c0..6ba84ba 100644 --- a/baseten/sandbox/_images.py +++ b/baseten/sandbox/_images.py @@ -427,8 +427,9 @@ def push( volume: Scratch disk in megabytes, from 0 to 131072, attached to the build environment for intermediate layers. Set it for a large image that runs out of space while building or - importing. 0, the default, builds in memory. Teams with HIPAA - compliance enabled accept only 0. + importing. ``None`` or 0 builds in memory, which is the + default. Teams with HIPAA compliance enabled accept only + ``None`` or 0. wait: Whether to return only once the image is ready to use. timeout: How long to wait for the image. ``None`` waits with no limit. Processing continues regardless. @@ -1302,18 +1303,19 @@ def _upload_url( def _upload_headers( archive: _Archive, signed_headers: Mapping[str, str] | None -) -> dict[str, str]: +) -> httpx.Headers: """Give the headers of a build context upload. ``signed_headers`` are the ones a push returned with its upload URL, which is signed for them, so storage rejects an upload that leaves one out or changes it. They go as given, and are opaque. """ - return { - "Content-Type": "application/zip", - "Content-Length": str(archive.size), - **(signed_headers or {}), - } + headers = httpx.Headers( + {"Content-Type": "application/zip", "Content-Length": str(archive.size)} + ) + # Replaces ours case-insensitively, so a signed header is never sent twice. + headers.update(signed_headers or {}) + return headers def _pushed_info( @@ -1381,11 +1383,8 @@ def _add_log_page( ) -def _image_info( - image: baseten.client.managementapi.SandboxImage - | baseten.client.managementapi.SandboxImageSummary, -) -> ImageInfo: - """Convert an image record, from a listing or not.""" +def _image_info(image: baseten.client.managementapi.SandboxImage) -> ImageInfo: + """Convert an image record.""" return ImageInfo( name=image.name, status=image.status, diff --git a/scripts/apigen/specs/config.schema.json b/scripts/apigen/specs/config.schema.json index b87664c..73637ed 100644 --- a/scripts/apigen/specs/config.schema.json +++ b/scripts/apigen/specs/config.schema.json @@ -142,6 +142,22 @@ "minLength": 1, "title": "Path", "type": "string" + }, + "include": { + "description": "Relative glob patterns selecting files to mount; empty includes all files.", + "items": { + "type": "string" + }, + "title": "Include", + "type": "array" + }, + "exclude": { + "description": "Relative glob patterns excluding files from the mount; exclusions win.", + "items": { + "type": "string" + }, + "title": "Exclude", + "type": "array" } }, "required": [ diff --git a/scripts/apigen/specs/management.json b/scripts/apigen/specs/management.json index 8c3efbd..044dc35 100644 --- a/scripts/apigen/specs/management.json +++ b/scripts/apigen/specs/management.json @@ -444,6 +444,43 @@ } }, "x-stability": "unstable" + }, + "patch": { + "summary": "Updates a volume", + "x-codeSamples": [ + { + "lang": "bash", + "source": "curl --request PATCH \\\n--url https://api.baseten.co/v1/volumes/{volume_namespace}/{volume_name} \\\n--header \"Authorization: Bearer $BASETEN_API_KEY\" \\\n--data '{\n \"expires_at\": \"2030-01-01T00:00:00Z\",\n \"expected_sequence\": null\n}'" + }, + { + "lang": "python", + "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/volumes/{volume_namespace}/{volume_name}\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"PATCH\",\n url,\n headers=headers,\n json={'expires_at': '2030-01-01T00:00:00Z', 'expected_sequence': None}\n)\n\nprint(response.text)" + } + ], + "description": "Updates a volume's settings, such as its expiration time. Only the settings in the request change.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PatchVolumeRequestV1" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PatchVolumeResponseV1" + } + } + } + } + }, + "x-stability": "unstable" } }, "/v1/volumes/{volume_namespace}/{volume_name}/versions": { @@ -623,6 +660,103 @@ } ] }, + "/v1/volumes/{volume_namespace}/{volume_name}/tags": { + "post": { + "summary": "Sets a volume tag", + "x-codeSamples": [ + { + "lang": "bash", + "source": "curl --request POST \\\n--url https://api.baseten.co/v1/volumes/{volume_namespace}/{volume_name}/tags \\\n--header \"Authorization: Bearer $BASETEN_API_KEY\" \\\n--data '{\n \"tag\": \"nightly\",\n \"version\": \"head\",\n \"expires_at\": null,\n \"expected_sequence\": null\n}'" + }, + { + "lang": "python", + "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/volumes/{volume_namespace}/{volume_name}/tags\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"POST\",\n url,\n headers=headers,\n json={'tag': 'nightly', 'version': 'head', 'expires_at': None, 'expected_sequence': None}\n)\n\nprint(response.text)" + } + ], + "description": "Setting an existing tag overwrites it. The previous version remains available by digest. Pass `expires_at` to set the tag's expiration time. Omit `expires_at` to clear any expiration time on the tag. Version or volume expiration still deletes the tag. Setting `head` changes the version used by references without a tag or digest. You cannot set `head` on an expiring version or supply `expires_at` when setting `head`.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetVolumeTagRequestV1" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetVolumeTagResponseV1" + } + } + } + } + }, + "x-stability": "unstable" + }, + "parameters": [ + { + "$ref": "#/components/parameters/volume_namespace" + }, + { + "$ref": "#/components/parameters/volume_name" + } + ] + }, + "/v1/volumes/{volume_namespace}/{volume_name}/tags/{volume_tag}": { + "delete": { + "summary": "Deletes a volume tag", + "x-codeSamples": [ + { + "lang": "bash", + "source": "curl --request DELETE \\\n--url https://api.baseten.co/v1/volumes/{volume_namespace}/{volume_name}/tags/{volume_tag} \\\n--header \"Authorization: Bearer $BASETEN_API_KEY\" \\\n--data '{\n \"expected_sequence\": null\n}'" + }, + { + "lang": "python", + "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/volumes/{volume_namespace}/{volume_name}/tags/{volume_tag}\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"DELETE\",\n url,\n headers=headers,\n json={'expected_sequence': None}\n)\n\nprint(response.text)" + } + ], + "description": "The version remains available by digest and any other tags. If the tag is absent, the request succeeds with `deleted` set to `false`. Deleting `head` makes references without a tag or digest stop resolving until `head` is set again.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteVolumeTagRequestV1" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteVolumeTagResponseV1" + } + } + } + } + }, + "x-stability": "unstable" + }, + "parameters": [ + { + "$ref": "#/components/parameters/volume_namespace" + }, + { + "$ref": "#/components/parameters/volume_name" + }, + { + "$ref": "#/components/parameters/volume_tag" + } + ] + }, "/v1/token": { "post": { "summary": "Creates a sandbox access token", @@ -3444,6 +3578,275 @@ } ] }, + "/v1/models/{model_id}/deployments/{deployment_id}/startups": { + "get": { + "summary": "Lists the startups of a model deployment", + "x-codeSamples": [ + { + "lang": "bash", + "source": "curl --request GET \\\n--url https://api.baseten.co/v1/models/{model_id}/deployments/{deployment_id}/startups \\\n--header \"Authorization: Bearer $BASETEN_API_KEY\"\n" + }, + { + "lang": "python", + "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/models/{model_id}/deployments/{deployment_id}/startups\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"GET\",\n url,\n headers=headers,\n params={'start_epoch_millis': None, 'end_epoch_millis': None, 'min_duration_ms': None, 'cursor': None, 'phase': None, 'search': None, 'sort_by': None, 'direction': None}\n)\n\nprint(response.text)" + } + ], + "description": "Lists replica startups in the given time range, newest first by default. Pass the `cursor` from a response to fetch the next page.", + "parameters": [ + { + "name": "start_epoch_millis", + "in": "query", + "required": true, + "description": "Start of the query window, in epoch milliseconds.", + "schema": { + "title": "Start Epoch Millis", + "type": "integer" + } + }, + { + "name": "end_epoch_millis", + "in": "query", + "required": true, + "description": "End of the query window, in epoch milliseconds. Must be after the start.", + "schema": { + "title": "End Epoch Millis", + "type": "integer" + } + }, + { + "name": "min_duration_ms", + "in": "query", + "required": false, + "description": "Only include startups that lasted at least this many milliseconds.", + "schema": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Min Duration Ms" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "description": "Opaque cursor returned by a previous page. Omit to fetch the first page. Pass the same filters and sort as the request that returned it.", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cursor" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "description": "Maximum number of attempts to return.", + "schema": { + "default": 50, + "maximum": 100, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "name": "outcomes", + "in": "query", + "required": false, + "description": "Only include attempts with one of these outcomes. Repeat to pass several.", + "schema": { + "items": { + "$ref": "#/components/schemas/StartupOutcomeV1" + }, + "title": "Outcomes", + "type": "array" + } + }, + { + "name": "phase", + "in": "query", + "required": false, + "description": "Only include attempts that spent longer than 0 ms in this phase.", + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/StartupPhaseV1" + }, + { + "type": "null" + } + ], + "default": null + } + }, + { + "name": "search", + "in": "query", + "required": false, + "description": "Only include attempts whose replica ID matches exactly, or whose deployment name contains this text (case-insensitive).", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Search" + } + }, + { + "name": "sort_by", + "in": "query", + "required": false, + "description": "Field to sort attempts by. Defaults to `STARTED_AT`.", + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/StartupSortFieldV1" + }, + { + "type": "null" + } + ], + "default": null + } + }, + { + "name": "direction", + "in": "query", + "required": false, + "description": "Sort direction. Defaults to `desc`.", + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SortOrderV1" + }, + { + "type": "null" + } + ], + "default": null + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StartupAttemptsV1" + } + } + } + } + }, + "x-stability": "unstable" + }, + "parameters": [ + { + "$ref": "#/components/parameters/model_id" + }, + { + "$ref": "#/components/parameters/deployment_id" + } + ] + }, + "/v1/models/{model_id}/deployments/{deployment_id}/startups/summary": { + "get": { + "summary": "Gets the startup summary for a model deployment", + "x-codeSamples": [ + { + "lang": "bash", + "source": "curl --request GET \\\n--url https://api.baseten.co/v1/models/{model_id}/deployments/{deployment_id}/startups/summary \\\n--header \"Authorization: Bearer $BASETEN_API_KEY\"\n" + }, + { + "lang": "python", + "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/models/{model_id}/deployments/{deployment_id}/startups/summary\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"GET\",\n url,\n headers=headers,\n params={'start_epoch_millis': None, 'end_epoch_millis': None, 'min_duration_ms': None}\n)\n\nprint(response.text)" + } + ], + "description": "Gets the distribution of replica startup durations, in total and per phase, in the given time range.", + "parameters": [ + { + "name": "start_epoch_millis", + "in": "query", + "required": true, + "description": "Start of the query window, in epoch milliseconds.", + "schema": { + "title": "Start Epoch Millis", + "type": "integer" + } + }, + { + "name": "end_epoch_millis", + "in": "query", + "required": true, + "description": "End of the query window, in epoch milliseconds. Must be after the start.", + "schema": { + "title": "End Epoch Millis", + "type": "integer" + } + }, + { + "name": "min_duration_ms", + "in": "query", + "required": false, + "description": "Only include startups that lasted at least this many milliseconds.", + "schema": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Min Duration Ms" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StartupSummaryV1" + } + } + } + } + }, + "x-stability": "unstable" + }, + "parameters": [ + { + "$ref": "#/components/parameters/model_id" + }, + { + "$ref": "#/components/parameters/deployment_id" + } + ] + }, "/v1/models/{model_id}/deployments/{deployment_id}/replicas/{replica_id}": { "delete": { "summary": "Terminates a replica in a deployment", @@ -4026,6 +4429,275 @@ } ] }, + "/v1/models/{model_id}/environments/{env_name}/startups": { + "get": { + "summary": "Lists the startups of a model environment", + "x-codeSamples": [ + { + "lang": "bash", + "source": "curl --request GET \\\n--url https://api.baseten.co/v1/models/{model_id}/environments/{env_name}/startups \\\n--header \"Authorization: Bearer $BASETEN_API_KEY\"\n" + }, + { + "lang": "python", + "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/models/{model_id}/environments/{env_name}/startups\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"GET\",\n url,\n headers=headers,\n params={'start_epoch_millis': None, 'end_epoch_millis': None, 'min_duration_ms': None, 'cursor': None, 'phase': None, 'search': None, 'sort_by': None, 'direction': None}\n)\n\nprint(response.text)" + } + ], + "description": "Lists replica startups of every deployment that was active on the environment in the given time range, newest first by default. Pass the `cursor` from a response to fetch the next page.", + "parameters": [ + { + "name": "start_epoch_millis", + "in": "query", + "required": true, + "description": "Start of the query window, in epoch milliseconds.", + "schema": { + "title": "Start Epoch Millis", + "type": "integer" + } + }, + { + "name": "end_epoch_millis", + "in": "query", + "required": true, + "description": "End of the query window, in epoch milliseconds. Must be after the start.", + "schema": { + "title": "End Epoch Millis", + "type": "integer" + } + }, + { + "name": "min_duration_ms", + "in": "query", + "required": false, + "description": "Only include startups that lasted at least this many milliseconds.", + "schema": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Min Duration Ms" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "description": "Opaque cursor returned by a previous page. Omit to fetch the first page. Pass the same filters and sort as the request that returned it.", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cursor" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "description": "Maximum number of attempts to return.", + "schema": { + "default": 50, + "maximum": 100, + "minimum": 1, + "title": "Limit", + "type": "integer" + } + }, + { + "name": "outcomes", + "in": "query", + "required": false, + "description": "Only include attempts with one of these outcomes. Repeat to pass several.", + "schema": { + "items": { + "$ref": "#/components/schemas/StartupOutcomeV1" + }, + "title": "Outcomes", + "type": "array" + } + }, + { + "name": "phase", + "in": "query", + "required": false, + "description": "Only include attempts that spent longer than 0 ms in this phase.", + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/StartupPhaseV1" + }, + { + "type": "null" + } + ], + "default": null + } + }, + { + "name": "search", + "in": "query", + "required": false, + "description": "Only include attempts whose replica ID matches exactly, or whose deployment name contains this text (case-insensitive).", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Search" + } + }, + { + "name": "sort_by", + "in": "query", + "required": false, + "description": "Field to sort attempts by. Defaults to `STARTED_AT`.", + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/StartupSortFieldV1" + }, + { + "type": "null" + } + ], + "default": null + } + }, + { + "name": "direction", + "in": "query", + "required": false, + "description": "Sort direction. Defaults to `desc`.", + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SortOrderV1" + }, + { + "type": "null" + } + ], + "default": null + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StartupAttemptsV1" + } + } + } + } + }, + "x-stability": "unstable" + }, + "parameters": [ + { + "$ref": "#/components/parameters/model_id" + }, + { + "$ref": "#/components/parameters/env_name" + } + ] + }, + "/v1/models/{model_id}/environments/{env_name}/startups/summary": { + "get": { + "summary": "Gets the startup summary for a model environment", + "x-codeSamples": [ + { + "lang": "bash", + "source": "curl --request GET \\\n--url https://api.baseten.co/v1/models/{model_id}/environments/{env_name}/startups/summary \\\n--header \"Authorization: Bearer $BASETEN_API_KEY\"\n" + }, + { + "lang": "python", + "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/models/{model_id}/environments/{env_name}/startups/summary\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"GET\",\n url,\n headers=headers,\n params={'start_epoch_millis': None, 'end_epoch_millis': None, 'min_duration_ms': None}\n)\n\nprint(response.text)" + } + ], + "description": "Gets the distribution of replica startup durations, in total and per phase, across every deployment that was active on the environment in the given time range.", + "parameters": [ + { + "name": "start_epoch_millis", + "in": "query", + "required": true, + "description": "Start of the query window, in epoch milliseconds.", + "schema": { + "title": "Start Epoch Millis", + "type": "integer" + } + }, + { + "name": "end_epoch_millis", + "in": "query", + "required": true, + "description": "End of the query window, in epoch milliseconds. Must be after the start.", + "schema": { + "title": "End Epoch Millis", + "type": "integer" + } + }, + { + "name": "min_duration_ms", + "in": "query", + "required": false, + "description": "Only include startups that lasted at least this many milliseconds.", + "schema": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Min Duration Ms" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StartupSummaryV1" + } + } + } + } + }, + "x-stability": "unstable" + }, + "parameters": [ + { + "$ref": "#/components/parameters/model_id" + }, + { + "$ref": "#/components/parameters/env_name" + } + ] + }, "/v1/models/{model_id}/environments/{env_name}/activate": { "post": { "summary": "Activates a deployment associated with an environment", @@ -6937,11 +7609,11 @@ "x-codeSamples": [ { "lang": "bash", - "source": "curl --request POST \\\n--url https://api.baseten.co/v1/loops/checkpoints/validate \\\n--header \"Authorization: Bearer $BASETEN_API_KEY\" \\\n--data '{\n \"checkpoint_path\": \"bt://loops:k4q95w5/sampler_weights/step-100\"\n}'" + "source": "curl --request POST \\\n--url https://api.baseten.co/v1/loops/checkpoints/validate \\\n--header \"Authorization: Bearer $BASETEN_API_KEY\" \\\n--data '{\n \"checkpoint_path\": \"bt://loops:k4q95w5/sampler_weights/step-100\",\n \"run_id\": \"k4q95w5\"\n}'" }, { "lang": "python", - "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/loops/checkpoints/validate\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"POST\",\n url,\n headers=headers,\n json={'checkpoint_path': 'bt://loops:k4q95w5/sampler_weights/step-100'}\n)\n\nprint(response.text)" + "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/loops/checkpoints/validate\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"POST\",\n url,\n headers=headers,\n json={'checkpoint_path': 'bt://loops:k4q95w5/sampler_weights/step-100', 'run_id': 'k4q95w5'}\n)\n\nprint(response.text)" } ], "description": "Returns whether the caller can manage and use this checkpoint.", @@ -8159,7 +8831,7 @@ "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/model_apis/usage\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"GET\",\n url,\n headers=headers,\n params={'start_time': None, 'end_time': None, 'bucket_width': '1m', 'limit': None, 'cursor': None}\n)\n\nprint(response.text)" } ], - "description": "Returns your organization's Model APIs token usage as a series of contiguous time buckets, broken down by the dimensions you pass in group_by. Buckets with no usage are included, so the series has no gaps. Usage is retained for 92 days, so buckets older than that are returned with no results.", + "description": "Returns Model APIs token usage for the teams you can manage as a series of contiguous time buckets, broken down by the dimensions you pass in group_by. Organization admins receive organization-wide totals by default; other callers may receive lower totals than callers with management-access to more teams. Buckets with no usage are included, so the series has no gaps. Usage is retained for 92 days, so buckets older than that are returned with no results.", "parameters": [ { "name": "start_time", @@ -8213,7 +8885,7 @@ "name": "group_by", "in": "query", "required": false, - "description": "Dimensions to break usage down by, repeated once per dimension: api_key, user, model. Defaults to model.", + "description": "Dimensions to break usage down by, repeated once per dimension: api_key, user, model, or team. Defaults to model.", "schema": { "items": { "$ref": "#/components/schemas/UsageDimensionV1" @@ -8261,6 +8933,19 @@ "type": "array" } }, + { + "name": "team_ids", + "in": "query", + "required": false, + "description": "Return only usage for these teams, repeated once per team. Defaults to all teams you can manage.", + "schema": { + "items": { + "type": "string" + }, + "title": "Team Ids", + "type": "array" + } + }, { "name": "limit", "in": "query", @@ -8351,11 +9036,11 @@ "x-codeSamples": [ { "lang": "bash", - "source": "curl --request POST \\\n--url https://api.baseten.co/v1/llm_models \\\n--header \"Authorization: Bearer $BASETEN_API_KEY\" \\\n--data '{\n \"resources\": null,\n \"region\": null,\n \"llm_version\": null,\n \"model_metadata\": null,\n \"autoscaling_settings\": {\n \"autoscaling_window\": 600,\n \"concurrency_target\": null,\n \"max_replica\": 5,\n \"max_scale_down_rate\": null,\n \"min_replica\": 1,\n \"scale_down_delay\": 300,\n \"target_in_flight_tokens\": null,\n \"target_utilization_percentage\": null\n },\n \"additional_autoscaling_config\": {\n \"metrics\": [\n {\n \"name\": \"in_flight_tokens\",\n \"target\": 40000\n }\n ]\n },\n \"metadata\": {\n \"environment\": \"production\",\n \"git_sha\": \"abc123\"\n },\n \"weights\": [\n {\n \"mount_location\": \"/models/base\",\n \"source\": \"hf://meta-llama/Llama-3-8B\"\n }\n ],\n \"name\": null\n}'" + "source": "curl --request POST \\\n--url https://api.baseten.co/v1/llm_models \\\n--header \"Authorization: Bearer $BASETEN_API_KEY\" \\\n--data '{\n \"resources\": null,\n \"region\": null,\n \"llm_version\": null,\n \"model_metadata\": null,\n \"autoscaling_settings\": {\n \"autoscaling_window\": 600,\n \"concurrency_target\": null,\n \"max_replica\": 5,\n \"max_scale_down_rate\": null,\n \"min_replica\": 1,\n \"scale_down_delay\": 300,\n \"target_in_flight_tokens\": null,\n \"target_utilization_percentage\": null\n },\n \"additional_autoscaling_config\": {\n \"metrics\": [\n {\n \"name\": \"in_flight_tokens\",\n \"target\": 40000\n }\n ]\n },\n \"metadata\": {\n \"environment\": \"production\",\n \"git_sha\": \"abc123\"\n },\n \"weights\": [\n {\n \"mount_location\": \"/models/base\",\n \"source\": \"hf://meta-llama/Llama-3-8B\"\n }\n ],\n \"egress_restrictions\": null,\n \"name\": null\n}'" }, { "lang": "python", - "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/llm_models\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"POST\",\n url,\n headers=headers,\n json={'resources': None, 'region': None, 'llm_version': None, 'model_metadata': None, 'autoscaling_settings': {'autoscaling_window': 600, 'concurrency_target': None, 'max_replica': 5, 'max_scale_down_rate': None, 'min_replica': 1, 'scale_down_delay': 300, 'target_in_flight_tokens': None, 'target_utilization_percentage': None}, 'additional_autoscaling_config': {'metrics': [{'name': 'in_flight_tokens', 'target': 40000}]}, 'metadata': {'environment': 'production', 'git_sha': 'abc123'}, 'weights': [{'mount_location': '/models/base', 'source': 'hf://meta-llama/Llama-3-8B'}], 'name': None}\n)\n\nprint(response.text)" + "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/llm_models\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"POST\",\n url,\n headers=headers,\n json={'resources': None, 'region': None, 'llm_version': None, 'model_metadata': None, 'autoscaling_settings': {'autoscaling_window': 600, 'concurrency_target': None, 'max_replica': 5, 'max_scale_down_rate': None, 'min_replica': 1, 'scale_down_delay': 300, 'target_in_flight_tokens': None, 'target_utilization_percentage': None}, 'additional_autoscaling_config': {'metrics': [{'name': 'in_flight_tokens', 'target': 40000}]}, 'metadata': {'environment': 'production', 'git_sha': 'abc123'}, 'weights': [{'mount_location': '/models/base', 'source': 'hf://meta-llama/Llama-3-8B'}], 'egress_restrictions': None, 'name': None}\n)\n\nprint(response.text)" } ], "requestBody": { @@ -8388,11 +9073,11 @@ "x-codeSamples": [ { "lang": "bash", - "source": "curl --request POST \\\n--url https://api.baseten.co/v1/teams/{team_id}/llm_models \\\n--header \"Authorization: Bearer $BASETEN_API_KEY\" \\\n--data '{\n \"resources\": null,\n \"region\": null,\n \"llm_version\": null,\n \"model_metadata\": null,\n \"autoscaling_settings\": {\n \"autoscaling_window\": 600,\n \"concurrency_target\": null,\n \"max_replica\": 5,\n \"max_scale_down_rate\": null,\n \"min_replica\": 1,\n \"scale_down_delay\": 300,\n \"target_in_flight_tokens\": null,\n \"target_utilization_percentage\": null\n },\n \"additional_autoscaling_config\": {\n \"metrics\": [\n {\n \"name\": \"in_flight_tokens\",\n \"target\": 40000\n }\n ]\n },\n \"metadata\": {\n \"environment\": \"production\",\n \"git_sha\": \"abc123\"\n },\n \"weights\": [\n {\n \"mount_location\": \"/models/base\",\n \"source\": \"hf://meta-llama/Llama-3-8B\"\n }\n ],\n \"name\": null\n}'" + "source": "curl --request POST \\\n--url https://api.baseten.co/v1/teams/{team_id}/llm_models \\\n--header \"Authorization: Bearer $BASETEN_API_KEY\" \\\n--data '{\n \"resources\": null,\n \"region\": null,\n \"llm_version\": null,\n \"model_metadata\": null,\n \"autoscaling_settings\": {\n \"autoscaling_window\": 600,\n \"concurrency_target\": null,\n \"max_replica\": 5,\n \"max_scale_down_rate\": null,\n \"min_replica\": 1,\n \"scale_down_delay\": 300,\n \"target_in_flight_tokens\": null,\n \"target_utilization_percentage\": null\n },\n \"additional_autoscaling_config\": {\n \"metrics\": [\n {\n \"name\": \"in_flight_tokens\",\n \"target\": 40000\n }\n ]\n },\n \"metadata\": {\n \"environment\": \"production\",\n \"git_sha\": \"abc123\"\n },\n \"weights\": [\n {\n \"mount_location\": \"/models/base\",\n \"source\": \"hf://meta-llama/Llama-3-8B\"\n }\n ],\n \"egress_restrictions\": null,\n \"name\": null\n}'" }, { "lang": "python", - "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/teams/{team_id}/llm_models\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"POST\",\n url,\n headers=headers,\n json={'resources': None, 'region': None, 'llm_version': None, 'model_metadata': None, 'autoscaling_settings': {'autoscaling_window': 600, 'concurrency_target': None, 'max_replica': 5, 'max_scale_down_rate': None, 'min_replica': 1, 'scale_down_delay': 300, 'target_in_flight_tokens': None, 'target_utilization_percentage': None}, 'additional_autoscaling_config': {'metrics': [{'name': 'in_flight_tokens', 'target': 40000}]}, 'metadata': {'environment': 'production', 'git_sha': 'abc123'}, 'weights': [{'mount_location': '/models/base', 'source': 'hf://meta-llama/Llama-3-8B'}], 'name': None}\n)\n\nprint(response.text)" + "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/teams/{team_id}/llm_models\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"POST\",\n url,\n headers=headers,\n json={'resources': None, 'region': None, 'llm_version': None, 'model_metadata': None, 'autoscaling_settings': {'autoscaling_window': 600, 'concurrency_target': None, 'max_replica': 5, 'max_scale_down_rate': None, 'min_replica': 1, 'scale_down_delay': 300, 'target_in_flight_tokens': None, 'target_utilization_percentage': None}, 'additional_autoscaling_config': {'metrics': [{'name': 'in_flight_tokens', 'target': 40000}]}, 'metadata': {'environment': 'production', 'git_sha': 'abc123'}, 'weights': [{'mount_location': '/models/base', 'source': 'hf://meta-llama/Llama-3-8B'}], 'egress_restrictions': None, 'name': None}\n)\n\nprint(response.text)" } ], "requestBody": { @@ -8430,11 +9115,11 @@ "x-codeSamples": [ { "lang": "bash", - "source": "curl --request POST \\\n--url https://api.baseten.co/v1/llm_models/{model_id}/deployments \\\n--header \"Authorization: Bearer $BASETEN_API_KEY\" \\\n--data '{\n \"resources\": null,\n \"region\": null,\n \"llm_version\": null,\n \"model_metadata\": null,\n \"autoscaling_settings\": {\n \"autoscaling_window\": 600,\n \"concurrency_target\": null,\n \"max_replica\": 5,\n \"max_scale_down_rate\": null,\n \"min_replica\": 1,\n \"scale_down_delay\": 300,\n \"target_in_flight_tokens\": null,\n \"target_utilization_percentage\": null\n },\n \"additional_autoscaling_config\": {\n \"metrics\": [\n {\n \"name\": \"in_flight_tokens\",\n \"target\": 40000\n }\n ]\n },\n \"metadata\": {\n \"environment\": \"production\",\n \"git_sha\": \"abc123\"\n },\n \"weights\": [\n {\n \"mount_location\": \"/models/base\",\n \"source\": \"hf://meta-llama/Llama-3-8B\"\n }\n ]\n}'" + "source": "curl --request POST \\\n--url https://api.baseten.co/v1/llm_models/{model_id}/deployments \\\n--header \"Authorization: Bearer $BASETEN_API_KEY\" \\\n--data '{\n \"resources\": null,\n \"region\": null,\n \"llm_version\": null,\n \"model_metadata\": null,\n \"autoscaling_settings\": {\n \"autoscaling_window\": 600,\n \"concurrency_target\": null,\n \"max_replica\": 5,\n \"max_scale_down_rate\": null,\n \"min_replica\": 1,\n \"scale_down_delay\": 300,\n \"target_in_flight_tokens\": null,\n \"target_utilization_percentage\": null\n },\n \"additional_autoscaling_config\": {\n \"metrics\": [\n {\n \"name\": \"in_flight_tokens\",\n \"target\": 40000\n }\n ]\n },\n \"metadata\": {\n \"environment\": \"production\",\n \"git_sha\": \"abc123\"\n },\n \"weights\": [\n {\n \"mount_location\": \"/models/base\",\n \"source\": \"hf://meta-llama/Llama-3-8B\"\n }\n ],\n \"egress_restrictions\": null\n}'" }, { "lang": "python", - "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/llm_models/{model_id}/deployments\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"POST\",\n url,\n headers=headers,\n json={'resources': None, 'region': None, 'llm_version': None, 'model_metadata': None, 'autoscaling_settings': {'autoscaling_window': 600, 'concurrency_target': None, 'max_replica': 5, 'max_scale_down_rate': None, 'min_replica': 1, 'scale_down_delay': 300, 'target_in_flight_tokens': None, 'target_utilization_percentage': None}, 'additional_autoscaling_config': {'metrics': [{'name': 'in_flight_tokens', 'target': 40000}]}, 'metadata': {'environment': 'production', 'git_sha': 'abc123'}, 'weights': [{'mount_location': '/models/base', 'source': 'hf://meta-llama/Llama-3-8B'}]}\n)\n\nprint(response.text)" + "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/llm_models/{model_id}/deployments\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"POST\",\n url,\n headers=headers,\n json={'resources': None, 'region': None, 'llm_version': None, 'model_metadata': None, 'autoscaling_settings': {'autoscaling_window': 600, 'concurrency_target': None, 'max_replica': 5, 'max_scale_down_rate': None, 'min_replica': 1, 'scale_down_delay': 300, 'target_in_flight_tokens': None, 'target_utilization_percentage': None}, 'additional_autoscaling_config': {'metrics': [{'name': 'in_flight_tokens', 'target': 40000}]}, 'metadata': {'environment': 'production', 'git_sha': 'abc123'}, 'weights': [{'mount_location': '/models/base', 'source': 'hf://meta-llama/Llama-3-8B'}], 'egress_restrictions': None}\n)\n\nprint(response.text)" } ], "requestBody": { @@ -8593,14 +9278,14 @@ "x-codeSamples": [ { "lang": "bash", - "source": "curl --request PATCH \\\n--url https://api.baseten.co/v1/library_listings/{user_defined_listing_id} \\\n--header \"Authorization: Bearer $BASETEN_API_KEY\" \\\n--data '{\n \"display_name\": null,\n \"is_public\": null,\n \"trending\": null,\n \"metadata\": null\n}'" + "source": "curl --request PATCH \\\n--url https://api.baseten.co/v1/library_listings/{user_defined_listing_id} \\\n--header \"Authorization: Bearer $BASETEN_API_KEY\" \\\n--data '{\n \"display_name\": null,\n \"is_public\": null,\n \"trending\": null,\n \"is_listed\": null,\n \"metadata\": null\n}'" }, { "lang": "python", - "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/library_listings/{user_defined_listing_id}\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"PATCH\",\n url,\n headers=headers,\n json={'display_name': None, 'is_public': None, 'trending': None, 'metadata': None}\n)\n\nprint(response.text)" + "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/library_listings/{user_defined_listing_id}\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"PATCH\",\n url,\n headers=headers,\n json={'display_name': None, 'is_public': None, 'trending': None, 'is_listed': None, 'metadata': None}\n)\n\nprint(response.text)" } ], - "description": "Updates a library listing. Supported fields are the display name, public visibility, trending status, and the model-level metadata. When metadata is provided, it replaces the stored metadata.", + "description": "Updates a library listing. Supported fields are the display name, public visibility, listed status, trending status, and the model-level metadata. An unlisted listing is left out of catalogs but still deploys from its direct link. When metadata is provided, it replaces the stored metadata.", "requestBody": { "content": { "application/json": { @@ -9578,7 +10263,7 @@ "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/routes\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"POST\",\n url,\n headers=headers,\n json={'team_id': 'abc1234', 'display_name': 'Assistant', 'target': {'model': 'zai-org/GLM-5.3', 'type': 'BASETEN_MODEL_API'}, 'description': 'Assistant for code review and debugging.'}\n)\n\nprint(response.text)" } ], - "description": "Creates and derives a name for the provided route configuration", + "description": "Creates and derives a name for the provided route configuration. CONNECTION targets reference a connection owned by the route's team; create the connection first.", "requestBody": { "content": { "application/json": { @@ -9614,10 +10299,10 @@ }, { "lang": "python", - "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/routes/usage\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"GET\",\n url,\n headers=headers,\n params={'cursor': None, 'start_date': None, 'end_date': None}\n)\n\nprint(response.text)" + "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/routes/usage\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"GET\",\n url,\n headers=headers,\n params={'cursor': None, 'team_id': None, 'start_date': None, 'end_date': None}\n)\n\nprint(response.text)" } ], - "description": "Buckets are UTC days, and days with no usage are included. Organization admins see all route usage in the organization. Other members, including viewers, see only usage from Routes keys they created, including keys that have since expired, been revoked, or been deleted. Usage is metered every 15 minutes, so the current hour can lag. Model API costs use your prices at the time of each request and include tool calls. xAI costs are the charges xAI reports. OpenAI and Anthropic costs are estimated from Baseten's reference prices and may differ from your provider's bill. Vertex and OpenAI-compatible usage is not included. Costs for OpenAI, Anthropic, and xAI estimate what you pay those providers; they are not Baseten charges. Usage is retained for 92 days.", + "description": "Buckets are UTC days, and days with no usage are included. Organization admins see all route usage in the organization. Team admins see usage in teams they manage. Other members, including viewers, see only usage from Routes keys they created, including keys that have since expired, been revoked, or been deleted. Usage can be filtered and grouped by the team it was metered in. Usage is metered every 15 minutes, so the current hour can lag. Model API costs use your prices at the time of each request and include tool calls. xAI costs are the charges xAI reports. OpenAI and Anthropic costs are estimated from Baseten's reference prices and may differ from your provider's bill. Vertex and OpenAI-compatible usage is not included. Costs for OpenAI, Anthropic, and xAI estimate what you pay those providers; they are not Baseten charges. Usage is retained for 92 days.", "parameters": [ { "name": "cursor", @@ -9650,6 +10335,24 @@ "type": "integer" } }, + { + "name": "team_id", + "in": "query", + "required": false, + "description": "Limit usage to a team you can access. Organization admins can view all usage in the team, team admins can view usage in teams they manage, and other members can view only their own usage. Without a team, organization admins see all organization usage, team admins see their managed teams, and other members see their own usage.", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Team Id" + } + }, { "name": "start_date", "in": "query", @@ -9692,7 +10395,7 @@ "name": "group_by", "in": "query", "required": false, - "description": "Dimensions to break usage down by, repeated once per dimension: USER, MODEL, or PROVIDER. Each result represents one observed combination of the requested dimensions within that day, and results are sorted by those values. Combinations without usage are omitted, so result counts can differ between days. Defaults to MODEL.", + "description": "Dimensions to break usage down by, repeated once per dimension: USER, TEAM, MODEL, PROVIDER, or ROUTER. Each result represents one observed combination of the requested dimensions within that day, and results are sorted by those values. Combinations without usage are omitted, so result counts can differ between days. ROUTER returns only usage sent through an intelligent router, one row per router, or per router and model with MODEL. Router rows repeat usage already counted in other groupings, so don't add them to model totals. Defaults to MODEL.", "schema": { "items": { "$ref": "#/components/schemas/RouteUsageDimensionV1" @@ -9756,27 +10459,27 @@ "x-stability": "unstable" } }, - "/v1/routes/settings/users/{user_id}": { + "/v1/routes/settings/users/{user_id}/teams": { "get": { - "summary": "Gets a user's route settings", + "summary": "Lists a user's route settings in each of their teams", "x-codeSamples": [ { "lang": "bash", - "source": "curl --request GET \\\n--url https://api.baseten.co/v1/routes/settings/users/{user_id} \\\n--header \"Authorization: Bearer $BASETEN_API_KEY\"\n" + "source": "curl --request GET \\\n--url https://api.baseten.co/v1/routes/settings/users/{user_id}/teams \\\n--header \"Authorization: Bearer $BASETEN_API_KEY\"\n" }, { "lang": "python", - "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/routes/settings/users/{user_id}\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"GET\",\n url,\n headers=headers,\n json={}\n)\n\nprint(response.text)" + "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/routes/settings/users/{user_id}/teams\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"GET\",\n url,\n headers=headers,\n json={}\n)\n\nprint(response.text)" } ], - "description": "Returns the settings that apply to the user, including the spend limit that caps what they can spend on Baseten Code each month: the limit set on the user, their team's per-member limit, and the effective limit for this user. The user limit always overrides the team's per-member limit.", + "description": "Returns the user's settings in each team they belong to (every team, for organization admins), alphabetical by team name: their spend limit for Baseten Code in the team, their spend in the team this month, and whether that spend reached the limit. Members can read their own settings; organization admins can read anyone's.", "responses": { "200": { "description": "", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RouteUserSettingsV1" + "$ref": "#/components/schemas/RouteUserTeamSettingsListV1" } } } @@ -9788,44 +10491,7 @@ { "$ref": "#/components/parameters/user_id" } - ], - "patch": { - "summary": "Updates a user's route settings", - "x-codeSamples": [ - { - "lang": "bash", - "source": "curl --request PATCH \\\n--url https://api.baseten.co/v1/routes/settings/users/{user_id} \\\n--header \"Authorization: Bearer $BASETEN_API_KEY\" \\\n--data '{\n \"spend_limit\": null\n}'" - }, - { - "lang": "python", - "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/routes/settings/users/{user_id}\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"PATCH\",\n url,\n headers=headers,\n json={'spend_limit': None}\n)\n\nprint(response.text)" - } - ], - "description": "Changes only the fields in the request. Once the user's metered spend in a month reaches the spend limit, requests with Routes keys they created are rejected until the limit is raised or the next month starts. Spend is metered every 15 minutes and can lag, so a user can go over the limit. The user's own limit supersedes their team's per-member limit.", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateRouteUserSettingsRequestV1" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RouteUserSettingsV1" - } - } - } - } - }, - "x-stability": "unstable" - } + ] }, "/v1/routes/settings/teams/{team_id}": { "get": { @@ -9872,7 +10538,7 @@ "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/routes/settings/teams/{team_id}\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"PATCH\",\n url,\n headers=headers,\n json={'harness_defaults': None, 'spend_limit': None}\n)\n\nprint(response.text)" } ], - "description": "Changes only the fields in the request, all or nothing. Requires team admin.", + "description": "Changes only the fields in the request, all or nothing. Requires team admin, and organization admin to change the team-wide spend limit.", "requestBody": { "content": { "application/json": { @@ -9898,6 +10564,114 @@ "x-stability": "unstable" } }, + "/v1/routes/settings/teams/{team_id}/users": { + "get": { + "summary": "Lists users' route settings in a team", + "x-codeSamples": [ + { + "lang": "bash", + "source": "curl --request GET \\\n--url https://api.baseten.co/v1/routes/settings/teams/{team_id}/users \\\n--header \"Authorization: Bearer $BASETEN_API_KEY\"\n" + }, + { + "lang": "python", + "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/routes/settings/teams/{team_id}/users\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"GET\",\n url,\n headers=headers,\n json={}\n)\n\nprint(response.text)" + } + ], + "description": "Returns the settings in the team of each member of the team, including organization admins and Code service accounts. Requires team admin for the specified team or organization admin.", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouteTeamUserSettingsListV1" + } + } + } + } + }, + "x-stability": "unstable" + }, + "parameters": [ + { + "$ref": "#/components/parameters/team_id" + } + ] + }, + "/v1/routes/settings/teams/{team_id}/users/{user_id}": { + "get": { + "summary": "Gets a user's route settings in a team", + "x-codeSamples": [ + { + "lang": "bash", + "source": "curl --request GET \\\n--url https://api.baseten.co/v1/routes/settings/teams/{team_id}/users/{user_id} \\\n--header \"Authorization: Bearer $BASETEN_API_KEY\"\n" + }, + { + "lang": "python", + "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/routes/settings/teams/{team_id}/users/{user_id}\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"GET\",\n url,\n headers=headers,\n json={}\n)\n\nprint(response.text)" + } + ], + "description": "Returns the user's spend limit for Baseten Code in the team, their spend in the team this month, and whether that spend reached the limit. Members can read their own settings; team admins can read members' settings in teams they administer, and organization admins can read anyone's.", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouteTeamUserSettingsV1" + } + } + } + } + }, + "x-stability": "unstable" + }, + "parameters": [ + { + "$ref": "#/components/parameters/team_id" + }, + { + "$ref": "#/components/parameters/user_id" + } + ], + "patch": { + "summary": "Updates a user's route settings in a team", + "x-codeSamples": [ + { + "lang": "bash", + "source": "curl --request PATCH \\\n--url https://api.baseten.co/v1/routes/settings/teams/{team_id}/users/{user_id} \\\n--header \"Authorization: Bearer $BASETEN_API_KEY\" \\\n--data '{\n \"spend_limit\": null\n}'" + }, + { + "lang": "python", + "source": "import requests\nimport os\nAPI_KEY = os.environ.get(\"BASETEN_API_KEY\", \"\")\nurl = \"https://api.baseten.co/v1/routes/settings/teams/{team_id}/users/{user_id}\"\n\nheaders = {\"Authorization\": f\"Bearer {API_KEY}\"}\n\nresponse = requests.request(\n \"PATCH\",\n url,\n headers=headers,\n json={'spend_limit': None}\n)\n\nprint(response.text)" + } + ], + "description": "Changes only the fields in the request. Once the user's metered spend in the team in a month reaches the spend limit, requests with Routes keys they created in the team are rejected until the limit is raised or the next month starts. Spend is metered every 15 minutes and can lag, so a user can go over the limit. Requires team admin for the specified team or organization admin.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateRouteUserSettingsRequestV1" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouteTeamUserSettingsV1" + } + } + } + } + }, + "x-stability": "unstable" + } + }, "/v1/routes/connections": { "get": { "summary": "Lists connections", @@ -10615,64 +11389,18 @@ } ] }, - "/v1/sandboxes/configuration": { - "get": { - "operationId": "GetSandboxConfiguration", - "summary": "Get sandbox configuration", - "description": "Return available Carbon-compatible sandbox regions. Configuration is the same for every team, so no team is selected. Only enabled regions supporting Carbon are returned. This describes regional capability, not the runtime selected for a particular team or sandbox. Region configuration is cached for up to 30 seconds.", - "tags": [ - "Sandboxes" - ], - "responses": { - "200": { - "description": "Successful operation.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GetSandboxConfigurationResponseV1" - }, - "example": { - "regions": [ - { - "name": "us-was-1", - "country": "us", - "continent": "na", - "location": "Washington", - "info_generation": "CARBON" - } - ] - } - } - } - }, - "400": { - "$ref": "#/components/responses/SandboxError400" - }, - "401": { - "$ref": "#/components/responses/SandboxError401" - }, - "403": { - "$ref": "#/components/responses/SandboxError403" - }, - "429": { - "$ref": "#/components/responses/SandboxError429" - }, - "500": { - "$ref": "#/components/responses/SandboxError500" - } - }, - "x-stability": "unstable" - } - }, "/v1/sandboxes/instances/{sandbox_name}/metrics": { "get": { "operationId": "GetSandboxMetrics", "summary": "Get sandbox metrics", - "description": "Returns every interval in the requested half-open range [start_time, end_time).\nBuckets align to UTC interval boundaries; partial boundary buckets include only samples\nwithin the requested range. The first bucket may start before start_time. Ranges are limited to 31 days\nand 10000 buckets. Request counts are zero when there is no traffic, and null\nfor buckets entirely before sandbox creation. CPU and memory are null without\nsamples, including standby. Error rate is the fraction of requests returning\n4xx or 5xx, null when there are no requests. Telemetry failures return an error.\n", + "description": "Returns every interval with labeled request counts, CPU cores, and RSS memory bytes. CURRENT returns one snapshot at now, with\nrequest counts over the preceding minute and latest gauges.\nSUMMARY returns one value set over the window. SERIES snaps the window outward\nto UTC step boundaries and echoes those boundaries; each value covers\n[start_epoch_millis, start_epoch_millis + step_seconds).\nMissing gauge samples remain null. Request counts are zero after\ncreation without traffic, and null before creation. CPU averages are averages\nof valid sampled CPU rates; memory averages are averages of sampled RSS.\nWindows default to one hour, are limited to 7 days after alignment, and at most\n10000 intervals. Telemetry failures return an error.\n", "tags": [ "Sandboxes" ], "parameters": [ + { + "$ref": "#/components/parameters/SandboxName" + }, { "$ref": "#/components/parameters/TeamId" }, @@ -10680,46 +11408,21 @@ "$ref": "#/components/parameters/TeamIdHeader" }, { - "$ref": "#/components/parameters/SandboxName" + "$ref": "#/components/parameters/SandboxMetricMode" }, { - "name": "start_time", - "in": "query", - "required": true, - "schema": { - "type": "string", - "format": "date-time" - } + "$ref": "#/components/parameters/SandboxMetricStart" }, { - "name": "end_time", - "in": "query", - "required": true, - "schema": { - "type": "string", - "format": "date-time" - } + "$ref": "#/components/parameters/SandboxMetricEnd" }, { - "name": "interval_seconds", - "in": "query", - "required": true, - "schema": { - "type": "integer", - "enum": [ - 10, - 30, - 60, - 300, - 900, - 3600 - ] - } + "$ref": "#/components/parameters/SandboxMetricNames" } ], "responses": { "200": { - "description": "Complete sandbox metrics time series.", + "description": "Index-aligned sandbox metrics.", "content": { "application/json": { "schema": { @@ -10729,7 +11432,7 @@ } }, "400": { - "description": "Invalid range, interval, or query parameter." + "description": "Invalid range, mode, step, metric selection, or response size." }, "401": { "description": "Authentication required." @@ -10842,6 +11545,55 @@ "x-stability": "unstable" } }, + "/v1/sandboxes/configuration": { + "get": { + "operationId": "GetSandboxConfiguration", + "summary": "Get sandbox configuration", + "description": "Return available Carbon-compatible sandbox regions. Configuration is the same for every team, so no team is selected. Only enabled regions supporting Carbon are returned. This describes regional capability, not the runtime selected for a particular team or sandbox. Region configuration is cached for up to 30 seconds.", + "tags": [ + "Sandboxes" + ], + "responses": { + "200": { + "description": "Successful operation.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSandboxConfigurationResponseV1" + }, + "example": { + "regions": [ + { + "name": "us-was-1", + "country": "us", + "continent": "na", + "location": "Washington", + "info_generation": "CARBON" + } + ] + } + } + } + }, + "400": { + "$ref": "#/components/responses/SandboxError400" + }, + "401": { + "$ref": "#/components/responses/SandboxError401" + }, + "403": { + "$ref": "#/components/responses/SandboxError403" + }, + "429": { + "$ref": "#/components/responses/SandboxError429" + }, + "500": { + "$ref": "#/components/responses/SandboxError500" + } + }, + "x-stability": "unstable" + } + }, "/v1/sandboxes/instances": { "get": { "operationId": "ListSandboxes", @@ -11773,7 +12525,7 @@ ], "operationId": "DeleteImageTag", "summary": "Delete an image tag", - "description": "Delete a specific image version. Reject deletion if the version is referenced by an active deployment. Return the updated image with its remaining tags, possibly empty.", + "description": "Delete a specific image version. Reject deletion if the version is referenced by an active deployment. Return the updated repository summary. Use the tag listing endpoint for remaining image versions.", "tags": [ "Images" ], @@ -13031,10 +13783,12 @@ "type": "object" }, "DeleteVolumeRequestV1": { + "additionalProperties": false, "properties": { "expected_sequence": { "anyOf": [ { + "minimum": 0, "type": "integer" }, { @@ -13081,6 +13835,85 @@ "title": "DeleteVolumeResponseV1", "type": "object" }, + "PatchVolumeRequestV1": { + "additionalProperties": false, + "properties": { + "expires_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Time at which the volume expires, in ISO 8601 format, with a UTC offset. It must be in the future and at most ten years ahead. Set it to null to clear the expiration time. A body that sets no field is rejected. When the volume expires, all live versions are deleted, all tags are removed, and the volume disappears from listings. Clearing the expiration time afterward does not restore deleted versions. Each deleted version can be restored until its delete_after time. Setting or clearing the expiration time requires the same permission as deleting the volume.", + "examples": [ + "2030-01-01T00:00:00Z" + ], + "title": "Expires At", + "x-null-distinct": true + }, + "expected_sequence": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Revision the volume is expected to be at. When set, the update fails with a conflict if the volume has changed since. Take the value from a volume's sequence, or from volume_sequence.", + "title": "Expected Sequence" + } + }, + "title": "PatchVolumeRequestV1", + "type": "object" + }, + "PatchVolumeResponseV1": { + "properties": { + "namespace": { + "description": "Namespace the volume belongs to, in lowercase.", + "title": "Namespace", + "type": "string" + }, + "volume": { + "description": "Name of the volume, in lowercase.", + "title": "Volume", + "type": "string" + }, + "expires_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Volume expiration time in ISO 8601 format. Null when no expiration time is set.", + "title": "Expires At" + }, + "volume_sequence": { + "description": "Revision of the volume after the update.", + "title": "Volume Sequence", + "type": "integer" + } + }, + "required": [ + "namespace", + "volume", + "expires_at", + "volume_sequence" + ], + "title": "PatchVolumeResponseV1", + "type": "object" + }, "VolumeVersionV1": { "properties": { "namespace": { @@ -13246,10 +14079,12 @@ "type": "object" }, "DeleteVolumeVersionRequestV1": { + "additionalProperties": false, "properties": { "expected_sequence": { "anyOf": [ { + "minimum": 0, "type": "integer" }, { @@ -13464,10 +14299,12 @@ "type": "object" }, "RestoreVolumeVersionRequestV1": { + "additionalProperties": false, "properties": { "expected_sequence": { "anyOf": [ { + "minimum": 0, "type": "integer" }, { @@ -13526,6 +14363,179 @@ "title": "RestoreVolumeVersionResponseV1", "type": "object" }, + "SetVolumeTagRequestV1": { + "additionalProperties": false, + "properties": { + "tag": { + "description": "Name of the tag to set. Tags are case-sensitive. Setting a tag that already exists overwrites it, and the version it pointed at before stays reachable by digest.", + "examples": [ + "nightly" + ], + "pattern": "^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$", + "title": "Tag", + "type": "string" + }, + "version": { + "description": "Version to set the tag on. Address it with `:` for a tag, `@` for a full content digest or a prefix of at least 12 hexadecimal characters, or `head` for the version a reference with no tag or digest resolves to.", + "examples": [ + "head" + ], + "title": "Version", + "type": "string" + }, + "expires_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Time at which the tag expires, in ISO 8601 format, with a UTC offset. It must be in the future and at most ten years ahead. Omit it for a tag with no expiration time of its own. Omitting it also clears the expiration time of an existing tag. Version or volume expiration still deletes the tag. Do not specify it when setting `head`.", + "title": "Expires At" + }, + "expected_sequence": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Revision the volume is expected to be at. When set, the request fails with a conflict if the volume has changed since. Take the value from volume_sequence.", + "title": "Expected Sequence" + } + }, + "required": [ + "tag", + "version" + ], + "title": "SetVolumeTagRequestV1", + "type": "object" + }, + "SetVolumeTagResponseV1": { + "properties": { + "namespace": { + "description": "Namespace the volume belongs to, in lowercase.", + "title": "Namespace", + "type": "string" + }, + "volume": { + "description": "Name of the volume, in lowercase.", + "title": "Volume", + "type": "string" + }, + "tag": { + "description": "Name of the tag that was set, with its original capitalization.", + "title": "Tag", + "type": "string" + }, + "digest": { + "description": "Content digest of the version the tag points at, as `b3:`.", + "title": "Digest", + "type": "string" + }, + "version_ref": { + "description": "Full address of the tagged version, as `bdn:/@`. Paste it into a config.yaml to pin this version, even if the tag is later set on another version.", + "title": "Version Ref", + "type": "string" + }, + "volume_sequence": { + "description": "Revision of the volume after the tag was set.", + "title": "Volume Sequence", + "type": "integer" + }, + "expires_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Tag expiration time in ISO 8601 format. Null when the tag has no expiration time of its own.", + "title": "Expires At" + } + }, + "required": [ + "namespace", + "volume", + "tag", + "digest", + "version_ref", + "volume_sequence", + "expires_at" + ], + "title": "SetVolumeTagResponseV1", + "type": "object" + }, + "DeleteVolumeTagRequestV1": { + "additionalProperties": false, + "properties": { + "expected_sequence": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Revision the volume is expected to be at. When set, the delete fails with a conflict if the volume has changed since, so it cannot delete a tag someone else has just set on another version. Take the value from volume_sequence.", + "title": "Expected Sequence" + } + }, + "title": "DeleteVolumeTagRequestV1", + "type": "object" + }, + "DeleteVolumeTagResponseV1": { + "properties": { + "namespace": { + "description": "Namespace the volume belongs to, in lowercase.", + "title": "Namespace", + "type": "string" + }, + "volume": { + "description": "Name of the volume, in lowercase.", + "title": "Volume", + "type": "string" + }, + "tag": { + "description": "Tag name from the request, with its original capitalization.", + "title": "Tag", + "type": "string" + }, + "deleted": { + "description": "Whether the tag existed and was deleted. False when the volume had no tag by that name, which is not an error.", + "title": "Deleted", + "type": "boolean" + }, + "volume_sequence": { + "description": "Revision of the volume after the delete.", + "title": "Volume Sequence", + "type": "integer" + } + }, + "required": [ + "namespace", + "volume", + "tag", + "deleted", + "volume_sequence" + ], + "title": "DeleteVolumeTagResponseV1", + "type": "object" + }, "TokenScopeV1": { "description": "What a token minted by POST /v1/token grants access to.", "enum": [ @@ -14627,6 +15637,9 @@ "USER_ROLE_UPDATED": "#/components/schemas/AuditLogEventUserRoleUpdatedV1", "USER_TEAM_ROLE_UPDATED": "#/components/schemas/AuditLogEventUserTeamRoleUpdatedV1", "VOLUME_DELETED": "#/components/schemas/AuditLogEventVolumeDeletedV1", + "VOLUME_TAG_DELETED": "#/components/schemas/AuditLogEventVolumeTagDeletedV1", + "VOLUME_TAG_SET": "#/components/schemas/AuditLogEventVolumeTagSetV1", + "VOLUME_UPDATED": "#/components/schemas/AuditLogEventVolumeUpdatedV1", "VOLUME_VERSION_DELETED": "#/components/schemas/AuditLogEventVolumeVersionDeletedV1", "VOLUME_VERSION_RESTORED": "#/components/schemas/AuditLogEventVolumeVersionRestoredV1", "WEBHOOK_SIGNING_SECRET_CREATED": "#/components/schemas/AuditLogEventWebhookSigningSecretCreatedV1", @@ -14785,6 +15798,15 @@ }, { "$ref": "#/components/schemas/AuditLogEventVolumeVersionRestoredV1" + }, + { + "$ref": "#/components/schemas/AuditLogEventVolumeTagSetV1" + }, + { + "$ref": "#/components/schemas/AuditLogEventVolumeTagDeletedV1" + }, + { + "$ref": "#/components/schemas/AuditLogEventVolumeUpdatedV1" } ], "title": "Event Data" @@ -17726,7 +18748,10 @@ "SSH_CERTIFICATE_SIGNED", "VOLUME_DELETED", "VOLUME_VERSION_DELETED", - "VOLUME_VERSION_RESTORED" + "VOLUME_VERSION_RESTORED", + "VOLUME_TAG_SET", + "VOLUME_TAG_DELETED", + "VOLUME_UPDATED" ], "title": "AuditLogEventTypeV1", "type": "string" @@ -17906,6 +18931,112 @@ "title": "AuditLogEventVolumeDeletedV1", "type": "object" }, + "AuditLogEventVolumeTagDeletedV1": { + "description": "Deleting a volume tag succeeded. `deleted` is false if the tag was already absent.", + "properties": { + "event_type": { + "const": "VOLUME_TAG_DELETED", + "title": "Event Type", + "type": "string" + }, + "volume_ref": { + "title": "Volume Ref", + "type": "string" + }, + "tag": { + "title": "Tag", + "type": "string" + }, + "deleted": { + "title": "Deleted", + "type": "boolean" + } + }, + "required": [ + "event_type", + "volume_ref", + "tag", + "deleted" + ], + "title": "AuditLogEventVolumeTagDeletedV1", + "type": "object" + }, + "AuditLogEventVolumeTagSetV1": { + "description": "A volume tag was set on a version.", + "properties": { + "event_type": { + "const": "VOLUME_TAG_SET", + "title": "Event Type", + "type": "string" + }, + "volume_ref": { + "title": "Volume Ref", + "type": "string" + }, + "tag": { + "title": "Tag", + "type": "string" + }, + "digest": { + "title": "Digest", + "type": "string" + } + }, + "required": [ + "event_type", + "volume_ref", + "tag", + "digest" + ], + "title": "AuditLogEventVolumeTagSetV1", + "type": "object" + }, + "AuditLogEventVolumeUpdatedV1": { + "description": "A volume's settings were updated.", + "properties": { + "event_type": { + "const": "VOLUME_UPDATED", + "title": "Event Type", + "type": "string" + }, + "volume_ref": { + "title": "Volume Ref", + "type": "string" + }, + "expires_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The volume's expiration time after the update, as an ISO 8601 string. Null when the update cleared it.", + "title": "Expires At" + }, + "previous_expires_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The expiration time the update replaced, as an ISO 8601 string. Null when the volume had none.", + "title": "Previous Expires At" + } + }, + "required": [ + "event_type", + "volume_ref", + "expires_at", + "previous_expires_at" + ], + "title": "AuditLogEventVolumeUpdatedV1", + "type": "object" + }, "AuditLogEventVolumeVersionDeletedV1": { "description": "One version of a volume was deleted.", "properties": { @@ -20209,6 +21340,497 @@ "title": "GetDeploymentMetricsRequestV1", "type": "object" }, + "StartupAttemptV1": { + "description": "One startup attempt of a replica.", + "properties": { + "model_id": { + "description": "Unique identifier of the model.", + "title": "Model Id", + "type": "string" + }, + "deployment_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Unique identifier of the deployment.", + "title": "Deployment Id" + }, + "deployment_name": { + "description": "Name of the deployment.", + "title": "Deployment Name", + "type": "string" + }, + "environment_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Name of the environment. Null when querying a deployment.", + "title": "Environment Name" + }, + "replica_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Identifier of the replica that started.", + "title": "Replica Id" + }, + "started_at_epoch_millis": { + "description": "Time the attempt started, in epoch milliseconds.", + "title": "Started At Epoch Millis", + "type": "integer" + }, + "ready_at_epoch_millis": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Time the replica became ready, in epoch milliseconds. Null if it never did.", + "title": "Ready At Epoch Millis" + }, + "duration_ms": { + "description": "Duration of the attempt, in milliseconds.", + "title": "Duration Ms", + "type": "integer" + }, + "outcome": { + "$ref": "#/components/schemas/StartupOutcomeV1", + "description": "Outcome of the attempt." + }, + "phase_summary": { + "description": "Phases recorded during the attempt.", + "items": { + "$ref": "#/components/schemas/StartupPhaseSummaryV1" + }, + "title": "Phase Summary", + "type": "array" + } + }, + "required": [ + "model_id", + "deployment_id", + "deployment_name", + "environment_name", + "replica_id", + "started_at_epoch_millis", + "ready_at_epoch_millis", + "duration_ms", + "outcome", + "phase_summary" + ], + "title": "StartupAttemptV1", + "type": "object" + }, + "StartupOutcomeV1": { + "description": "Outcome of a whole startup attempt.", + "enum": [ + "SUCCESS", + "FAILED", + "STALLED" + ], + "title": "StartupOutcomeV1", + "type": "string" + }, + "StartupPhaseStatusV1": { + "description": "Outcome of one phase, independent of the outcome of the whole startup.", + "enum": [ + "COMPLETED", + "FAILED" + ], + "title": "StartupPhaseStatusV1", + "type": "string" + }, + "StartupPhaseSummaryV1": { + "description": "One phase recorded during a startup attempt.", + "properties": { + "phase": { + "$ref": "#/components/schemas/StartupPhaseV1", + "description": "The phase." + }, + "status": { + "$ref": "#/components/schemas/StartupPhaseStatusV1", + "description": "Outcome of the phase." + }, + "offset_ms": { + "description": "Time from the start of the attempt to the start of the phase, in milliseconds.", + "title": "Offset Ms", + "type": "integer" + }, + "duration_ms": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Duration of the phase, in milliseconds. Can be 0 for a phase that finished within a second. Null if the phase did not finish.", + "title": "Duration Ms" + } + }, + "required": [ + "phase", + "status", + "offset_ms", + "duration_ms" + ], + "title": "StartupPhaseSummaryV1", + "type": "object" + }, + "StartupPhaseV1": { + "description": "A stage of a replica startup.\n\n- ``SCHEDULING``: waiting for the replica to be placed on a node.\n- ``IMAGE_PULL``: pulling the container image.\n- ``WEIGHT_DOWNLOAD``: downloading model weights.\n- ``CONTAINER_CREATE``: creating and starting the model container, including\n ``CONTAINER_START`` and ``READINESS``.\n- ``CONTAINER_START``: starting the model container.\n- ``READINESS``: waiting for the model to pass its readiness check.\n\n``CONTAINER_START`` and ``READINESS`` are accepted as filters but are reported only as\npart of ``CONTAINER_CREATE``.", + "enum": [ + "SCHEDULING", + "IMAGE_PULL", + "WEIGHT_DOWNLOAD", + "CONTAINER_CREATE", + "CONTAINER_START", + "READINESS" + ], + "title": "StartupPhaseV1", + "type": "string" + }, + "StartupAttemptsV1": { + "description": "A page of startup attempts, newest first by default.", + "properties": { + "items": { + "description": "Items in this page.", + "items": { + "$ref": "#/components/schemas/StartupAttemptV1" + }, + "title": "Items", + "type": "array" + }, + "pagination": { + "$ref": "#/components/schemas/PaginationResponseV1", + "description": "Pagination metadata for the page." + }, + "total_count": { + "description": "Number of attempts matching the filters, counted within the 1,000 most recent startups in the window.", + "title": "Total Count", + "type": "integer" + } + }, + "required": [ + "items", + "pagination", + "total_count" + ], + "title": "StartupAttemptsV1", + "type": "object" + }, + "StartupSortFieldV1": { + "description": "Field to sort startup attempts by.", + "enum": [ + "OUTCOME", + "STARTED_AT", + "READY_AT", + "DURATION" + ], + "title": "StartupSortFieldV1", + "type": "string" + }, + "GetStartupAttemptsRequestV1": { + "description": "Query params for a startup attempts request (a deployment or an environment).", + "properties": { + "start_epoch_millis": { + "description": "Start of the query window, in epoch milliseconds.", + "title": "Start Epoch Millis", + "type": "integer" + }, + "end_epoch_millis": { + "description": "End of the query window, in epoch milliseconds. Must be after the start.", + "title": "End Epoch Millis", + "type": "integer" + }, + "min_duration_ms": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Only include startups that lasted at least this many milliseconds.", + "title": "Min Duration Ms" + }, + "cursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Opaque cursor returned by a previous page. Omit to fetch the first page. Pass the same filters and sort as the request that returned it.", + "title": "Cursor" + }, + "limit": { + "default": 50, + "description": "Maximum number of attempts to return.", + "maximum": 100, + "minimum": 1, + "title": "Limit", + "type": "integer" + }, + "outcomes": { + "description": "Only include attempts with one of these outcomes. Repeat to pass several.", + "items": { + "$ref": "#/components/schemas/StartupOutcomeV1" + }, + "title": "Outcomes", + "type": "array" + }, + "phase": { + "anyOf": [ + { + "$ref": "#/components/schemas/StartupPhaseV1" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Only include attempts that spent longer than 0 ms in this phase." + }, + "search": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Only include attempts whose replica ID matches exactly, or whose deployment name contains this text (case-insensitive).", + "title": "Search" + }, + "sort_by": { + "anyOf": [ + { + "$ref": "#/components/schemas/StartupSortFieldV1" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Field to sort attempts by. Defaults to `STARTED_AT`." + }, + "direction": { + "anyOf": [ + { + "$ref": "#/components/schemas/SortOrderV1" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Sort direction. Defaults to `desc`." + } + }, + "required": [ + "start_epoch_millis", + "end_epoch_millis" + ], + "title": "GetStartupAttemptsRequestV1", + "type": "object" + }, + "StartupPercentilesV1": { + "description": "A distribution of startup durations.\n\nStatistics cover only samples longer than 0 ms and are null when there are none.", + "properties": { + "sample_count": { + "description": "Number of startups in the distribution, including those that took 0 ms. A phase that finishes within a second can record 0 ms.", + "title": "Sample Count", + "type": "integer" + }, + "mean_ms": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Mean duration, in milliseconds.", + "title": "Mean Ms" + }, + "p50_ms": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "50th percentile duration, in milliseconds.", + "title": "P50 Ms" + }, + "p90_ms": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "90th percentile duration, in milliseconds.", + "title": "P90 Ms" + }, + "p95_ms": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "95th percentile duration, in milliseconds.", + "title": "P95 Ms" + }, + "p99_ms": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "99th percentile duration, in milliseconds.", + "title": "P99 Ms" + } + }, + "required": [ + "sample_count", + "mean_ms", + "p50_ms", + "p90_ms", + "p95_ms", + "p99_ms" + ], + "title": "StartupPercentilesV1", + "type": "object" + }, + "StartupPhasePercentilesV1": { + "description": "The duration distribution of one startup phase.", + "properties": { + "phase": { + "$ref": "#/components/schemas/StartupPhaseV1", + "description": "The phase." + }, + "duration_ms": { + "$ref": "#/components/schemas/StartupPercentilesV1", + "description": "Durations of the phase." + } + }, + "required": [ + "phase", + "duration_ms" + ], + "title": "StartupPhasePercentilesV1", + "type": "object" + }, + "StartupSummaryV1": { + "description": "Startup duration distributions over a time window.", + "properties": { + "start_epoch_millis": { + "description": "Start of the returned window.", + "title": "Start Epoch Millis", + "type": "integer" + }, + "end_epoch_millis": { + "description": "End of the returned window.", + "title": "End Epoch Millis", + "type": "integer" + }, + "total_duration_ms": { + "$ref": "#/components/schemas/StartupPercentilesV1", + "description": "Durations of whole startups, sampled from the 1,000 most recent in the window." + }, + "phase_duration_ms": { + "description": "Durations of each phase that was recorded in the window.", + "items": { + "$ref": "#/components/schemas/StartupPhasePercentilesV1" + }, + "title": "Phase Duration Ms", + "type": "array" + } + }, + "required": [ + "start_epoch_millis", + "end_epoch_millis", + "total_duration_ms", + "phase_duration_ms" + ], + "title": "StartupSummaryV1", + "type": "object" + }, + "GetStartupSummaryRequestV1": { + "description": "Query params for a startup summary request (a deployment or an environment).", + "properties": { + "start_epoch_millis": { + "description": "Start of the query window, in epoch milliseconds.", + "title": "Start Epoch Millis", + "type": "integer" + }, + "end_epoch_millis": { + "description": "End of the query window, in epoch milliseconds. Must be after the start.", + "title": "End Epoch Millis", + "type": "integer" + }, + "min_duration_ms": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Only include startups that lasted at least this many milliseconds.", + "title": "Min Duration Ms" + } + }, + "required": [ + "start_epoch_millis", + "end_epoch_millis" + ], + "title": "GetStartupSummaryRequestV1", + "type": "object" + }, "TerminateReplicaResponseV1": { "description": "The response to a request to terminate a replica in a deployment.", "properties": { @@ -25833,6 +27455,19 @@ "title": "Base Model", "type": "string" }, + "lora_config_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The run's immutable LoRA config version.", + "title": "Lora Config Version" + }, "base_url": { "description": "The run's base URL.", "title": "Base Url", @@ -25914,14 +27549,30 @@ "type": "string" }, "model_id": { - "description": "Hashid of the underlying Baseten model.", - "title": "Model Id", - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Hashid of the underlying Baseten model. Null for a serverless sampler.", + "title": "Model Id" }, "deployment_id": { - "description": "Hashid of the specific model deployment (version).", - "title": "Deployment Id", - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Hashid of the specific model deployment (version). Null for a serverless sampler.", + "title": "Deployment Id" }, "status": { "$ref": "#/components/schemas/LoopsSamplerStatusV1", @@ -25955,8 +27606,6 @@ "base_url", "base_model", "created_at", - "model_id", - "deployment_id", "status", "user" ], @@ -26637,6 +28286,17 @@ "title": "Id", "type": "string" }, + "lora_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/LoopsLoRAConfigV1" + }, + { + "type": "null" + } + ], + "description": "The immutable LoRA config used to save this checkpoint. Null means the checkpoint is unsupported and cannot be loaded." + }, "run_id": { "description": "The ID of the run that produced the checkpoint.", "title": "Run Id", @@ -26655,12 +28315,55 @@ "lora_adapter_config", "size_bytes", "id", + "lora_config", "run_id", "target" ], "title": "LoopsCheckpointV1", "type": "object" }, + "LoopsLoRAConfigV1": { + "description": "A published LoRA config: which modules the adapter targets and how experts are laid out.", + "properties": { + "config_version": { + "description": "The config version, unique per base model.", + "title": "Config Version", + "type": "string" + }, + "targets": { + "description": "The module names the LoRA adapter targets.", + "items": { + "type": "string" + }, + "title": "Targets", + "type": "array" + }, + "expert_layout": { + "anyOf": [ + { + "enum": [ + "per_expert", + "shared_outer", + "shared_adapter" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "How routed-expert LoRA weights are laid out. Null when no experts are targeted.", + "title": "Expert Layout" + } + }, + "required": [ + "config_version", + "targets" + ], + "title": "LoopsLoRAConfigV1", + "type": "object" + }, "TrainerCheckpointTarget": { "description": "Whether a TrainerServerCheckpoint is loadable by the sampler or the trainer.\n\nSAMPLER checkpoints are consumed by the sampling server for inference;\nTRAINER checkpoints capture full trainer state for resuming training.\nMirrored in the bt:// URI as\n``bt://loops:/(sampler_weights|weights)/``.", "enum": [ @@ -26753,6 +28456,22 @@ ], "title": "Checkpoint Path", "type": "string" + }, + "run_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Loops run whose sampler will load the checkpoint. The checkpoint must use the run's LoRA config.", + "examples": [ + "k4q95w5" + ], + "title": "Run Id" } }, "required": [ @@ -26762,7 +28481,7 @@ "type": "object" }, "ValidateLoopsCheckpointResponseV1": { - "description": "Response for ``POST /v1/loops/checkpoints/validate``. Empty on success;\ninaccessible or malformed paths raise 400.", + "description": "Response for ``POST /v1/loops/checkpoints/validate``. Empty on success;\ninaccessible or malformed paths, and checkpoints of another LoRA config than\n``run_id``'s, raise 400.", "properties": {}, "title": "ValidateLoopsCheckpointResponseV1", "type": "object" @@ -28833,6 +30552,19 @@ "description": "Model that served the usage. Null when not grouping by model.", "title": "Model" }, + "team_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Team the usage is attributed to. Null when not grouping by team or when team attribution is unavailable.", + "title": "Team Id" + }, "input_tokens": { "description": "Total input tokens, cached and uncached combined.", "title": "Input Tokens", @@ -28905,7 +30637,8 @@ "enum": [ "api_key", "user", - "model" + "model", + "team" ], "title": "UsageDimensionV1", "type": "string" @@ -28946,7 +30679,7 @@ "description": "Width of each time bucket: 1m, 1h, or 1d. Defaults to 1d." }, "group_by": { - "description": "Dimensions to break usage down by, repeated once per dimension: api_key, user, model. Defaults to model.", + "description": "Dimensions to break usage down by, repeated once per dimension: api_key, user, model, or team. Defaults to model.", "items": { "$ref": "#/components/schemas/UsageDimensionV1" }, @@ -28977,6 +30710,14 @@ "title": "Models", "type": "array" }, + "team_ids": { + "description": "Return only usage for these teams, repeated once per team. Defaults to all teams you can manage.", + "items": { + "type": "string" + }, + "title": "Team Ids", + "type": "array" + }, "limit": { "anyOf": [ { @@ -29007,6 +30748,54 @@ "title": "ModelApisUsageRequestV1", "type": "object" }, + "EgressRestrictionsV1": { + "properties": { + "fqdn_allow_list": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Allowed outbound FQDNs; '*' wildcards are supported.", + "examples": [ + [ + "*.example.com" + ] + ], + "title": "Fqdn Allow List" + }, + "ip_allow_list": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Allowed outbound IPv4 addresses or CIDR ranges.", + "examples": [ + [ + "1.1.1.1/32" + ] + ], + "title": "Ip Allow List" + } + }, + "title": "EgressRestrictionsV1", + "type": "object" + }, "CreateLLMModelRequestV1": { "description": "A request to create a BIS-LLM model", "properties": { @@ -29161,6 +30950,18 @@ ], "title": "Weights" }, + "egress_restrictions": { + "anyOf": [ + { + "$ref": "#/components/schemas/EgressRestrictionsV1" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Restricts this deployment's egress to the specified FQDNs and IP addresses; an empty block allows none. Requires the organization to have egress restrictions enabled." + }, "name": { "description": "Name of the model", "title": "Name", @@ -29367,6 +31168,18 @@ ] ], "title": "Weights" + }, + "egress_restrictions": { + "anyOf": [ + { + "$ref": "#/components/schemas/EgressRestrictionsV1" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Restricts this deployment's egress to the specified FQDNs and IP addresses; an empty block allows none. Requires the organization to have egress restrictions enabled." } }, "required": [ @@ -29528,6 +31341,11 @@ "title": "Is Public", "type": "boolean" }, + "is_listed": { + "description": "Whether the listing appears in catalogs; unlisted listings still deploy from a direct link", + "title": "Is Listed", + "type": "boolean" + }, "trending": { "anyOf": [ { @@ -29574,6 +31392,7 @@ "display_name", "user_defined_id", "is_public", + "is_listed", "trending", "closed_source", "created_at", @@ -29618,6 +31437,12 @@ "title": "Is Public", "type": "boolean" }, + "is_listed": { + "default": true, + "description": "Whether the listing appears in catalogs; unlisted listings still deploy from a direct link", + "title": "Is Listed", + "type": "boolean" + }, "closed_source": { "default": false, "description": "Whether the listing is closed source (deployers cannot view or download the Truss, and forks copy mirrored weights instead of re-mirroring from upstream)", @@ -29695,6 +31520,19 @@ "description": "Whether the listing is trending", "title": "Trending" }, + "is_listed": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Whether the listing appears in catalogs; unlisted listings still deploy from a direct link", + "title": "Is Listed" + }, "metadata": { "anyOf": [ { @@ -30080,6 +31918,18 @@ "description": "Human-readable tag for this version", "title": "Version Tag", "type": "string" + }, + "is_listed": { + "default": true, + "description": "Whether the listing appears in catalogs; unlisted listings still deploy from a direct link. Only used when creating a new listing.", + "title": "Is Listed", + "type": "boolean" + }, + "replace_model": { + "default": false, + "description": "Allow publishing from a different model than the listing's current one. The listing then points at the new model; earlier versions and existing deployments are unaffected. Fails if the new model already backs another listing.", + "title": "Replace Model", + "type": "boolean" } }, "required": [ @@ -32013,35 +33863,6 @@ "title": "RouteRefV1", "type": "object" }, - "RouteTargetAnthropicV1": { - "properties": { - "type": { - "const": "ANTHROPIC", - "description": "Target kind for Anthropic.", - "title": "Type", - "type": "string" - }, - "model": { - "description": "Model name sent to the provider.", - "minLength": 1, - "title": "Model", - "type": "string" - }, - "secret_name": { - "description": "Name of a credential secret owned by the route's team.", - "minLength": 1, - "title": "Secret Name", - "type": "string" - } - }, - "required": [ - "type", - "model", - "secret_name" - ], - "title": "RouteTargetAnthropicV1", - "type": "object" - }, "RouteTargetBasetenModelAPIV1": { "properties": { "type": { @@ -32118,40 +33939,11 @@ "title": "RouteTargetClassifierModelBasedV1", "type": "object" }, - "RouteTargetOpenAIV1": { - "properties": { - "type": { - "const": "OPENAI", - "description": "Target kind for OpenAI.", - "title": "Type", - "type": "string" - }, - "model": { - "description": "Model name sent to the provider.", - "minLength": 1, - "title": "Model", - "type": "string" - }, - "secret_name": { - "description": "Name of a credential secret owned by the route's team.", - "minLength": 1, - "title": "Secret Name", - "type": "string" - } - }, - "required": [ - "type", - "model", - "secret_name" - ], - "title": "RouteTargetOpenAIV1", - "type": "object" - }, - "RouteTargetXAIV1": { + "RouteTargetConnectionV1": { "properties": { "type": { - "const": "XAI", - "description": "Target kind for xAI.", + "const": "CONNECTION", + "description": "Target kind for a provider reached through a team connection.", "title": "Type", "type": "string" }, @@ -32161,19 +33953,18 @@ "title": "Model", "type": "string" }, - "secret_name": { - "description": "Name of a credential secret owned by the route's team.", - "minLength": 1, - "title": "Secret Name", + "connection_id": { + "description": "Identifier of the team connection authenticating with the provider.", + "title": "Connection Id", "type": "string" } }, "required": [ "type", "model", - "secret_name" + "connection_id" ], - "title": "RouteTargetXAIV1", + "title": "RouteTargetConnectionV1", "type": "object" }, "RouteV1": { @@ -32212,11 +34003,9 @@ "description": "Configured upstream target.", "discriminator": { "mapping": { - "ANTHROPIC": "#/components/schemas/RouteTargetAnthropicV1", "BASETEN_MODEL_API": "#/components/schemas/RouteTargetBasetenModelAPIV1", "CLASSIFIER_MODEL_BASED": "#/components/schemas/RouteTargetClassifierModelBasedV1", - "OPENAI": "#/components/schemas/RouteTargetOpenAIV1", - "XAI": "#/components/schemas/RouteTargetXAIV1" + "CONNECTION": "#/components/schemas/RouteTargetConnectionV1" }, "propertyName": "type" }, @@ -32225,13 +34014,7 @@ "$ref": "#/components/schemas/RouteTargetBasetenModelAPIV1" }, { - "$ref": "#/components/schemas/RouteTargetAnthropicV1" - }, - { - "$ref": "#/components/schemas/RouteTargetOpenAIV1" - }, - { - "$ref": "#/components/schemas/RouteTargetXAIV1" + "$ref": "#/components/schemas/RouteTargetConnectionV1" }, { "$ref": "#/components/schemas/RouteTargetClassifierModelBasedV1" @@ -32352,37 +34135,7 @@ "title": "RoutesRequestV1", "type": "object" }, - "RouteTargetConfigAnthropicV1": { - "additionalProperties": false, - "properties": { - "type": { - "const": "ANTHROPIC", - "description": "Target kind for Anthropic.", - "title": "Type", - "type": "string" - }, - "model": { - "description": "Model name sent to the provider.", - "minLength": 1, - "title": "Model", - "type": "string" - }, - "secret_name": { - "description": "Name of a credential secret owned by the route's team.", - "minLength": 1, - "title": "Secret Name", - "type": "string" - } - }, - "required": [ - "type", - "model", - "secret_name" - ], - "title": "RouteTargetConfigAnthropicV1", - "type": "object" - }, - "RouteTargetConfigBasetenModelAPIV1": { + "UpsertRouteTargetBasetenModelAPIV1": { "additionalProperties": false, "properties": { "type": { @@ -32402,10 +34155,10 @@ "type", "model" ], - "title": "RouteTargetConfigBasetenModelAPIV1", + "title": "UpsertRouteTargetBasetenModelAPIV1", "type": "object" }, - "RouteTargetConfigClassifierModelBasedV1": { + "UpsertRouteTargetClassifierModelBasedV1": { "additionalProperties": false, "properties": { "type": { @@ -32469,15 +34222,15 @@ "allowed_route_ids", "default_route_id" ], - "title": "RouteTargetConfigClassifierModelBasedV1", + "title": "UpsertRouteTargetClassifierModelBasedV1", "type": "object" }, - "RouteTargetConfigOpenAIV1": { + "UpsertRouteTargetConnectionV1": { "additionalProperties": false, "properties": { "type": { - "const": "OPENAI", - "description": "Target kind for OpenAI.", + "const": "CONNECTION", + "description": "Target kind for a provider reached through a team connection.", "title": "Type", "type": "string" }, @@ -32487,49 +34240,22 @@ "title": "Model", "type": "string" }, - "secret_name": { - "description": "Name of a credential secret owned by the route's team.", - "minLength": 1, - "title": "Secret Name", - "type": "string" - } - }, - "required": [ - "type", - "model", - "secret_name" - ], - "title": "RouteTargetConfigOpenAIV1", - "type": "object" - }, - "RouteTargetConfigXAIV1": { - "additionalProperties": false, - "properties": { - "type": { - "const": "XAI", - "description": "Target kind for xAI.", - "title": "Type", - "type": "string" - }, - "model": { - "description": "Model name sent to the provider.", - "minLength": 1, - "title": "Model", - "type": "string" - }, - "secret_name": { - "description": "Name of a credential secret owned by the route's team.", + "connection_id": { + "description": "Identifier of a connection owned by the route's team. Its provider serves the route.", + "examples": [ + "abc1234" + ], "minLength": 1, - "title": "Secret Name", + "title": "Connection Id", "type": "string" } }, "required": [ "type", "model", - "secret_name" + "connection_id" ], - "title": "RouteTargetConfigXAIV1", + "title": "UpsertRouteTargetConnectionV1", "type": "object" }, "CreateRouteRequestV1": { @@ -32571,14 +34297,12 @@ "title": "Display Name" }, "target": { - "description": "Upstream target for the route.", + "description": "Upstream target for the route. CONNECTION targets use the provider and credentials of a connection owned by the route's team.", "discriminator": { "mapping": { - "ANTHROPIC": "#/components/schemas/RouteTargetConfigAnthropicV1", - "BASETEN_MODEL_API": "#/components/schemas/RouteTargetConfigBasetenModelAPIV1", - "CLASSIFIER_MODEL_BASED": "#/components/schemas/RouteTargetConfigClassifierModelBasedV1", - "OPENAI": "#/components/schemas/RouteTargetConfigOpenAIV1", - "XAI": "#/components/schemas/RouteTargetConfigXAIV1" + "BASETEN_MODEL_API": "#/components/schemas/UpsertRouteTargetBasetenModelAPIV1", + "CLASSIFIER_MODEL_BASED": "#/components/schemas/UpsertRouteTargetClassifierModelBasedV1", + "CONNECTION": "#/components/schemas/UpsertRouteTargetConnectionV1" }, "propertyName": "type" }, @@ -32590,19 +34314,13 @@ ], "oneOf": [ { - "$ref": "#/components/schemas/RouteTargetConfigBasetenModelAPIV1" - }, - { - "$ref": "#/components/schemas/RouteTargetConfigAnthropicV1" - }, - { - "$ref": "#/components/schemas/RouteTargetConfigOpenAIV1" + "$ref": "#/components/schemas/UpsertRouteTargetBasetenModelAPIV1" }, { - "$ref": "#/components/schemas/RouteTargetConfigXAIV1" + "$ref": "#/components/schemas/UpsertRouteTargetConnectionV1" }, { - "$ref": "#/components/schemas/RouteTargetConfigClassifierModelBasedV1" + "$ref": "#/components/schemas/UpsertRouteTargetClassifierModelBasedV1" } ], "title": "Target" @@ -32683,6 +34401,19 @@ "description": "ID of the user who created the Routes key. Null when not grouping by USER.", "title": "User Id" }, + "team_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "ID of the team the usage was metered in. Null when not grouping by TEAM, and for usage metered before it was attributed to teams.", + "title": "Team Id" + }, "model": { "anyOf": [ { @@ -32708,6 +34439,19 @@ "default": null, "description": "Provider that served the requests. Null when not grouping by PROVIDER." }, + "router_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Slug of the intelligent router, matching its name in `GET /v1/routes`. Null when not grouping by ROUTER.", + "title": "Router Id" + }, "cost_usd": { "description": "Estimated cost in USD, returned as an exact decimal string. Costs for OpenAI, Anthropic, and xAI estimate what you pay those providers; they are not Baseten charges.", "examples": [ @@ -32735,6 +34479,11 @@ "description": "Output tokens.", "title": "Output Tokens", "type": "integer" + }, + "requests": { + "description": "Number of requests; with ROUTER, the requests sent through that router. Usage recorded before requests were counted adds zero.", + "title": "Requests", + "type": "integer" } }, "required": [ @@ -32742,7 +34491,8 @@ "input_tokens", "cached_input_tokens", "uncached_input_tokens", - "output_tokens" + "output_tokens", + "requests" ], "title": "RoutesUsageResultV1", "type": "object" @@ -32772,8 +34522,10 @@ "RouteUsageDimensionV1": { "enum": [ "USER", + "TEAM", "MODEL", - "PROVIDER" + "PROVIDER", + "ROUTER" ], "title": "RouteUsageDimensionV1", "type": "string" @@ -32801,6 +34553,19 @@ "title": "Limit", "type": "integer" }, + "team_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Limit usage to a team you can access. Organization admins can view all usage in the team, team admins can view usage in teams they manage, and other members can view only their own usage. Without a team, organization admins see all organization usage, team admins see their managed teams, and other members see their own usage.", + "title": "Team Id" + }, "start_date": { "anyOf": [ { @@ -32830,7 +34595,7 @@ "title": "End Date" }, "group_by": { - "description": "Dimensions to break usage down by, repeated once per dimension: USER, MODEL, or PROVIDER. Each result represents one observed combination of the requested dimensions within that day, and results are sorted by those values. Combinations without usage are omitted, so result counts can differ between days. Defaults to MODEL.", + "description": "Dimensions to break usage down by, repeated once per dimension: USER, TEAM, MODEL, PROVIDER, or ROUTER. Each result represents one observed combination of the requested dimensions within that day, and results are sorted by those values. Combinations without usage are omitted, so result counts can differ between days. ROUTER returns only usage sent through an intelligent router, one row per router, or per router and model with MODEL. Router rows repeat usage already counted in other groupings, so don't add them to model totals. Defaults to MODEL.", "items": { "$ref": "#/components/schemas/RouteUsageDimensionV1" }, @@ -32909,6 +34674,14 @@ "title": "RouteSettingSourceV1", "type": "string" }, + "RouteSpendBlockedByV1": { + "enum": [ + "member", + "team" + ], + "title": "RouteSpendBlockedByV1", + "type": "string" + }, "RouteSpendLimitSettingV1": { "properties": { "user_monthly_limit_usd": { @@ -32920,12 +34693,27 @@ "type": "null" } ], - "description": "Standing spend limit in USD for each UTC calendar month set on the user. Null when the user has no limit of their own.", + "description": "Standing spend limit in USD for each UTC calendar month set on the user in the team. Null when the user has no limit of their own there.", "examples": [ "200" ], "title": "User Monthly Limit Usd" }, + "month_override_usd": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Spend limit in USD for the current UTC calendar month set on the user in the team, which supersedes their standing limit. Null when there is none.", + "examples": [ + "300" + ], + "title": "Month Override Usd" + }, "team_default": { "anyOf": [ { @@ -32935,17 +34723,34 @@ "type": "null" } ], - "description": "Per-member limit of the team the user's active Code key belongs to. This limit applies when the user has no limit of their own. Null when there is none." + "description": "Per-member limit of the team. This limit applies when the user has no limit of their own. Null when there is none." }, "effective": { "$ref": "#/components/schemas/RouteEffectiveSpendLimitV1", "description": "The effective limit enforced for the current month." + }, + "team_wide_monthly_limit_usd": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Team-wide limit of the team in USD for each UTC calendar month. Once all members' spend in the team reaches it, the user's requests in the team are rejected whatever their own limit. Null when there is none.", + "examples": [ + "5000" + ], + "title": "Team Wide Monthly Limit Usd" } }, "required": [ "user_monthly_limit_usd", + "month_override_usd", "team_default", - "effective" + "effective", + "team_wide_monthly_limit_usd" ], "title": "RouteSpendLimitSettingV1", "type": "object" @@ -32953,7 +34758,7 @@ "RouteSpendLimitTeamDefaultV1": { "properties": { "team_id": { - "description": "ID of the team the user's active Code key belongs to.", + "description": "ID of the team whose per-member limit this is.", "examples": [ "abc1234" ], @@ -32976,8 +34781,24 @@ "title": "RouteSpendLimitTeamDefaultV1", "type": "object" }, - "RouteUserSettingsV1": { + "RouteTeamUserSettingsV1": { "properties": { + "team_id": { + "description": "ID of the team.", + "examples": [ + "abc1234" + ], + "title": "Team Id", + "type": "string" + }, + "team_name": { + "description": "Name of the team.", + "examples": [ + "Engineering" + ], + "title": "Team Name", + "type": "string" + }, "user_id": { "description": "ID of the user.", "examples": [ @@ -33003,61 +34824,61 @@ }, "spend_limit": { "$ref": "#/components/schemas/RouteSpendLimitSettingV1", - "description": "Spend limit for Baseten Code. Applies to requests with Routes keys the user created; personal API keys are not limited." - } - }, - "required": [ - "user_id", - "email", - "spend_limit" - ], - "title": "RouteUserSettingsV1", - "type": "object" - }, - "UpdateRouteSpendLimitSettingV1": { - "additionalProperties": false, - "minProperties": 1, - "properties": { - "user_monthly_limit_usd": { + "description": "Spend limit for Baseten Code in the team. Applies to requests with Routes keys the user created in the team; personal API keys are not limited." + }, + "month_spend_usd": { + "description": "The user's metered Code spend in USD in the team this UTC calendar month, returned as an exact decimal string.", + "examples": [ + "12.5" + ], + "title": "Month Spend Usd", + "type": "string" + }, + "is_blocked": { + "description": "Whether the user's spend this month reached their limit, or the team's spend reached its team-wide limit, so requests with Routes keys they created in the team are rejected.", + "title": "Is Blocked", + "type": "boolean" + }, + "blocked_by": { "anyOf": [ { - "type": "string" + "$ref": "#/components/schemas/RouteSpendBlockedByV1" }, { "type": "null" } ], - "default": null, - "description": "Standing spend limit in USD for each UTC calendar month. Send null to remove the limit; omit to leave it unchanged.", - "examples": [ - "200" - ], - "title": "User Monthly Limit Usd", - "x-null-distinct": true + "description": "`member` when the user's spend this month reached their limit, `team` when the team's spend reached its team-wide limit. Null when the user is not blocked." } }, - "title": "UpdateRouteSpendLimitSettingV1", + "required": [ + "team_id", + "team_name", + "user_id", + "email", + "spend_limit", + "month_spend_usd", + "is_blocked", + "blocked_by" + ], + "title": "RouteTeamUserSettingsV1", "type": "object" }, - "UpdateRouteUserSettingsRequestV1": { - "additionalProperties": false, - "minProperties": 1, + "RouteUserTeamSettingsListV1": { "properties": { - "spend_limit": { - "anyOf": [ - { - "$ref": "#/components/schemas/UpdateRouteSpendLimitSettingV1" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Spend limit fields to change. Pass null to remove the user's limit; omit to leave it unchanged.", - "x-null-distinct": true + "teams": { + "description": "The user's settings in each team they have a role in and in any team they have Code spend in this month, alphabetical by team name.", + "items": { + "$ref": "#/components/schemas/RouteTeamUserSettingsV1" + }, + "title": "Teams", + "type": "array" } }, - "title": "UpdateRouteUserSettingsRequestV1", + "required": [ + "teams" + ], + "title": "RouteUserTeamSettingsListV1", "type": "object" }, "BackgroundHarnessDefaultsV1": { @@ -33169,15 +34990,31 @@ "type": "null" } ], - "description": "Spend limit in USD for each UTC calendar month that applies to each member whose active Code key belongs to this team, unless the member has a limit of their own. Returned as an exact decimal string. Null when the team has no per-member limit.", + "description": "Spend limit in USD for each UTC calendar month that applies to each member's Code spend in this team, unless the member has a limit of their own. Returned as an exact decimal string. Null when the team has no per-member limit.", "examples": [ "200" ], "title": "Per Member Monthly Limit Usd" + }, + "team_wide_monthly_limit_usd": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Spend limit in USD for each UTC calendar month on the Code spend of all members in this team together. Once it is reached, every member's requests in the team are rejected. Returned as an exact decimal string. Null when the team has no team-wide limit.", + "examples": [ + "5000" + ], + "title": "Team Wide Monthly Limit Usd" } }, "required": [ - "per_member_monthly_limit_usd" + "per_member_monthly_limit_usd", + "team_wide_monthly_limit_usd" ], "title": "RouteTeamSpendLimitSettingV1", "type": "object" @@ -33198,7 +35035,7 @@ }, "spend_limit": { "$ref": "#/components/schemas/RouteTeamSpendLimitSettingV1", - "description": "Spend limits for Baseten Code for members whose active Code key belongs to this team." + "description": "Spend limits for Baseten Code for members' spend in this team." } }, "required": [ @@ -33353,12 +35190,29 @@ } ], "default": null, - "description": "Spend limit in USD for each UTC calendar month that applies to each member whose active Code key belongs to this team, unless the member has a limit of their own. Send null to remove it; omit to leave it unchanged.", + "description": "Spend limit in USD for each UTC calendar month that applies to each member's Code spend in this team, unless the member has a limit of their own. Send null to remove it; omit to leave it unchanged.", "examples": [ "200" ], "title": "Per Member Monthly Limit Usd", "x-null-distinct": true + }, + "team_wide_monthly_limit_usd": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Spend limit in USD for each UTC calendar month on the Code spend of all members in this team together. Send null to remove it; omit to leave it unchanged.", + "examples": [ + "5000" + ], + "title": "Team Wide Monthly Limit Usd", + "x-null-distinct": true } }, "title": "UpdateRouteTeamSpendLimitSettingV1", @@ -33391,14 +35245,94 @@ } ], "default": null, - "description": "Spend limit fields to change. Pass null to remove the team's per-member limit; omit to leave it unchanged.", + "description": "Spend limit fields to change. Pass null to remove the team's limits; omit to leave them unchanged.", "x-null-distinct": true } }, "title": "UpdateRouteTeamSettingsRequestV1", "type": "object" }, - "RouteConnectionAnthropicV1": { + "RouteTeamUserSettingsListV1": { + "properties": { + "users": { + "description": "Settings of each user with a role in the team and of anyone with Code spend in the team this month.", + "items": { + "$ref": "#/components/schemas/RouteTeamUserSettingsV1" + }, + "title": "Users", + "type": "array" + } + }, + "required": [ + "users" + ], + "title": "RouteTeamUserSettingsListV1", + "type": "object" + }, + "UpdateRouteSpendLimitSettingV1": { + "additionalProperties": false, + "minProperties": 1, + "properties": { + "user_monthly_limit_usd": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Standing spend limit in USD for each UTC calendar month. Send null to remove the limit; omit to leave it unchanged.", + "examples": [ + "200" + ], + "title": "User Monthly Limit Usd", + "x-null-distinct": true + }, + "month_override_usd": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Spend limit in USD for the current UTC calendar month, which supersedes the standing limit. Send null to remove it; omit to leave it unchanged.", + "examples": [ + "300" + ], + "title": "Month Override Usd", + "x-null-distinct": true + } + }, + "title": "UpdateRouteSpendLimitSettingV1", + "type": "object" + }, + "UpdateRouteUserSettingsRequestV1": { + "additionalProperties": false, + "minProperties": 1, + "properties": { + "spend_limit": { + "anyOf": [ + { + "$ref": "#/components/schemas/UpdateRouteSpendLimitSettingV1" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Spend limit fields to change. Pass null to remove the user's limit; omit to leave it unchanged.", + "x-null-distinct": true + } + }, + "title": "UpdateRouteUserSettingsRequestV1", + "type": "object" + }, + "RouteConnectionConfigAnthropicV1": { "properties": { "provider": { "const": "ANTHROPIC", @@ -33422,10 +35356,10 @@ "secret_id", "secret_name" ], - "title": "RouteConnectionAnthropicV1", + "title": "RouteConnectionConfigAnthropicV1", "type": "object" }, - "RouteConnectionOpenAIV1": { + "RouteConnectionConfigOpenAIV1": { "properties": { "provider": { "const": "OPENAI", @@ -33449,7 +35383,34 @@ "secret_id", "secret_name" ], - "title": "RouteConnectionOpenAIV1", + "title": "RouteConnectionConfigOpenAIV1", + "type": "object" + }, + "RouteConnectionConfigXAIV1": { + "properties": { + "provider": { + "const": "XAI", + "description": "Provider kind for xAI.", + "title": "Provider", + "type": "string" + }, + "secret_id": { + "description": "Identifier of the team secret holding the provider API key.", + "title": "Secret Id", + "type": "string" + }, + "secret_name": { + "description": "Name of the team secret holding the provider API key.", + "title": "Secret Name", + "type": "string" + } + }, + "required": [ + "provider", + "secret_id", + "secret_name" + ], + "title": "RouteConnectionConfigXAIV1", "type": "object" }, "RouteConnectionV1": { @@ -33468,21 +35429,21 @@ "description": "Provider the connection authenticates with, and the team secret holding its API key.", "discriminator": { "mapping": { - "ANTHROPIC": "#/components/schemas/RouteConnectionAnthropicV1", - "OPENAI": "#/components/schemas/RouteConnectionOpenAIV1", - "XAI": "#/components/schemas/RouteConnectionXAIV1" + "ANTHROPIC": "#/components/schemas/RouteConnectionConfigAnthropicV1", + "OPENAI": "#/components/schemas/RouteConnectionConfigOpenAIV1", + "XAI": "#/components/schemas/RouteConnectionConfigXAIV1" }, "propertyName": "provider" }, "oneOf": [ { - "$ref": "#/components/schemas/RouteConnectionAnthropicV1" + "$ref": "#/components/schemas/RouteConnectionConfigAnthropicV1" }, { - "$ref": "#/components/schemas/RouteConnectionOpenAIV1" + "$ref": "#/components/schemas/RouteConnectionConfigOpenAIV1" }, { - "$ref": "#/components/schemas/RouteConnectionXAIV1" + "$ref": "#/components/schemas/RouteConnectionConfigXAIV1" } ], "title": "Config" @@ -33510,33 +35471,6 @@ "title": "RouteConnectionV1", "type": "object" }, - "RouteConnectionXAIV1": { - "properties": { - "provider": { - "const": "XAI", - "description": "Provider kind for xAI.", - "title": "Provider", - "type": "string" - }, - "secret_id": { - "description": "Identifier of the team secret holding the provider API key.", - "title": "Secret Id", - "type": "string" - }, - "secret_name": { - "description": "Name of the team secret holding the provider API key.", - "title": "Secret Name", - "type": "string" - } - }, - "required": [ - "provider", - "secret_id", - "secret_name" - ], - "title": "RouteConnectionXAIV1", - "type": "object" - }, "RouteConnectionsResponseV1": { "properties": { "items": { @@ -33599,7 +35533,7 @@ "title": "RouteConnectionsRequestV1", "type": "object" }, - "RouteConnectionConfigAnthropicV1": { + "UpsertRouteConnectionConfigAnthropicV1": { "additionalProperties": false, "properties": { "provider": { @@ -33619,10 +35553,10 @@ "provider", "secret_id" ], - "title": "RouteConnectionConfigAnthropicV1", + "title": "UpsertRouteConnectionConfigAnthropicV1", "type": "object" }, - "RouteConnectionConfigOpenAIV1": { + "UpsertRouteConnectionConfigOpenAIV1": { "additionalProperties": false, "properties": { "provider": { @@ -33642,10 +35576,10 @@ "provider", "secret_id" ], - "title": "RouteConnectionConfigOpenAIV1", + "title": "UpsertRouteConnectionConfigOpenAIV1", "type": "object" }, - "RouteConnectionConfigXAIV1": { + "UpsertRouteConnectionConfigXAIV1": { "additionalProperties": false, "properties": { "provider": { @@ -33665,7 +35599,7 @@ "provider", "secret_id" ], - "title": "RouteConnectionConfigXAIV1", + "title": "UpsertRouteConnectionConfigXAIV1", "type": "object" }, "CreateRouteConnectionRequestV1": { @@ -33692,9 +35626,9 @@ "description": "Provider the connection authenticates with, and the team secret holding its API key.", "discriminator": { "mapping": { - "ANTHROPIC": "#/components/schemas/RouteConnectionConfigAnthropicV1", - "OPENAI": "#/components/schemas/RouteConnectionConfigOpenAIV1", - "XAI": "#/components/schemas/RouteConnectionConfigXAIV1" + "ANTHROPIC": "#/components/schemas/UpsertRouteConnectionConfigAnthropicV1", + "OPENAI": "#/components/schemas/UpsertRouteConnectionConfigOpenAIV1", + "XAI": "#/components/schemas/UpsertRouteConnectionConfigXAIV1" }, "propertyName": "provider" }, @@ -33706,13 +35640,13 @@ ], "oneOf": [ { - "$ref": "#/components/schemas/RouteConnectionConfigAnthropicV1" + "$ref": "#/components/schemas/UpsertRouteConnectionConfigAnthropicV1" }, { - "$ref": "#/components/schemas/RouteConnectionConfigOpenAIV1" + "$ref": "#/components/schemas/UpsertRouteConnectionConfigOpenAIV1" }, { - "$ref": "#/components/schemas/RouteConnectionConfigXAIV1" + "$ref": "#/components/schemas/UpsertRouteConnectionConfigXAIV1" } ], "title": "Config" @@ -33738,109 +35672,16 @@ "title": "RouteConnectionTombstoneV1", "type": "object" }, - "UpdateRouteConnectionConfigAnthropicV1": { - "additionalProperties": false, - "properties": { - "provider": { - "const": "ANTHROPIC", - "description": "Provider kind for Anthropic.", - "title": "Provider", - "type": "string" - }, - "secret_id": { - "anyOf": [ - { - "minLength": 1, - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Identifier of the new secret, owned by the connection's team, that holds the provider API key. Omit to keep the current secret.", - "title": "Secret Id", - "x-null-distinct": true - } - }, - "required": [ - "provider" - ], - "title": "UpdateRouteConnectionConfigAnthropicV1", - "type": "object" - }, - "UpdateRouteConnectionConfigOpenAIV1": { - "additionalProperties": false, - "properties": { - "provider": { - "const": "OPENAI", - "description": "Provider kind for OpenAI.", - "title": "Provider", - "type": "string" - }, - "secret_id": { - "anyOf": [ - { - "minLength": 1, - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Identifier of the new secret, owned by the connection's team, that holds the provider API key. Omit to keep the current secret.", - "title": "Secret Id", - "x-null-distinct": true - } - }, - "required": [ - "provider" - ], - "title": "UpdateRouteConnectionConfigOpenAIV1", - "type": "object" - }, - "UpdateRouteConnectionConfigXAIV1": { - "additionalProperties": false, - "properties": { - "provider": { - "const": "XAI", - "description": "Provider kind for xAI.", - "title": "Provider", - "type": "string" - }, - "secret_id": { - "anyOf": [ - { - "minLength": 1, - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Identifier of the new secret, owned by the connection's team, that holds the provider API key. Omit to keep the current secret.", - "title": "Secret Id", - "x-null-distinct": true - } - }, - "required": [ - "provider" - ], - "title": "UpdateRouteConnectionConfigXAIV1", - "type": "object" - }, "UpdateRouteConnectionRequestV1": { "additionalProperties": false, "properties": { "config": { - "description": "Connection fields to change. The provider must match the connection and is immutable.", + "description": "Complete new connection configuration, replacing the current one. The provider must match the connection and is immutable.", "discriminator": { "mapping": { - "ANTHROPIC": "#/components/schemas/UpdateRouteConnectionConfigAnthropicV1", - "OPENAI": "#/components/schemas/UpdateRouteConnectionConfigOpenAIV1", - "XAI": "#/components/schemas/UpdateRouteConnectionConfigXAIV1" + "ANTHROPIC": "#/components/schemas/UpsertRouteConnectionConfigAnthropicV1", + "OPENAI": "#/components/schemas/UpsertRouteConnectionConfigOpenAIV1", + "XAI": "#/components/schemas/UpsertRouteConnectionConfigXAIV1" }, "propertyName": "provider" }, @@ -33852,13 +35693,13 @@ ], "oneOf": [ { - "$ref": "#/components/schemas/UpdateRouteConnectionConfigAnthropicV1" + "$ref": "#/components/schemas/UpsertRouteConnectionConfigAnthropicV1" }, { - "$ref": "#/components/schemas/UpdateRouteConnectionConfigOpenAIV1" + "$ref": "#/components/schemas/UpsertRouteConnectionConfigOpenAIV1" }, { - "$ref": "#/components/schemas/UpdateRouteConnectionConfigXAIV1" + "$ref": "#/components/schemas/UpsertRouteConnectionConfigXAIV1" } ], "title": "Config" @@ -33934,29 +35775,21 @@ { "discriminator": { "mapping": { - "ANTHROPIC": "#/components/schemas/RouteTargetConfigAnthropicV1", - "BASETEN_MODEL_API": "#/components/schemas/RouteTargetConfigBasetenModelAPIV1", - "CLASSIFIER_MODEL_BASED": "#/components/schemas/RouteTargetConfigClassifierModelBasedV1", - "OPENAI": "#/components/schemas/RouteTargetConfigOpenAIV1", - "XAI": "#/components/schemas/RouteTargetConfigXAIV1" + "BASETEN_MODEL_API": "#/components/schemas/UpsertRouteTargetBasetenModelAPIV1", + "CLASSIFIER_MODEL_BASED": "#/components/schemas/UpsertRouteTargetClassifierModelBasedV1", + "CONNECTION": "#/components/schemas/UpsertRouteTargetConnectionV1" }, "propertyName": "type" }, "oneOf": [ { - "$ref": "#/components/schemas/RouteTargetConfigBasetenModelAPIV1" - }, - { - "$ref": "#/components/schemas/RouteTargetConfigAnthropicV1" - }, - { - "$ref": "#/components/schemas/RouteTargetConfigOpenAIV1" + "$ref": "#/components/schemas/UpsertRouteTargetBasetenModelAPIV1" }, { - "$ref": "#/components/schemas/RouteTargetConfigXAIV1" + "$ref": "#/components/schemas/UpsertRouteTargetConnectionV1" }, { - "$ref": "#/components/schemas/RouteTargetConfigClassifierModelBasedV1" + "$ref": "#/components/schemas/UpsertRouteTargetClassifierModelBasedV1" } ] }, @@ -35040,60 +36873,6 @@ "title": "RegisterAPIKeyResponseV1", "type": "object" }, - "GetSandboxConfigurationResponseV1": { - "type": "object", - "required": [ - "regions" - ], - "properties": { - "regions": { - "type": "array", - "description": "Available Carbon-compatible regions, sorted by name. Empty when none are available.", - "items": { - "$ref": "#/components/schemas/SandboxRegionV1" - } - } - } - }, - "SandboxRegionV1": { - "type": "object", - "required": [ - "name", - "country", - "continent", - "location", - "info_generation" - ], - "properties": { - "name": { - "type": "string", - "description": "Public region identifier to use when creating a sandbox.", - "example": "us-was-1" - }, - "country": { - "type": "string", - "description": "Country code.", - "example": "us" - }, - "continent": { - "type": "string", - "description": "Continent code.", - "example": "na" - }, - "location": { - "type": "string", - "description": "Region location.", - "example": "Washington" - }, - "info_generation": { - "type": "string", - "description": "Runtime generation supported by this region, using the public name CARBON. Actual runtime selection depends on the team and sandbox configuration.", - "enum": [ - "CARBON" - ] - } - } - }, "SandboxLogsV1": { "type": "object", "required": [ @@ -35165,98 +36944,200 @@ } } }, + "SandboxMetricModeV1": { + "type": "string", + "enum": [ + "CURRENT", + "SUMMARY", + "SERIES" + ] + }, + "SandboxMetricNameV1": { + "type": "string", + "enum": [ + "baseten_sandbox_requests_total", + "baseten_sandbox_cpu_usage_cores", + "baseten_sandbox_memory_bytes" + ] + }, "SandboxMetricsV1": { "type": "object", "required": [ - "sandbox_name", - "start_time", - "end_time", - "interval_seconds", - "data" + "start_epoch_millis", + "end_epoch_millis", + "mode", + "step_seconds", + "metric_descriptors", + "metric_values" ], "properties": { - "sandbox_name": { - "type": "string" + "start_epoch_millis": { + "type": "integer", + "format": "int64", + "description": "Returned window start, snapped outward in SERIES. Equal to end for CURRENT." }, - "start_time": { - "type": "string", - "format": "date-time" + "end_epoch_millis": { + "type": "integer", + "format": "int64", + "description": "Returned exclusive window end, snapped outward in SERIES. Snapshot timestamp for CURRENT." }, - "end_time": { - "type": "string", - "format": "date-time" + "mode": { + "$ref": "#/components/schemas/SandboxMetricModeV1" }, - "interval_seconds": { - "type": "integer" + "step_seconds": { + "description": "Seconds per SERIES interval; null in CURRENT and SUMMARY.", + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] }, - "data": { + "metric_descriptors": { + "type": "array", + "minItems": 1, + "maxItems": 3, + "items": { + "$ref": "#/components/schemas/SandboxMetricDescriptorV1" + } + }, + "metric_values": { "type": "array", "maxItems": 10000, "items": { - "$ref": "#/components/schemas/SandboxMetricsPointV1" + "$ref": "#/components/schemas/SandboxMetricValueSetV1" } } } }, - "SandboxMetricsPointV1": { + "SandboxMetricDescriptorV1": { "type": "object", "required": [ - "timestamp", - "requests", - "cpu_percent", - "memory_bytes", - "error_rate" + "name", + "unit_hint", + "kind", + "label_sets" ], "properties": { - "timestamp": { + "name": { + "$ref": "#/components/schemas/SandboxMetricNameV1" + }, + "unit_hint": { "type": "string", - "format": "date-time", - "description": "Start of this interval." + "enum": [ + "COUNT", + "CORES", + "BYTES" + ], + "description": "Requests use COUNT, CPU uses CORES (1 means one fully used core), and memory uses BYTES." }, - "requests": { - "description": "Request count. Zero without traffic; null for intervals entirely before creation.", - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } + "kind": { + "type": "string", + "enum": [ + "GAUGE", + "COUNTER" ] }, - "cpu_percent": { - "description": "Peak CPU usage in this interval, where 100 is one full CPU core. Null without a sample.", - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" + "label_sets": { + "type": "array", + "maxItems": 3, + "description": "Requests use status=2xx,4xx,5xx; CPU and memory use stat=avg,max.\n", + "items": { + "type": "object", + "additionalProperties": { + "type": "string" } - ] + } + } + } + }, + "SandboxMetricValueSetV1": { + "type": "object", + "required": [ + "start_epoch_millis", + "values" + ], + "properties": { + "start_epoch_millis": { + "type": "integer", + "format": "int64", + "description": "Start of the SERIES interval or SUMMARY window; snapshot timestamp in CURRENT." }, - "memory_bytes": { - "description": "Memory usage in bytes, averaged per series then maximum across series. Null without a sample.", - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" + "values": { + "type": "array", + "minItems": 1, + "maxItems": 3, + "description": "Values indexed first by metric_descriptors, then by that descriptor's label_sets. Null means no sample.", + "items": { + "type": "array", + "maxItems": 3, + "items": { + "anyOf": [ + { + "type": "number", + "format": "double" + }, + { + "type": "null" + } + ] } - ] + } + } + } + }, + "GetSandboxConfigurationResponseV1": { + "type": "object", + "required": [ + "regions" + ], + "properties": { + "regions": { + "type": "array", + "description": "Available Carbon-compatible regions, sorted by name. Empty when none are available.", + "items": { + "$ref": "#/components/schemas/SandboxRegionV1" + } + } + } + }, + "SandboxRegionV1": { + "type": "object", + "required": [ + "name", + "country", + "continent", + "location", + "info_generation" + ], + "properties": { + "name": { + "type": "string", + "description": "Public region identifier to use when creating a sandbox.", + "example": "us-was-1" }, - "error_rate": { - "description": "Fraction of requests returning 4xx or 5xx. Null when there are no requests.", - "anyOf": [ - { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - { - "type": "null" - } + "country": { + "type": "string", + "description": "Country code.", + "example": "us" + }, + "continent": { + "type": "string", + "description": "Continent code.", + "example": "na" + }, + "location": { + "type": "string", + "description": "Region location.", + "example": "Washington" + }, + "info_generation": { + "type": "string", + "description": "Runtime generation supported by this region, using the public name CARBON. Actual runtime selection depends on the team and sandbox configuration.", + "enum": [ + "CARBON" ] } } @@ -35718,7 +37599,7 @@ "properties": { "name": { "type": "string", - "description": "Name of the environment variable", + "description": "Name of the environment variable. Must start with a letter or underscore and contain only letters, digits and underscores (^[A-Za-z_][A-Za-z0-9_]*$); create and update reject other names.", "example": "NODE_ENV" }, "secret": { @@ -35907,9 +37788,10 @@ }, "memory": { "type": "integer", - "description": "Memory allocation in megabytes. Also determines CPU allocation (CPU cores = memory in MB / 2048, e.g., 4096MB = 2 CPUs).", + "description": "Memory allocation in megabytes. Also determines CPU allocation (CPU cores = memory in MB / 2048, e.g., 4096MB = 2 CPUs). Must be between 1024 and 262144.", "example": 4096, - "minimum": 1 + "minimum": 512, + "maximum": 262144 }, "ports": { "description": "Set of ports for a resource", @@ -36148,10 +38030,11 @@ }, "memory": { "type": "integer", - "description": "Memory allocation in megabytes. Also determines CPU allocation (CPU cores = memory in MB / 2048, e.g., 4096MB = 2 CPUs). Defaults to 4096.", + "description": "Memory allocation in megabytes. Also determines CPU allocation (CPU cores = memory in MB / 2048, e.g., 4096MB = 2 CPUs). Defaults to 4096. Must be between 1024 and 262144.", "default": 4096, "example": 4096, - "minimum": 1 + "minimum": 512, + "maximum": 262144 }, "ports": { "description": "Set of ports for a resource", @@ -36845,7 +38728,7 @@ "type": "array", "description": "Image repositories on this page.", "items": { - "$ref": "#/components/schemas/SandboxImageSummaryV1" + "$ref": "#/components/schemas/SandboxImageV1" } }, "pagination": { @@ -36930,7 +38813,7 @@ "size": 134217728 } }, - "SandboxImageSummaryV1": { + "SandboxImageV1": { "type": "object", "description": "Sandbox image repository summary. Fetch tags through GET /sandboxes/images/{image_name}/tags.", "properties": { @@ -37000,40 +38883,6 @@ "tag_count": 2 } }, - "SandboxImageV1": { - "description": "Sandbox image repository. Get operations return a summary with an empty tags array.", - "allOf": [ - { - "$ref": "#/components/schemas/SandboxImageSummaryV1" - }, - { - "type": "object", - "properties": { - "tags": { - "type": "array", - "description": "Empty for get summary responses. Use GET /sandboxes/images/{image_name}/tags to retrieve paginated image versions.", - "items": { - "$ref": "#/components/schemas/SandboxImageTagV1" - }, - "example": [] - } - }, - "required": [ - "tags" - ] - } - ], - "example": { - "name": "base-image", - "status": "BUILT", - "created_at": "2026-09-15T21:20:00Z", - "updated_at": "2026-09-16T21:25:00Z", - "last_deployed_at": "2026-09-16T21:26:58.545765901Z", - "size": 260046848, - "tag_count": 2, - "tags": [] - } - }, "PushSandboxImageRequestV1": { "type": "object", "description": "Push a sandbox image from a source archive or an existing registry image.", @@ -37114,7 +38963,7 @@ }, "image": { "type": "string", - "description": "Registered image reference including its tag, when available. Tags are assigned by the service; GET /sandboxes/images/{image_name} returns the available tags once processing completes.", + "description": "Registered image reference including its tag, when available. Tags are assigned by the service; GET /sandboxes/images/{image_name}/tags returns the available tags once processing completes.", "example": "b10/base-image:latest" } }, @@ -37348,6 +39197,14 @@ "in": "path", "required": true }, + "volume_tag": { + "schema": { + "type": "string" + }, + "name": "volume_tag", + "in": "path", + "required": true + }, "secret_name": { "schema": { "type": "string" @@ -37540,6 +39397,50 @@ "in": "path", "required": true }, + "SandboxMetricMode": { + "name": "mode", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SandboxMetricModeV1" + }, + "description": "Defaults to CURRENT, which rejects start_epoch_millis and end_epoch_millis. SERIES uses a server-selected step of 30 seconds up to 2 hours, 60 seconds up to 1 day, 600 seconds up to 3 days, and 3600 seconds beyond 3 days." + }, + "SandboxMetricStart": { + "name": "start_epoch_millis", + "in": "query", + "schema": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "description": "Unix milliseconds. Defaults to one hour before end; SERIES snaps outward." + }, + "SandboxMetricEnd": { + "name": "end_epoch_millis", + "in": "query", + "schema": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "description": "Exclusive end in Unix milliseconds, default now. Maximum aligned window is 7 days." + }, + "SandboxMetricNames": { + "name": "metrics", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "minItems": 1, + "maxItems": 3, + "uniqueItems": true, + "items": { + "$ref": "#/components/schemas/SandboxMetricNameV1" + } + }, + "description": "Metric names in response order. Omit to request all three signals." + }, "TeamId": { "name": "team_id", "in": "query", @@ -38352,28 +40253,14 @@ } }, "ImageReady": { - "summary": "Built image with all metadata and tags", + "summary": "Built image repository summary", "value": { "name": "base-image", "status": "BUILT", "created_at": "2026-09-15T21:20:00Z", "updated_at": "2026-09-16T21:25:00Z", "last_deployed_at": "2026-09-16T21:26:58.545765901Z", - "size": 260046848, - "tags": [ - { - "name": "latest", - "created_at": "2026-09-16T21:20:00Z", - "updated_at": "2026-09-16T21:25:00Z", - "size": 134217728 - }, - { - "name": "20260915212000", - "created_at": "2026-09-15T21:20:00Z", - "updated_at": "2026-09-15T21:25:00Z", - "size": 125829120 - } - ] + "size": 260046848 }, "description": "Dates and byte sizes are representative example data. Image references use the b10 namespace." }, @@ -38385,15 +40272,7 @@ "created_at": "2026-09-15T21:20:00Z", "updated_at": "2026-09-16T21:41:13Z", "last_deployed_at": "2026-09-16T21:26:58.545765901Z", - "size": 134217728, - "tags": [ - { - "name": "latest", - "created_at": "2026-09-16T21:20:00Z", - "updated_at": "2026-09-16T21:25:00Z", - "size": 134217728 - } - ] + "size": 134217728 } }, "ImageSummary": { @@ -38403,8 +40282,7 @@ "status": "BUILT", "created_at": "2026-09-15T21:20:00Z", "size": 260046848, - "tag_count": 2, - "tags": [] + "tag_count": 2 } }, "ImagesList": { diff --git a/tests/sandbox/test_images.py b/tests/sandbox/test_images.py index a28cb5a..6151afa 100644 --- a/tests/sandbox/test_images.py +++ b/tests/sandbox/test_images.py @@ -122,7 +122,6 @@ def _image(status: str = "BUILT", **fields: Any) -> dict[str, Any]: "created_at": "2026-10-07T12:00:00Z", "size": 1024, "tag_count": 2, - "tags": [], **fields, } diff --git a/tests/sandbox/test_parity.py b/tests/sandbox/test_parity.py index 1357c05..8fad135 100644 --- a/tests/sandbox/test_parity.py +++ b/tests/sandbox/test_parity.py @@ -243,12 +243,6 @@ def _kwargs(method: Callable[..., Any]) -> set[str]: ours=_dataclass_fields(ImageInfo), generated=baseten.client.managementapi.SandboxImage, renamed={"size": "size_bytes"}, - generated_only={"tags": "always empty; list_tags lists them"}, - ), - "ImageInfo/Summary": _Parity( - ours=_dataclass_fields(ImageInfo), - generated=baseten.client.managementapi.SandboxImageSummary, - renamed={"size": "size_bytes"}, ), "ImageTagInfo": _Parity( ours=_dataclass_fields(ImageTagInfo),