Background
We are planning to migrate LiveHint AI from direct UpGrade v6 API requests to the Python client introduced in #3146.
LiveHint currently calls the following endpoints directly:
POST /api/v6/init
POST /api/v6/assign
POST /api/v6/mark
POST /api/v6/reward
Most of these calls map cleanly to the Python client. However, two client limitations prevent us from preserving the existing application behavior:
- The Python client does not expose an HTTP timeout configuration.
UpgradeClient.mark_decision_point() requires condition: str, even though a missing assignment should be marked with a null condition.
1. Add configurable HTTP timeout support
Current behavior
ApiService creates new httpx.Client and httpx.AsyncClient instances without specifying a timeout:
async with httpx.AsyncClient() as client:
...
with httpx.Client() as client:
...
As a result, the client uses httpx's default timeout.
LiveHint currently uses a 0.5-second timeout for UpGrade requests. Migrating without configurable timeout support would increase the failure-path latency from approximately 0.5 seconds to the httpx default.
Because these calls occur synchronously in user-facing Flask routes, this would be a user-visible behavioral change.
Proposed API
Add an optional timeout to UpgradeClient:
client = UpgradeClient(
user_id="user-123",
host_url="https://upgrade.example.com",
context="livehint-ai",
timeout=0.5,
)
Suggested constructor signature:
def __init__(
self,
user_id: str,
host_url: str,
context: str,
token: str = "",
client_session_id: str | None = None,
timeout: float | httpx.Timeout = 5.0,
) -> None:
The value should be passed to ApiService and used by both HTTP clients:
async with httpx.AsyncClient(timeout=self._timeout) as client:
...
with httpx.Client(timeout=self._timeout) as client:
...
The default should preserve the current effective behavior for existing Python client consumers.
2. Allow a null condition when marking no assignment
Current behavior
The backend and JavaScript client support marking a decision point without a condition when no experiment assignment exists.
Conceptually, the request is:
{
"status": "no condition assigned",
"data": {
"site": "problem-info",
"target": "mathbook_tx",
"assignedCondition": {
"conditionCode": null
}
}
}
The Python client currently declares condition and condition_code as str in:
UpgradeClient.mark_decision_point
UpgradeClient.mark_decision_point_sync
ApiService.mark_decision_point
ApiService.mark_decision_point_sync
ApiService._build_mark_body
This makes the following valid use case incompatible with the public type contract:
client.mark_decision_point_sync(
condition=None,
status=UpgradeClient.MARKED_DECISION_POINT_STATUS.NO_CONDITION_ASSIGNED,
site="problem-info",
target="mathbook_tx",
)
Although None may currently pass through at runtime, callers should not need to violate the documented type contract.
Proposed change
Update the relevant public and internal method signatures to accept:
and:
condition_code: str | None
When the condition is None, the serialized request should send:
"assignedCondition": {
"conditionCode": null
}
This should match the backend and JavaScript client behavior for NO_CONDITION_ASSIGNED.
Tests
Please add coverage for the following cases.
Timeout
- A custom numeric timeout is forwarded to
httpx.AsyncClient.
- A custom numeric timeout is forwarded to
httpx.Client.
- An
httpx.Timeout object is accepted.
- Omitting the timeout preserves the current default behavior.
- Timeout exceptions continue to propagate consistently from sync and async APIs.
Nullable condition
- Async
mark_decision_point accepts condition=None.
- Sync
mark_decision_point_sync accepts condition=None.
- The request contains
"conditionCode": null.
- The request uses the
NO_CONDITION_ASSIGNED status.
- Normal non-null condition marking remains unchanged.
- Assignment-based marking through
Assignment.mark_decision_point remains unchanged.
Documentation
Update the Python client README to document:
- The new optional
timeout constructor parameter.
- A no-assignment marking example using
condition=None.
- The default timeout behavior.
Example:
client = UpgradeClient(
user_id="user-123",
host_url="https://upgrade.example.com",
context="my-app",
timeout=0.5,
)
client.init_sync()
assignment = client.get_decision_point_assignment_sync(
site="problem-info",
target="mathbook_tx",
)
if assignment is None:
client.mark_decision_point_sync(
condition=None,
status=UpgradeClient.MARKED_DECISION_POINT_STATUS.NO_CONDITION_ASSIGNED,
site="problem-info",
target="mathbook_tx",
)
Acceptance criteria
UpgradeClient exposes a backward-compatible timeout option.
- The configured timeout is used by both synchronous and asynchronous HTTP requests.
- Public mark methods accept
None as the condition.
- A null condition is serialized correctly for
NO_CONDITION_ASSIGNED.
- Existing client behavior remains backward compatible.
- Python client unit tests, type checking, and linting pass.
- The changes are included in a published PyPI patch release so downstream applications can pin the fixed version.
Downstream use case
Once released, LiveHint AI will use the client for the complete flow:
init → get decision-point assignment → validate payload → mark
It will also replace its direct reward request with send_reward_sync().
Configurable timeout and nullable-condition support will allow that migration without changing LiveHint's existing failure latency or no-assignment telemetry.
Background
We are planning to migrate LiveHint AI from direct UpGrade v6 API requests to the Python client introduced in #3146.
LiveHint currently calls the following endpoints directly:
POST /api/v6/initPOST /api/v6/assignPOST /api/v6/markPOST /api/v6/rewardMost of these calls map cleanly to the Python client. However, two client limitations prevent us from preserving the existing application behavior:
UpgradeClient.mark_decision_point()requirescondition: str, even though a missing assignment should be marked with a null condition.1. Add configurable HTTP timeout support
Current behavior
ApiServicecreates newhttpx.Clientandhttpx.AsyncClientinstances without specifying a timeout:As a result, the client uses httpx's default timeout.
LiveHint currently uses a 0.5-second timeout for UpGrade requests. Migrating without configurable timeout support would increase the failure-path latency from approximately 0.5 seconds to the httpx default.
Because these calls occur synchronously in user-facing Flask routes, this would be a user-visible behavioral change.
Proposed API
Add an optional timeout to
UpgradeClient:Suggested constructor signature:
The value should be passed to
ApiServiceand used by both HTTP clients:The default should preserve the current effective behavior for existing Python client consumers.
2. Allow a null condition when marking no assignment
Current behavior
The backend and JavaScript client support marking a decision point without a condition when no experiment assignment exists.
Conceptually, the request is:
{ "status": "no condition assigned", "data": { "site": "problem-info", "target": "mathbook_tx", "assignedCondition": { "conditionCode": null } } }The Python client currently declares
conditionandcondition_codeasstrin:UpgradeClient.mark_decision_pointUpgradeClient.mark_decision_point_syncApiService.mark_decision_pointApiService.mark_decision_point_syncApiService._build_mark_bodyThis makes the following valid use case incompatible with the public type contract:
Although
Nonemay currently pass through at runtime, callers should not need to violate the documented type contract.Proposed change
Update the relevant public and internal method signatures to accept:
and:
When the condition is
None, the serialized request should send:This should match the backend and JavaScript client behavior for
NO_CONDITION_ASSIGNED.Tests
Please add coverage for the following cases.
Timeout
httpx.AsyncClient.httpx.Client.httpx.Timeoutobject is accepted.Nullable condition
mark_decision_pointacceptscondition=None.mark_decision_point_syncacceptscondition=None."conditionCode": null.NO_CONDITION_ASSIGNEDstatus.Assignment.mark_decision_pointremains unchanged.Documentation
Update the Python client README to document:
timeoutconstructor parameter.condition=None.Example:
Acceptance criteria
UpgradeClientexposes a backward-compatible timeout option.Noneas the condition.NO_CONDITION_ASSIGNED.Downstream use case
Once released, LiveHint AI will use the client for the complete flow:
It will also replace its direct reward request with
send_reward_sync().Configurable timeout and nullable-condition support will allow that migration without changing LiveHint's existing failure latency or no-assignment telemetry.