Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 54 additions & 2 deletions docs/api-reference/models-benchmarks.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ Returns an `Optional[Model]` - a single `Model` object if found, or `None` if th

### `add(*model_ids, timeout=None)`

Adds public models to the project by their IDs.
Adds models (public or custom) to the project by their IDs.

#### Parameters

Expand All @@ -123,7 +123,7 @@ success = client.models.add("model-id-1", "model-id-2")

### `remove(*model_ids, timeout=None)`

Removes models from the project by their IDs.
Removes models (public or custom) from the project's model list. The underlying records are not deleted — use `delete_custom` to fully tear down a custom model.

#### Parameters

Expand Down Expand Up @@ -187,6 +187,58 @@ if result:
print(f"Created model: {result.model_id}")
```

### `update_custom(model_id, *, api_url=None, api_key=None, max_tokens=None, timeout=None)`

Updates a custom model's mutable fields. At least one of `api_url`, `api_key`, or `max_tokens` must be provided. Primary use case: repointing `api_url` for ephemeral vLLM endpoints behind cloudflared tunnels whose URL changes between sessions.

#### Parameters

| Parameter | Type | Required | Description |
| ------------ | -------------------------------- | -------- | -------------------------------------------------------- |
| `model_id` | `str` | Yes | ID of the custom model to update |
| `api_url` | `str \| None` | No | New base URL for the OpenAI-compatible API endpoint |
| `api_key` | `str \| None` | No | New API key for the model provider |
| `max_tokens` | `int \| None` | No | New maximum tokens value |
| `timeout` | `float \| httpx.Timeout \| None` | No | Override request timeout |

#### Returns

Returns `bool` — `True` on success, `False` otherwise.

#### Example

```python
client = Stratix()

# Repoint the api_url without re-creating the model
client.models.update_custom(
"model-id-from-create-custom",
api_url="https://my-new-endpoint.example.com/v1",
)
```

### `delete_custom(model_id, *, timeout=None)`

Disables a custom model and removes it from `Project.Models`. The backend tears down the model's S3 yaml artifacts and AWS secret, and marks the record as disabled (preserving any evaluation references). Public models cannot be deleted via the SDK.

#### Parameters

| Parameter | Type | Required | Description |
| ---------- | -------------------------------- | -------- | --------------------------------- |
| `model_id` | `str` | Yes | ID of the custom model to delete |
| `timeout` | `float \| httpx.Timeout \| None` | No | Override request timeout |

#### Returns

Returns `bool` — `True` on success, `False` otherwise.

#### Example

```python
client = Stratix()
client.models.delete_custom("model-id-from-create-custom")
```

## Benchmarks

### `get(type=None, name=None, key=None, categories=None, languages=None, timeout=None)`
Expand Down
63 changes: 63 additions & 0 deletions docs/examples/models-and-benchmarks.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,69 @@ def main():
print(f" - {m.name} (id={m.id}, key={m.key})")


if __name__ == "__main__":
main()
```

## Repointing a Custom Model's `api_url`

Use this when your model's endpoint URL changes — for example, when serving a vLLM instance behind a cloudflared tunnel that rotates its hostname between sessions.

```python
from layerlens import Stratix


def main():
client = Stratix()

result = client.models.create_custom(
name="My Tunnel-backed Model",
key="my-org/tunnel-model-v1",
description="vLLM served behind a cloudflared tunnel",
api_url="https://tunnel-1.example.com/v1",
api_key="my-provider-api-key",
max_tokens=4096,
)
assert result is not None

# Later, when the tunnel URL changes:
client.models.update_custom(
result.model_id,
api_url="https://tunnel-2.example.com/v1",
)

# Run evaluations as usual — the model now points at the new endpoint.


if __name__ == "__main__":
main()
```

## Replacing a Custom Model

`delete_custom` releases the model's name so it can be reused. This is useful for replacing a misconfigured model without picking a new name.

```python
from layerlens import Stratix


def main():
client = Stratix()

# Tear down the old version
client.models.delete_custom("old-model-id")

# Recreate with the same name (now free)
client.models.create_custom(
name="My Custom Model",
key="my-org/custom-model-v2",
description="Replacement after schema migration",
api_url="https://my-endpoint.example.com/v1",
api_key="my-provider-api-key",
max_tokens=4096,
)


if __name__ == "__main__":
main()
```
Expand Down
26 changes: 26 additions & 0 deletions samples/core/custom_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,32 @@ def main() -> None:
else:
print("\nNo custom models found in project")

if not result:
return

# ── Update mutable fields (e.g. repoint api_url) ──────────────────
#
# Use this when your endpoint URL changes -- common for vLLM
# instances served behind cloudflared tunnels whose hostname
# rotates between sessions.

updated = client.models.update_custom(
result.model_id,
api_url="https://my-new-endpoint.example.com/v1",
)
if updated:
print(f"\nCustom model {result.model_id} api_url updated")

# ── Full teardown ─────────────────────────────────────────────────
#
# ``delete_custom`` disables the model, removes it from
# ``Project.Models``, and releases its name for reuse. Evaluation
# references to the disabled record are preserved.

deleted = client.models.delete_custom(result.model_id)
if deleted:
print(f"Custom model {result.model_id} deleted")


if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion src/layerlens/_version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "1.6.0"
__version__ = "1.6.1"

# Will be templated during the build
__git_commit__ = "__GIT_COMMIT__"
133 changes: 117 additions & 16 deletions src/layerlens/resources/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,10 +169,10 @@ def add(
*model_ids: str,
timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT,
) -> bool:
"""Add models to the project by their IDs."""
# Only fetch public (platform) models — custom models are managed
# separately and must not be included in the project patch payload.
current = self.get(timeout=timeout, type="public") or []
"""Add models (public or custom) to the project by their IDs."""
# Fetch the full current list (public + custom). The project's
# PATCH endpoint expects the complete model set in a single payload.
current = self.get(timeout=timeout) or []
current_ids = [str(m.id) for m in current]
new_ids = list(dict.fromkeys(current_ids + list(model_ids)))
return self._patch_project_models(new_ids, timeout)
Expand All @@ -182,10 +182,13 @@ def remove(
*model_ids: str,
timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT,
) -> bool:
"""Remove models from the project by their IDs."""
# Only fetch public (platform) models — custom models are managed
# separately and must not be included in the project patch payload.
current = self.get(timeout=timeout, type="public") or []
"""Remove models (public or custom) from the project's model list.

Note: this only detaches the models from the project. The underlying
records are not deleted — use ``delete_custom`` to fully tear down a
custom model.
"""
current = self.get(timeout=timeout) or []
remove_set = set(model_ids)
new_ids = [str(m.id) for m in current if str(m.id) not in remove_set]
return self._patch_project_models(new_ids, timeout)
Expand Down Expand Up @@ -256,6 +259,61 @@ def create_custom(
return CreateModelResponse(**resp)
return None

def update_custom(
self,
model_id: str,
*,
api_url: Optional[str] = None,
api_key: Optional[str] = None,
max_tokens: Optional[int] = None,
timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT,
) -> bool:
"""Update a custom model's mutable fields.

At least one of ``api_url``, ``api_key``, or ``max_tokens`` must be
provided. Returns ``True`` on success.

Primary use case: repointing ``api_url`` for ephemeral vLLM endpoints
behind cloudflared tunnels whose URL changes between sessions.

Args:
model_id: ID of the custom model to update.
api_url: New base URL for the OpenAI-compatible API endpoint.
api_key: New API key for the model provider.
max_tokens: New maximum tokens value.
timeout: Request timeout override.
"""
url = (
f"/organizations/{self._client.organization_id}/projects/{self._client.project_id}/custom-models/{model_id}"
)
body: Dict[str, Any] = {}
if api_url is not None:
body["api_url"] = api_url
if api_key is not None:
body["api_key"] = api_key
if max_tokens is not None:
body["max_tokens"] = max_tokens
resp = self._patch(url, body=body, timeout=timeout, cast_to=dict)
return isinstance(resp, dict) and "data" in resp

def delete_custom(
self,
model_id: str,
*,
timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT,
) -> bool:
"""Disable a custom model and detach it from the project.

The backend tears down the model's S3 yaml artifacts and the AWS
secret, marks the record as disabled (preserving evaluation
references), and removes the model ID from ``Project.Models``.
"""
url = (
f"/organizations/{self._client.organization_id}/projects/{self._client.project_id}/custom-models/{model_id}"
)
resp = self._delete(url, timeout=timeout, cast_to=dict)
return isinstance(resp, dict) and "data" in resp


class AsyncModels(AsyncAPIResource):
async def get(
Expand Down Expand Up @@ -368,10 +426,10 @@ async def add(
*model_ids: str,
timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT,
) -> bool:
"""Add models to the project by their IDs."""
# Only fetch public (platform) models — custom models are managed
# separately and must not be included in the project patch payload.
current = await self.get(timeout=timeout, type="public") or []
"""Add models (public or custom) to the project by their IDs."""
# Fetch the full current list (public + custom). The project's
# PATCH endpoint expects the complete model set in a single payload.
current = await self.get(timeout=timeout) or []
current_ids = [str(m.id) for m in current]
new_ids = list(dict.fromkeys(current_ids + list(model_ids)))
return await self._patch_project_models(new_ids, timeout)
Expand All @@ -381,10 +439,13 @@ async def remove(
*model_ids: str,
timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT,
) -> bool:
"""Remove models from the project by their IDs."""
# Only fetch public (platform) models — custom models are managed
# separately and must not be included in the project patch payload.
current = await self.get(timeout=timeout, type="public") or []
"""Remove models (public or custom) from the project's model list.

Note: this only detaches the models from the project. The underlying
records are not deleted — use ``delete_custom`` to fully tear down a
custom model.
"""
current = await self.get(timeout=timeout) or []
remove_set = set(model_ids)
new_ids = [str(m.id) for m in current if str(m.id) not in remove_set]
return await self._patch_project_models(new_ids, timeout)
Expand Down Expand Up @@ -454,3 +515,43 @@ async def create_custom(
if isinstance(resp, dict) and "model_id" in resp:
return CreateModelResponse(**resp)
return None

async def update_custom(
self,
model_id: str,
*,
api_url: Optional[str] = None,
api_key: Optional[str] = None,
max_tokens: Optional[int] = None,
timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT,
) -> bool:
"""Update a custom model's mutable fields.

At least one of ``api_url``, ``api_key``, or ``max_tokens`` must be
provided. Returns ``True`` on success.
"""
url = (
f"/organizations/{self._client.organization_id}/projects/{self._client.project_id}/custom-models/{model_id}"
)
body: Dict[str, Any] = {}
if api_url is not None:
body["api_url"] = api_url
if api_key is not None:
body["api_key"] = api_key
if max_tokens is not None:
body["max_tokens"] = max_tokens
resp = await self._patch(url, body=body, timeout=timeout, cast_to=dict)
return isinstance(resp, dict) and "data" in resp

async def delete_custom(
self,
model_id: str,
*,
timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT,
) -> bool:
"""Disable a custom model and detach it from the project."""
url = (
f"/organizations/{self._client.organization_id}/projects/{self._client.project_id}/custom-models/{model_id}"
)
resp = await self._delete(url, timeout=timeout, cast_to=dict)
return isinstance(resp, dict) and "data" in resp
Loading
Loading