mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
Add per-alias model concurrency admission (#990)
* feat(models): add per-alias concurrency admission Add registry-backed FIFO admission limits with queue-aware deadlines and full-stream leases. Expose max_concurrency through storage, admin configuration, OpenAPI, documentation, and diagrams, with role and live backend count coverage. * fix(api): omit null concurrency schema default Keep max_concurrency optional for presence-keyed updates without advertising a null default for its non-null integer OpenAPI shape.
This commit is contained in:
+18
-2
@@ -920,6 +920,7 @@ configurations so workstreams can use different LLM backends.
|
||||
[models.local]
|
||||
base_url = "http://localhost:8000/v1"
|
||||
model = "qwen3-32b"
|
||||
max_concurrency = 1
|
||||
# provider defaults to "openai"
|
||||
|
||||
[models.claude]
|
||||
@@ -958,6 +959,20 @@ binding on mismatch. Failed reload validation changes neither maps nor
|
||||
generation. If only non-transport fields changed, compatible client pools can
|
||||
remain warm, but the session still receives a new coherent config/lane.
|
||||
|
||||
**Per-alias admission:** Each registry alias owns a stable, hot-resizable
|
||||
`ModelAdmission`. `max_concurrency = 0` is unlimited; a positive value limits
|
||||
simultaneous generations for that alias in one process. Every registry-backed
|
||||
role carries the same gate on its `ModelLane`, so main turns, judges, task
|
||||
agents, perception, compaction, and background generation coordinate through
|
||||
one FIFO. Two aliases never share a gate implicitly, even when their URLs are
|
||||
identical. `model_turn()` materializes attachment fallbacks before admission,
|
||||
then holds one lease across eager stream creation and the complete drain,
|
||||
releasing before retry backoff. Admission wait is credited out of deadline
|
||||
accounting, preventing queued judges from spending their request budget before
|
||||
dispatch. The gate survives cap-only reloads in place; the field is excluded
|
||||
from semantic `ModelConfig` equality so a capacity edit does not reset judges
|
||||
or output-guard state.
|
||||
|
||||
Primary loops, recursive compaction, judges, title generation, audio, and task
|
||||
agents all consume `ModelLane` rather than inspecting provider/client handles.
|
||||
Fallback is a lane change, so retry classification and result provenance come
|
||||
@@ -1196,8 +1211,9 @@ Verified quirks of vLLM's Anthropic endpoint:
|
||||
|
||||
**Database model definitions:** On server entry points, models can also be
|
||||
defined in the `model_definitions` table (admin Models tab). DB models support
|
||||
the same per-model sampling overrides. Config.toml models override DB models
|
||||
with the same alias in-memory (the DB rows are never modified).
|
||||
the same per-model sampling overrides and per-alias `max_concurrency`.
|
||||
Config.toml models override DB models with the same alias in-memory (the DB
|
||||
rows are never modified).
|
||||
|
||||
**Lifecycle:**
|
||||
1. `load_model_registry()` loads DB model definitions (if storage available),
|
||||
|
||||
+4
-1
@@ -455,7 +455,10 @@ configuration editor.
|
||||
The **Channels** tab links users to either a Discord or Slack account
|
||||
via a per-row channel-type selector. The **Models** tab is a CRUD
|
||||
editor for `model_definitions`, including static and dynamic backend-auth
|
||||
modes. Model edits rebind existing workstreams at their next send while
|
||||
modes and a per-process **Max concurrent generations** limit for each alias
|
||||
(`0` means unlimited). The limit is shared by every model-backed role using
|
||||
that alias and a streaming generation holds its slot through the full decode.
|
||||
Model edits rebind existing workstreams at their next send while
|
||||
in-flight requests keep their original definition snapshot; see
|
||||
[Settings](settings.md#model-definition-reloads) for the full contract. The **Nodes** tab edits per-node
|
||||
metadata, and the **TLS** tab manages CA and leaf certificates for the
|
||||
|
||||
@@ -134,6 +134,7 @@ class "ModelLane" as ModelLane <<frozen>> {
|
||||
+ capabilities: ModelCapabilities
|
||||
+ extra_params: dict | None
|
||||
+ registry: ModelRegistry | None
|
||||
+ admission: ModelAdmission | None
|
||||
+ backend_auth_config: ModelConfig | None
|
||||
+ backend_auth_resolver: Callable | None
|
||||
}
|
||||
@@ -357,12 +358,13 @@ class "ModelRegistry" as ModelReg {
|
||||
- _models: dict[str, ModelConfig]
|
||||
- _clients: dict[str, Any]
|
||||
- _providers: dict[str, LLMProvider]
|
||||
- _admissions: dict[str, ModelAdmission]
|
||||
- _client_lock: Lock
|
||||
+ default: str
|
||||
+ fallback: list[str]
|
||||
+ agent_model: str | None
|
||||
--
|
||||
+ resolve_binding(alias) → (client, model, config, provider, generation)
|
||||
+ resolve_binding(alias) → (client, model, config, provider, admission, generation)
|
||||
+ get_client(alias) → Any
|
||||
+ get_provider(alias) → LLMProvider
|
||||
+ has_alias(alias) → bool
|
||||
@@ -376,6 +378,22 @@ class "ModelRegistry" as ModelReg {
|
||||
core/model_registry.py
|
||||
}
|
||||
|
||||
class "ModelAdmission" as ModelAdmission {
|
||||
- alias: str
|
||||
- _limit: int
|
||||
- _in_flight: int
|
||||
- _waiters: deque
|
||||
+ acquire(cancel_ref) → AdmissionLease
|
||||
+ set_limit(limit)
|
||||
+ snapshot() → AdmissionSnapshot
|
||||
--
|
||||
Per-process FIFO generation gate.
|
||||
Stable across alias hot reloads;
|
||||
queue time is deadline credit.
|
||||
--
|
||||
core/admission.py
|
||||
}
|
||||
|
||||
class "ModelConfig" as ModelCfg <<frozen>> {
|
||||
+ alias: str
|
||||
+ provider: str
|
||||
@@ -385,6 +403,7 @@ class "ModelConfig" as ModelCfg <<frozen>> {
|
||||
+ temperature: float | None
|
||||
+ max_tokens: int | None
|
||||
+ reasoning_effort: str | None
|
||||
+ max_concurrency: int
|
||||
+ auth_mode: str
|
||||
+ obo_audience: str
|
||||
+ obo_scopes: str
|
||||
@@ -475,12 +494,14 @@ KindAdapter ..> ChatSession : constructs
|
||||
|
||||
ModelReg --> "*" ModelCfg : holds
|
||||
ModelReg --> "*" LLMProvider : caches
|
||||
ModelReg --> "*" ModelAdmission : owns per alias
|
||||
LLMProvider --> ModelCaps : returns
|
||||
ModelReg --> ResolvedBinding : resolves atomically
|
||||
ResolvedBinding --> ModelLane
|
||||
ModelLane --> LLMProvider
|
||||
ModelLane --> ModelCaps
|
||||
ModelLane --> ModelCfg : auth/config snapshot
|
||||
ModelLane --> ModelAdmission : admission lease
|
||||
ModelTurnFn --> ModelLane
|
||||
ModelTurnFn --> ModelTurnResult
|
||||
ModelTurnFn ..> BackendAuth : per-call resolver
|
||||
|
||||
@@ -10,6 +10,7 @@ participant "SessionManager" as Manager
|
||||
participant "ChatSession" as Session
|
||||
participant "SessionUIBase" as UI
|
||||
participant "model_turn()\n+ lowering" as Plant
|
||||
participant "ModelAdmission\n(per alias)" as Admission
|
||||
participant "LLM provider" as Provider
|
||||
participant "Tool workers" as Tools
|
||||
database "StorageBackend\n(SQLite / PostgreSQL)" as Storage
|
||||
@@ -54,6 +55,9 @@ loop until final answer and no queued input
|
||||
Session -> Plant : model_turn(active ModelLane, Turns,\n tools, cancel_ref, on_chunk)
|
||||
activate Plant
|
||||
Plant -> Plant : canonical Turns → provider wire\nrestore ids + repair + lane-specific fold
|
||||
Plant -> Plant : materialize attachment refs\n(nested perception before outer slot)
|
||||
Plant -> Admission : acquire(cancel_ref)
|
||||
activate Admission
|
||||
Plant -> Plant : resolve per-call backend credential\nfrom lane's pinned ModelConfig
|
||||
Plant -> Provider : create_streaming(...)
|
||||
activate Provider
|
||||
@@ -68,6 +72,8 @@ loop until final answer and no queued input
|
||||
Provider --> Plant : finish + usage + native blocks
|
||||
deactivate Provider
|
||||
Plant -> Plant : drain + re-ingest assistant Turn\nwith serving-lane provenance
|
||||
Plant -> Admission : release before retry backoff
|
||||
deactivate Admission
|
||||
Plant --> Session : ModelTurnResult
|
||||
deactivate Plant
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e1431edf3891785b922c52b7897e3af5d39ba9973a815f892d6afdb762c5297b
|
||||
size 612662
|
||||
oid sha256:c74e99c530c3a8af9ab35b1e4d8c4fef0ea35c0c04cc35da7cf3588e71382057
|
||||
size 661175
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:79299b25ccc10484af13684a89ed9457abb9bc1781604bf6a9000ccb84362e55
|
||||
size 311107
|
||||
oid sha256:b237e00c28225c847b3e1a083c5c35535d1bf1549f6e6c7e2b6c4fd0e037a980
|
||||
size 331906
|
||||
|
||||
@@ -54,6 +54,23 @@ When a per-model override is `NULL` (empty in the UI), the global default is
|
||||
used. Switching models via `/model <alias>` re-resolves sampling parameters
|
||||
from the new model's overrides or global defaults.
|
||||
|
||||
### Per-model concurrency
|
||||
|
||||
Each model definition may set `max_concurrency` to limit simultaneous model
|
||||
generations for that alias in one Turnstone process. `0` or an omitted value
|
||||
means unlimited. The gate is shared by every role using the alias—interactive
|
||||
turns, coordinators, task agents, judges, output guards, perception, compaction,
|
||||
and title generation—and a streaming generation holds its slot until the
|
||||
stream is fully drained or closed.
|
||||
|
||||
Admission is strictly per alias. Two aliases remain independent even when they
|
||||
point to the same URL; Turnstone does not infer shared capacity from endpoint
|
||||
text. Queue time is excluded from judge/output-guard deadline accounting, and
|
||||
each retry releases its slot before backoff and reacquires for the next wire
|
||||
attempt. The cap is local to each process, not cluster-wide; account for the
|
||||
number of nodes targeting the same inference server. Direct STT/TTS protocol
|
||||
calls and Cohere/Jina reranking do not currently consume this generation cap.
|
||||
|
||||
### Model backend authentication
|
||||
|
||||
Model definitions support four backend credential modes:
|
||||
@@ -445,6 +462,9 @@ send. Endpoint, provider, backend model ID, capabilities, extra parameters, and
|
||||
backend-auth configuration are replaced as one immutable binding. In-flight
|
||||
turns, judges, and task agents finish or cancel against the binding they
|
||||
started with; an admin edit never tears one request across two definitions.
|
||||
The alias's admission gate is retained and resized in place, so a concurrency
|
||||
edit preserves in-flight accounting and does not reset cached judges or the
|
||||
output-guard rate limiter.
|
||||
|
||||
Sampling and other saved workstream configuration remain workstream state. A
|
||||
model-definition edit does not silently rewrite a live workstream's chosen
|
||||
|
||||
Reference in New Issue
Block a user