Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
35a428c
fix(gateway): remove implicit model request timeout
seonghobae Sep 4, 2026
60c04f9
Merge remote-tracking branch 'origin/main' into codex/goal-39-unbound…
seonghobae Sep 4, 2026
a60ed89
Merge remote-tracking branch 'origin/main' into codex/goal-39-unbound…
seonghobae Sep 4, 2026
28082a1
Merge branch 'main' into codex/goal-39-unbounded-model-timeout
seonghobae Sep 4, 2026
284447f
fix(embeddings): preserve unbounded completion wait
seonghobae Sep 4, 2026
661ce8d
chore(gateway): integrate protected main into timeout repair
seonghobae Sep 6, 2026
35465a5
test(gateway): require durable model timeout policy
seonghobae Sep 6, 2026
c276fce
feat(gateway): persist administrator model timeout policy
seonghobae Sep 6, 2026
94c6856
test(gateway): cover timeout migration and numeric storage
seonghobae Sep 6, 2026
dac678c
test(gateway): prove persisted timeout rows rather than seed replay
seonghobae Sep 6, 2026
439da2e
fix(gateway): normalize timeout seconds before durable binding
seonghobae Sep 6, 2026
6236e98
test(gateway): reject unaudited model timeout mutation
seonghobae Sep 6, 2026
32b0199
docs(gateway): track timeout policy and audit acceptance gaps
seonghobae Sep 6, 2026
197e888
test(gateway): require transactional timeout history
seonghobae Sep 6, 2026
4e839ce
fix(gateway): commit timeout policy and history atomically
seonghobae Sep 6, 2026
c387943
test(gateway): cover timeout rollback and stale policy writers
seonghobae Sep 6, 2026
cb24fa9
docs(gateway): record transactional timeout evidence and remaining gates
seonghobae Sep 6, 2026
ef76ade
test(gateway): reject stale timeout policy after value restoration
seonghobae Sep 6, 2026
d911a38
fix(gateway): compare durable timeout policy revisions
seonghobae Sep 6, 2026
bceaeb2
test(gateway): reject mixed timeout value and revision snapshots
seonghobae Sep 6, 2026
e5e9c96
fix(gateway): read model configuration from one database snapshot
seonghobae Sep 6, 2026
5765307
docs(gateway): record timeout revision and snapshot proofs
seonghobae Sep 6, 2026
cbab94c
test(gateway): retain authenticated principal in timeout history
seonghobae Sep 6, 2026
e0dc420
test(gateway): authorize the timeout history actor fixture
seonghobae Sep 6, 2026
c1b372d
test(gateway): supply caller address to actor authorization
seonghobae Sep 6, 2026
c08a5fd
feat(gateway): retain opaque timeout policy actor evidence
seonghobae Sep 6, 2026
fc23402
test(gateway): validate timeout actor migration and secret boundaries
seonghobae Sep 6, 2026
9c01b57
docs(gateway): distinguish timeout actor evidence from HTTP authoriza…
seonghobae Sep 6, 2026
2919652
test(gateway): require revision-bound timeout restoration
seonghobae Sep 6, 2026
37bca9c
feat(gateway): restore model timeout as a revision-checked change
seonghobae Sep 6, 2026
62ba3c3
test(gateway): cover timeout restore ownership and rollback
seonghobae Sep 6, 2026
6aece30
docs(gateway): record revision-bound timeout restore evidence
seonghobae Sep 6, 2026
e0eab78
test(gateway): reject stale timeout in ordinary pool edits
seonghobae Sep 6, 2026
9701dec
fix(gateway): guard timeout policy in every durable pool save
seonghobae Sep 6, 2026
28313ef
docs(gateway): record ordinary save policy protection
seonghobae Sep 6, 2026
1edf0fb
test(gateway): preserve serving state when pool edits fail
seonghobae Sep 6, 2026
befe04c
fix(gateway): publish pool edits only after durable saves
seonghobae Sep 6, 2026
8a412b8
docs(gateway): record rejected edit publication evidence
seonghobae Sep 6, 2026
ad337e1
test(gateway): require rollback on late pool batch conflicts
seonghobae Sep 6, 2026
36fc35d
fix(gateway): commit pool batch changes in one transaction
seonghobae Sep 6, 2026
81145d4
docs(gateway): record atomic pool batch evidence
seonghobae Sep 6, 2026
a2951f6
test(gateway): verify timeout conflict on authenticated HTTP edits
seonghobae Sep 6, 2026
3612ab9
docs(gateway): distinguish HTTP conflict checks from timeout activation
seonghobae Sep 6, 2026
e794ed3
docs(gateway): link canonical runtime acceptance request
seonghobae Sep 6, 2026
b80e64b
test(admin): require durable timeout policy read without runtime acti…
seonghobae Sep 6, 2026
2e12ed4
feat(admin): read durable timeout policy without activating execution…
seonghobae Sep 6, 2026
6774dab
test(admin): bind timeout policy reads to admin scope and OpenAPI
seonghobae Sep 6, 2026
b0d1fda
docs(admin): distinguish policy visibility from runtime timeout enfor…
seonghobae Sep 6, 2026
80b3aa3
test(admin): require model-scoped stable timeout history pagination
seonghobae Sep 6, 2026
5dc69bc
feat(admin): expose bounded model timeout audit history
seonghobae Sep 6, 2026
f9505a5
test(admin): verify timeout history HTTP bounds and document schema
seonghobae Sep 6, 2026
b128834
docs(admin): record bounded read-only timeout audit evidence
seonghobae Sep 6, 2026
10225f4
test(server): require concurrent error log response correlation
seonghobae Sep 6, 2026
4b73933
test(server): inject generic handler failure after auth boundary
seonghobae Sep 6, 2026
0e7c03b
fix(server): correlate sanitized error logs with response IDs
seonghobae Sep 6, 2026
8c20f1e
test(server): keep untrusted error details out of correlation logs
seonghobae Sep 6, 2026
443aa5f
docs(server): record Strix timeout and correlation regression evidence
seonghobae Sep 6, 2026
4b1c108
test(server): reproduce SDK replay of terminal tool errors
seonghobae Sep 6, 2026
fc94faa
fix(server): prevent SDK replay of explicit tool stops
seonghobae Sep 6, 2026
5c82b9c
test(server): verify tool stop header without optional SDK
seonghobae Sep 6, 2026
ea5555d
docs(server): record explicit tool stop SDK retry contract
seonghobae Sep 6, 2026
08e5193
test(gateway): require typed nonreplayable transport outcomes
seonghobae Sep 6, 2026
260767d
test(server): prevent SDK replay of unknown provider outcomes
seonghobae Sep 6, 2026
fe1e85a
fix(gateway): surface unknown outcomes without enabling replay
seonghobae Sep 6, 2026
76d1caa
test(gateway): verify SDK to HTTP unknown outcome boundary
seonghobae Sep 6, 2026
4ceafa0
docs(gateway): record unknown outcome no-replay evidence
seonghobae Sep 6, 2026
a35a3c6
test(logging): require bounded upstream status evidence
seonghobae Sep 6, 2026
b20f994
fix(logging): retain numeric provider status without raw diagnostics
seonghobae Sep 6, 2026
0b949aa
test(logging): preserve typed status and avoid exception stringification
seonghobae Sep 6, 2026
73f8977
docs(logging): record bounded upstream status evidence
seonghobae Sep 6, 2026
b81739f
test(logging): require omission rather than partial diagnostic masking
seonghobae Sep 6, 2026
4316515
docs(logging): record full regression and canonical sanitizer prerequ…
seonghobae Sep 6, 2026
e1ff51b
test(timeout): preserve policy across unrelated writes
seonghobae Sep 7, 2026
b2a38b4
fix(timeout): isolate policy writes from pool updates
seonghobae Sep 7, 2026
9179873
test(timeout): verify isolated writes and disabled tombstones
seonghobae Sep 7, 2026
f50755b
style(tests): clean timeout integration checks
seonghobae Sep 7, 2026
78a71c0
fix(router): distinguish pre-send admission timeouts
seonghobae Sep 7, 2026
1ccc959
test(sdk): require pinned integration probes in locked CI
seonghobae Sep 7, 2026
a95d3c8
fix(audit): record timeout policy changes and history access
seonghobae Sep 7, 2026
7685d3f
fix(admin): make model timeout audit entries readable
seonghobae Sep 7, 2026
2b189ad
fix(gateway): apply per-model timeout on the serving path
seonghobae Sep 7, 2026
76c0475
test(local): accept per-model timeout on waiter mock
seonghobae Sep 7, 2026
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
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ push or open a PR.

### This repo: the org LLM gateway

- Classify retry safety by the failure boundary, not a generic timeout name.
Only a direct local-slot admission failure proves no upstream send began;
wrapped exceptions and post-send timeouts do not authorize replay. Test the
real slot-to-transport path with per-candidate transport call counts and keep
unknown-outcome no-replay controls alongside it. Transport spies are not wire
delivery evidence. Preserve the default-null model timeout.

- Endpoint races require a complete operator-reviewed equivalence contract.
Never infer equivalence from provider/model names, and never treat missing loser
usage as free or zero-cost execution.
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ and this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html)

### Fixed

- Model, Agent, gateway, and structured-output repair requests now default to
no application timeout. An administrator-owned per-model wait is applied only
to that model; there is no shared 90s/900s/3-hour ceiling. Explicit probe,
discovery, benchmark, and operator limits remain bounded.
- Workflow workers now preserve the caller message array exactly once, while
the added envelope carries only the subtask and Conductor-style prior-step
access list instead of duplicating the task or source attachments.
Expand Down
44 changes: 40 additions & 4 deletions contextual_orchestrator/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@
"models_table_scroll_hint": "Model details table. Scroll horizontally to review latency and success.",
"no_agents_configured": "Add a model connection to start routing requests.",
"no_audit_events": "Run a workflow to create your first audit event.",
"audit_timeout_changed": "Model time limit updated",
"audit_timeout_restored": "Model time limit restored",
"audit_model_reference": "Model",
"audit_revision_reference": "Revision",
"audit_restored_reference": "Restored from revision",
"audit_date_unknown": "Date unavailable",
"audit_event_heading": "Event",
"audit_detail_heading": "Detail",
"audit_created_heading": "Created",
"no_policy_evidence": "No policy evidence is loaded. Open Audit to review recorded events.",
"no_recent_errors": "No current alerts. Open Audit to review recent changes.",
"prompt_placeholder": "Describe the task you want to route, then run the trace.",
Expand Down Expand Up @@ -297,6 +306,15 @@
"models_table_scroll_hint": "모델 상세 표입니다. 지연 시간과 성공률을 보려면 가로로 스크롤하세요.",
"no_agents_configured": "요청 라우팅을 시작하려면 모델 연결을 추가하세요.",
"no_audit_events": "첫 감사 이벤트를 만들려면 워크플로를 실행하세요.",
"audit_timeout_changed": "모델 시간 제한 변경",
"audit_timeout_restored": "모델 시간 제한 복원",
"audit_model_reference": "모델",
"audit_revision_reference": "변경 버전",
"audit_restored_reference": "복원한 버전",
"audit_date_unknown": "날짜 확인 불가",
"audit_event_heading": "변경 내용",
"audit_detail_heading": "상세",
"audit_created_heading": "기록 시각",
"no_policy_evidence": "불러온 정책 근거가 없습니다. 기록된 이벤트를 검토하려면 감사를 여세요.",
"no_recent_errors": "현재 알림이 없습니다. 최근 변경 사항을 검토하려면 감사를 여세요.",
"prompt_placeholder": "라우팅할 작업을 설명한 다음 트레이스를 실행하세요.",
Expand Down Expand Up @@ -879,6 +897,9 @@
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { scroll-behavior: auto !important; }
}
.audit-table { min-width: 0; table-layout: fixed; }
.audit-table th, .audit-table td { white-space: normal; overflow-wrap: anywhere; vertical-align: top; }
.audit-table th:nth-child(2) { width: 45%; }
</style>
</head>
<body>
Expand Down Expand Up @@ -1067,7 +1088,7 @@
<section class="detail-grid view" data-view="audit" hidden>
<section class="panel wide">
<div class="panel-header"><h1 data-i18n="audit_compliance">Audit &amp; Compliance</h1><span class="chip">Evidence</span></div>
<table><thead><tr><th>Event</th><th>Detail</th><th>Created</th></tr></thead><tbody id="auditRows"></tbody></table>
<table class="audit-table"><thead><tr><th data-i18n="audit_event_heading">Event</th><th data-i18n="audit_detail_heading">Detail</th><th data-i18n="audit_created_heading">Created</th></tr></thead><tbody id="auditRows"></tbody></table>
</section>
</section>
<section class="detail-grid view" data-view="settings" hidden>
Expand Down Expand Up @@ -1616,9 +1637,24 @@
}
function renderAudit() {
const events = state.recent_audit_events || [];
els.auditRows.innerHTML = events.map(event => `
<tr><td>${escapeHtml(event.event_type)}</td><td><pre>${escapeHtml(JSON.stringify(event.event_detail))}</pre></td><td>${escapeHtml(event.created_at)}</td></tr>
`).join("") || `<tr><td colspan="3" class="empty" data-i18n="no_audit_events">${t("no_audit_events")}</td></tr>`;
els.auditRows.innerHTML = events.map(event => {
let label = event.event_type;
let detail = JSON.stringify(event.event_detail);
if (event.event_type === "model_timeout_policy_changed") {
const data = event.event_detail || {};
const restored = Number.isSafeInteger(data.restored_from_revision) && data.restored_from_revision > 0;
label = t(restored ? "audit_timeout_restored" : "audit_timeout_changed");
const references = [];
if (typeof data.worker_agent_id === "string") references.push(`${t("audit_model_reference")}: ${data.worker_agent_id}`);
if (Number.isSafeInteger(data.revision) && data.revision > 0) references.push(`${t("audit_revision_reference")}: ${data.revision}`);
if (restored) references.push(`${t("audit_restored_reference")}: ${data.restored_from_revision}`);
detail = references.join(" · ");
}
const date = typeof event.created_at === "number" && Number.isFinite(event.created_at)
? new Date(event.created_at * 1000) : new Date(NaN);
const created = Number.isNaN(date.getTime()) ? t("audit_date_unknown") : date.toLocaleString(currentLang);
return `<tr><td>${escapeHtml(label)}</td><td>${escapeHtml(detail)}</td><td>${escapeHtml(created)}</td></tr>`;
}).join("") || `<tr><td colspan="3" class="empty" data-i18n="no_audit_events">${t("no_audit_events")}</td></tr>`;
}
function renderSecondaryViews() {
if (!state.policy) return;
Expand Down
78 changes: 78 additions & 0 deletions contextual_orchestrator/api_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,78 @@
"responses": {"200": {"description": "Agent pool collection"}},
}
},
"/api/v1/agent_pools/{agent_pool_id}/worker_agents/{worker_agent_id}/timeout_policy/history": {
"get": {
"operationId": "list_model_timeout_history",
"summary": "Read older model timeout changes with a stable revision cursor",
"security": [{"admin_bearer_auth": []}],
"parameters": [
{"name": "agent_pool_id", "in": "path", "required": True, "schema": {"type": "string"}},
{"name": "worker_agent_id", "in": "path", "required": True, "schema": {"type": "string"}},
{"name": "page_size", "in": "query", "schema": {"type": "integer", "minimum": 1, "maximum": 100, "default": 20}},
{"name": "before_revision", "in": "query", "schema": {"type": "integer", "minimum": 1, "maximum": _AGENT_POOL_INTEGER_MAX}},
],
"responses": {
"200": {"description": "Descending revisions; changed_at is Unix seconds, actor_id is an opaque digest or null",
"content": {"application/json": {"schema": {
"type": "object", "additionalProperties": False,
"required": ["items", "next_before_revision", "history_available"],
"properties": {
"history_available": {"type": "boolean"},
"next_before_revision": {"type": ["integer", "null"], "minimum": 1},
"items": {"type": "array", "maxItems": 100, "items": {
"type": "object", "additionalProperties": False,
"required": ["revision", "previous_seconds", "configured_seconds", "changed_at", "actor_id", "restored_from_revision"],
"properties": {
"revision": {"type": "integer", "minimum": 1},
"previous_seconds": {"type": ["number", "null"]},
"configured_seconds": {"type": ["number", "null"]},
"changed_at": {"type": "number"},
"actor_id": {"type": ["string", "null"]},
"restored_from_revision": {"type": ["integer", "null"], "minimum": 1},
},
}},
},
}}}},
"400": {"description": "Invalid page size or revision cursor"},
"401": {"description": "Administrator authentication required"},
"404": {"description": "Model configuration not found"},
},
},
},
"/api/v1/agent_pools/{agent_pool_id}/worker_agents/{worker_agent_id}/timeout_policy": {
"get": {
"operationId": "get_model_timeout_policy",
"summary": "Read configured timeout and whether serving applies that model wait",
"security": [{"admin_bearer_auth": []}],
"parameters": [
{"name": name, "in": "path", "required": True, "schema": {"type": "string"}}
for name in ("agent_pool_id", "worker_agent_id")
],
"responses": {
"200": {
"description": "Stored policy and local snapshot; serving applies the selected model wait",
"content": {"application/json": {"schema": {
"type": "object", "additionalProperties": False,
"required": ["configured_seconds", "revision", "unit",
"serving_snapshot_seconds", "serving_snapshot_revision",
"enforcement_available"],
"properties": {
"configured_seconds": {"type": ["number", "null"], "exclusiveMinimum": 0},
"revision": {"type": "integer", "minimum": 0},
"unit": {"const": "seconds"},
"serving_snapshot_seconds": {"type": ["number", "null"], "exclusiveMinimum": 0},
"serving_snapshot_revision": {"type": "integer", "minimum": 0},
"enforcement_available": {"const": True},
},
}}},
},
"401": {"description": "Authentication required"},
"403": {"description": "Administrator scope required"},
"404": {"description": "Model configuration not found"},
},
},
},
"/api/v1/agent_pools/{agent_pool_id}/worker_agents/{worker_agent_id}": {
"patch": {
"operationId": "patch_worker_agent",
Expand Down Expand Up @@ -523,6 +595,12 @@
]
},
"stream_usage_supported": {"type": "boolean"},
"model_timeout_seconds": {
"anyOf": [
{"type": "number", "exclusiveMinimum": 0},
{"type": "null"},
]
},
},
},
},
Expand Down
4 changes: 2 additions & 2 deletions contextual_orchestrator/batch_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1111,8 +1111,8 @@ def _publish_terminal(
self._errors[job_id] = error
self._states[job_id] = status

def wait(self, job: BatchJob, *, timeout: float) -> Dict[str, Any]:
"""Wait within the caller's explicit deadline for a terminal state."""
def wait(self, job: BatchJob, *, timeout: float | None) -> Dict[str, Any]:
"""Wait for a terminal state, bounded only when the caller sets a deadline."""
Comment thread
coderabbitai[bot] marked this conversation as resolved.
event = self._terminal_events.get(job.job_id)
if event is not None:
event.wait(timeout=timeout)
Expand Down
26 changes: 19 additions & 7 deletions contextual_orchestrator/cost_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
_DEFAULT_EMBEDDING_MAX_TOKENS_PER_REQUEST = 280_000
_DEFAULT_EMBEDDING_MAX_CHARS_PER_PART = 240_000
_DEFAULT_EMBEDDING_MAX_INPUTS_PER_REQUEST = 1
_DEFAULT_PROVIDER_EMBEDDING_CLAIM_LEASE_SECONDS = 30.0
_BATCH_LEDGER_SETTLEMENT_TIMEOUT_SECONDS = 1.0
_EMBEDDING_UNIT_RE = re.compile(r"\S+\s*|\s+", re.UNICODE)

Expand Down Expand Up @@ -232,15 +233,22 @@ def _run_embedding_shard(

def _provider_embedding_backend(self) -> ProviderEmbeddingBatchBackend:
client = getattr(self.orchestrator, "client", None)
client_timeout = float(getattr(client, "timeout", 0))
configured_timeout = getattr(client, "timeout", None)
client_timeout = (
float(configured_timeout) if configured_timeout is not None else 0.0
Comment thread
seonghobae marked this conversation as resolved.
)
return ProviderEmbeddingBatchBackend(
self._run_provider_embeddings,
job_registry=self.job_registry,
max_concurrency=getattr(client, "local_concurrency", 1),
claim_lease_seconds=(
client_timeout
if self.job_registry.durable and client_timeout > 0
else None
else (
_DEFAULT_PROVIDER_EMBEDDING_CLAIM_LEASE_SECONDS
if self.job_registry.durable
else None
)
),
execution_timeout_seconds=client_timeout if client_timeout > 0 else None,
)
Expand Down Expand Up @@ -1834,9 +1842,9 @@ def complete_embeddings_batch(
) -> Dict[str, Any]:
"""Submit an embeddings batch and return its document (one round-trip).

Local backends complete immediately. Callers that require a synchronous
provider result pass ``wait_timeout``; a timed-out queued job is
cancelled so the synchronous surface does not leave orphaned work.
Local backends complete immediately. ``wait_timeout=None`` waits without
an application deadline; a timed-out queued job is cancelled only when
the caller supplied a finite deadline.
"""
job = self.submit_embeddings_batch(
inputs,
Expand All @@ -1848,9 +1856,13 @@ def complete_embeddings_batch(
owner_id=owner_id,
)
backend = self._embedding_backend_for(job)
if wait_timeout is not None and hasattr(backend, "wait"):
if hasattr(backend, "wait"):
status = backend.wait(job, timeout=wait_timeout)
if not status.get("is_complete") and hasattr(backend, "cancel"):
if (
wait_timeout is not None
and not status.get("is_complete")
and hasattr(backend, "cancel")
):
backend.cancel(job, reason="synchronous request deadline elapsed")
return self.embeddings_batch_document(job.job_id, owner_id=owner_id)

Expand Down
12 changes: 8 additions & 4 deletions contextual_orchestrator/endpoint_race.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def race_first_valid(
attempts: list[EndpointAttempt[T]],
*,
validate: Callable[[T], bool],
deadline_seconds: float,
deadline_seconds: float | None,
max_concurrency: int,
on_attempt_complete: Callable[[str, T | None, BaseException | None], None] | None = None,
) -> RaceOutcome[T]:
Expand All @@ -94,7 +94,7 @@ def race_first_valid(
raise ValueError("immediate_race requires concurrency capacity of at least two")
if max_concurrency < len(attempts):
raise ValueError("immediate_race capacity must cover every declared endpoint")
if deadline_seconds <= 0:
if deadline_seconds is not None and deadline_seconds <= 0:
raise ValueError("deadline_seconds must be positive")
contract = attempts[0].contract
if any(attempt.contract != contract for attempt in attempts[1:]):
Expand Down Expand Up @@ -128,8 +128,12 @@ def execute(attempt: EndpointAttempt[T]) -> T:
last_error: BaseException | None = None
try:
while pending:
remaining = deadline_seconds - (time.monotonic() - started)
if remaining <= 0:
remaining = (
None
if deadline_seconds is None
else deadline_seconds - (time.monotonic() - started)
)
if remaining is not None and remaining <= 0:
raise TimeoutError("equivalent endpoint race exceeded its deadline")
done, pending = wait(pending, timeout=remaining, return_when=FIRST_COMPLETED)
if not done:
Expand Down
Loading
Loading