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
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
# Changelog

## [1.6.0] - 2026-10-04

### Changed

- Sync the bundled OpenAPI spec, adding 10 paths and 18 schemas. The typed models gain the new response fields, including `attachments` on a run and on each step
- Move `LATEST_API_VERSION` to `2026-10-03`

### Added

- Add `list_cloud_drives()`, `get_cloud_drive()`, `update_cloud_drive()`, `disconnect_cloud_drive()`, `delete_cloud_drive()`, `list_cloud_drive_providers()`, `get_agents_using_cloud_drive()` and `list_cloud_drive_rejections()` for cloud-drive connections. The list methods return the items on either response shape
- Add `list_source_contents()` and `get_source_content_status()` to read the indexing status of a source's content, keyed by the `content_version_id` the upload methods return
- Add `list_embedding_models()` and `list_reranker_models()`, with the pricing and defaults that sit beside each list
- Add `ApiVersion` members for `2026-08-03`, `2026-08-21`, `2026-09-28`, `2026-09-30` and `2026-10-03`, so each can be selected without `allow_unknown_api_version`
- Add a `param_style` argument to `paginate()` for endpoints that page by `offset` rather than `page`

### Fixed

- Read the body of an error response in the streaming methods. A 422 from `run_streaming_agent_and_wait()` escaped as `httpx.ResponseNotRead`, a 422 from `run_streaming_agent()` lost its field-level detail, and every streaming error had an empty `response_text`
- Raise `SeclaiAPIStatusError` on a 422 whose `detail` is a plain string, from every method including the typed ones and the uploads. Decoding it as field-level validation raised `ValueError` from inside the generated models
- Apply the unknown-version guard to a `Seclai-Version` passed in a per-request `headers` argument, on `request()` and the streaming methods. Any value was sent
- Replace a header case-insensitively when a per-request `headers` argument or the auth layer supplies one the client already set. Both spellings were sent
- Yield the items from `paginate()` when the endpoint answers with a bare array, or under `data` while a per-resource `items_key` was given. It yielded nothing in both cases, and now raises `SeclaiError` on a shape it cannot read
- Copy `default_headers` at construction. Mutating the mapping afterwards put an unvalidated `Seclai-Version` on the wire
- Reject an empty `Seclai-Version` in `default_headers`. It was read as absent, sent anyway, and suppressed `api_version`

## [1.5.0] - 2026-07-27

### Changed
Expand Down Expand Up @@ -158,6 +183,7 @@ _Stable release. Packaging, CI, and documentation deployment only; no API change

_Initial release._

[1.6.0]: https://github.com/seclai/seclai-python/releases/tag/1.6.0
[1.5.0]: https://github.com/seclai/seclai-python/releases/tag/1.5.0
[1.4.0]: https://github.com/seclai/seclai-python/releases/tag/1.4.0
[1.3.0]: https://github.com/seclai/seclai-python/releases/tag/1.3.0
Expand Down
76 changes: 65 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,9 @@ reshape responses, and this client would decode them incorrectly rather than
reject them. Upgrade the package to adopt a new version, or pass
`allow_unknown_api_version=True` if you have to move first and accept that risk.

The guard only covers the header. An account pinned server-side can still be
The guard covers the header however it is supplied — `api_version`,
`default_headers`, or a per-request `headers` argument — and nothing else. An
account pinned server-side can still be
newer than this release — `get_api_version()` reports the `effective_version` the
request resolved to, and comparing it against `LATEST_API_VERSION` is how you
detect the gap.
Expand All @@ -184,8 +186,6 @@ of being ignored, and list endpoints move to the canonical
`{"data": [...], "pagination": {...}}` envelope. The affected methods read both
shapes, so they keep working either way — but the metadata moves:

| Method | Before | From 2026-07-27 |
| --- | --- | --- |
| Method | Before | From 2026-07-27 | Legacy paging |
| --- | --- | --- | --- |
| `list_evaluation_criteria_page()` | bare list | `data` + `pagination` | none — returns everything |
Expand All @@ -210,8 +210,25 @@ then removed once the canonical envelope is the default.

The two evaluation endpoints are **unpaginated** on the legacy shape — they
ignore `page`/`limit` and return everything — so a paginate-until-empty loop over
them only terminates once you have opted in. The other four paginate on either
shape.
them only terminates once you have opted in. `get_generation_tiers()` takes no
paging arguments at all and always returns the full set. The remaining three
paginate on either shape.

**Later versions.** Each is cumulative, and none changes a response shape this
client decodes:

| Version | What it changes |
| --- | --- |
| `2026-08-03` | `create_memory_bank()` and `update_memory_bank()` reject a non-zero `max_age_days` with a 400, and an omitted `retention_days` on create resolves per bank type |
| `2026-08-21` | `create_source()` rejects an embedding dimension its embedder does not support with a 400 — `list_embedding_models()` reports the supported ones |
| `2026-09-28` | Agent-definition writes use the current file-list grammar: an omitted `attachments` keeps the stored list and `[]` means no files |
| `2026-09-30` | A run's and a step's `output`, and a step's `input`, are the text rather than a JSON manifest; files are in `attachments` on every version |
| `2026-10-03` | A new LLM step written without `attachments` takes its parent's files, and a new retrieval step's matched media are its files |

The cloud-drive listings and `list_embedding_models()` / `list_reranker_models()`
follow the `2026-07-27` envelope rule as well. The cloud-drive methods return
the items on either shape; read the two model listings with
`unwrap_items(result, "models")`.

## Resources

Expand Down Expand Up @@ -282,9 +299,9 @@ search = client.search_agent_runs({"query": "test"})
# Fetch run details (optionally with step outputs)
detail = client.get_agent_run("run_id", include_step_outputs=True)

# Cancel or delete
# Cancel an in-flight or queued run. `delete_agent_run()` is the same
# operation on the same endpoint, returning a typed model instead of a dict.
client.cancel_agent_run("run_id")
client.delete_agent_run("run_id")
```

### Streaming
Expand Down Expand Up @@ -366,8 +383,8 @@ with response:
steps = client.generate_agent_steps("agent_id", {"user_input": "Build a RAG pipeline"})
config = client.generate_step_config("agent_id", {"step_type": "llm", "user_input": "..."})

# Conversation history
history = client.get_agent_ai_conversation_history("agent_id")
# Conversation history — step_type is required by the API
history = client.get_agent_ai_conversation_history("agent_id", step_type="llm")
client.mark_agent_ai_suggestion("agent_id", "conversation_id", {"accepted": True})
```

Expand Down Expand Up @@ -448,6 +465,33 @@ client.update_source("source_id", {"name": "Updated"})
client.delete_source("source_id")
```

Indexing status of a source's content, keyed by the `content_version_id` the
upload methods return:

```python
failed = client.list_source_contents("source_id", status="failed")
batch = client.list_source_contents(
"source_id", content_version_ids=["cv_1", "cv_2"]
)
one = client.get_source_content_status("source_id", "cv_1")
```

### Cloud drives

```python
providers = client.list_cloud_drive_providers()
drives = client.list_cloud_drives()
drive = client.get_cloud_drive("connection_id")
client.update_cloud_drive("connection_id", {"name": "Contracts"})

# Which agents depend on it, and which files it skipped and why
agents = client.get_agents_using_cloud_drive("connection_id")
skipped = client.list_cloud_drive_rejections("connection_id", limit=20)

client.disconnect_cloud_drive("connection_id") # keeps the connection
client.delete_cloud_drive("connection_id")
```

### File uploads

Upload a file to a source (max 200 MiB):
Expand Down Expand Up @@ -657,6 +701,10 @@ print(removed.get("cleanup_note")) # set when the domain was Seclai-managed
# Media-generation quality tiers (fast/balanced/thorough) and what each resolves to
tiers = client.get_generation_tiers()

# Embedding and reranker models, with their pricing
embedders = unwrap_items(client.list_embedding_models(), "models")
rerankers = unwrap_items(client.list_reranker_models(), "models")

alerts = client.list_model_alerts()
client.mark_model_alert_read("alert_id")
client.mark_all_model_alerts_read()
Expand Down Expand Up @@ -719,8 +767,14 @@ All list methods accept `page` and `limit` parameters. For auto-pagination acros
for agent in client.paginate("GET", "/agents"):
print(agent["name"])

# With a custom items key
for alert in client.paginate("GET", "/alerts", items_key="items"):
# With a per-resource items key; `data` is read first, so this works on
# either response shape
for config in client.paginate("GET", "/alerts/configs", items_key="configs"):
print(config["id"])

# Endpoints that declare `offset` rather than `page`
for alert in client.paginate("GET", "/models/alerts", items_key="alerts",
param_style="offset"):
print(alert["id"])
```

Expand Down
Loading
Loading