mainsequence 9.0.9.dev46__tar.gz → 9.0.10__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/PKG-INFO +1 -1
  2. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +8 -5
  3. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/agent_scaffold/skills/mainsequence-inference/SKILL.md +15 -0
  4. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/client/agent_runtime_models.py +17 -1
  5. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/client/inference.py +12 -1
  6. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/client/models_user.py +81 -17
  7. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence.egg-info/PKG-INFO +1 -1
  8. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/pyproject.toml +1 -1
  9. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_inference_client.py +30 -0
  10. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/LICENSE +0 -0
  11. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/README.md +0 -0
  12. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/agent_scaffold/AGENTS.md +0 -0
  13. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/agent_scaffold/skills/a2a_sdk_execution/SKILL.md +0 -0
  14. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  15. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/agent_scaffold/skills/maintenance/code_repository_maintenance/SKILL.md +0 -0
  16. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/agent_scaffold/skills/maintenance/metatables_transition/SKILL.md +0 -0
  17. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
  18. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
  19. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/agent_scaffold/skills/sdk_code_repository_execution/SKILL.md +0 -0
  20. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/__init__.py +0 -0
  21. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/__main__.py +0 -0
  22. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/_request_identity.py +0 -0
  23. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/bootstrap.py +0 -0
  24. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/cli/__init__.py +0 -0
  25. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/cli/api.py +0 -0
  26. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/cli/browser_auth.py +0 -0
  27. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/cli/cli.py +0 -0
  28. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/cli/code_repository_status.py +0 -0
  29. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/cli/config.py +0 -0
  30. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/cli/docker_utils.py +0 -0
  31. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/cli/doctor.py +0 -0
  32. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/cli/local_ops.py +0 -0
  33. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/cli/model_filters.py +0 -0
  34. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/cli/pydantic_cli.py +0 -0
  35. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/cli/sdk_utils.py +0 -0
  36. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/cli/ssh_utils.py +0 -0
  37. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/cli/ui.py +0 -0
  38. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/client/__init__.py +0 -0
  39. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/client/_transport.py +0 -0
  40. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/client/base.py +0 -0
  41. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/client/client.py +0 -0
  42. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/client/compute_validation.py +0 -0
  43. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/client/exceptions.py +0 -0
  44. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/client/github_issues.py +0 -0
  45. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/client/models_foundry.py +0 -0
  46. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/client/models_helpers.py +0 -0
  47. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/client/observability.py +0 -0
  48. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/client/utils.py +0 -0
  49. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/client/value_sets.py +0 -0
  50. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/code_repository_context.py +0 -0
  51. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/code_repository_skills.py +0 -0
  52. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/defaults.py +0 -0
  53. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/instrumentation/__init__.py +0 -0
  54. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/instrumentation/utils.py +0 -0
  55. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/logconf.py +0 -0
  56. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/repository_identity_security.py +0 -0
  57. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/runtime_credential_exchange.py +0 -0
  58. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/runtime_flags.py +0 -0
  59. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/scaffold_skills.py +0 -0
  60. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/server/__init__.py +0 -0
  61. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/server/caller_assertions.py +0 -0
  62. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence/server/fastapi.py +0 -0
  63. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence.egg-info/SOURCES.txt +0 -0
  64. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence.egg-info/dependency_links.txt +0 -0
  65. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence.egg-info/entry_points.txt +0 -0
  66. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence.egg-info/requires.txt +0 -0
  67. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/mainsequence.egg-info/top_level.txt +0 -0
  68. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/setup.cfg +0 -0
  69. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_auth_bootstrap.py +0 -0
  70. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_auth_precedence.py +0 -0
  71. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_caller_assertions.py +0 -0
  72. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_canonical_api_route_contract.py +0 -0
  73. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_cli_auth_contract.py +0 -0
  74. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_cli_browser_auth.py +0 -0
  75. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_cli_secure_storage.py +0 -0
  76. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_client.py +0 -0
  77. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_code_repository_branch_contract.py +0 -0
  78. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_code_repository_documentation_contract.py +0 -0
  79. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_code_repository_runtime_context.py +0 -0
  80. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_code_repository_skills.py +0 -0
  81. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_credential_repr.py +0 -0
  82. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_dependency_extras.py +0 -0
  83. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_dev_release_version.py +0 -0
  84. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_environment_resource_context.py +0 -0
  85. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_environment_response_contracts.py +0 -0
  86. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_fastapi_request_identity.py +0 -0
  87. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_github_issues_client.py +0 -0
  88. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_http_retry_budget.py +0 -0
  89. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_independent_context.py +0 -0
  90. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_instrumentation.py +0 -0
  91. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_job_client.py +0 -0
  92. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_local_ops.py +0 -0
  93. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_logconf.py +0 -0
  94. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_models_user_request_bound_auth.py +0 -0
  95. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_notification_uid_contract.py +0 -0
  96. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_observability_client.py +0 -0
  97. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_runtime_credential_exchange.py +0 -0
  98. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_scaffold_skills.py +0 -0
  99. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_secret_client_model.py +0 -0
  100. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_source_ownership.py +0 -0
  101. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_ssh_utils.py +0 -0
  102. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_tolerant_response_reading.py +0 -0
  103. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_tolerant_value_sets.py +0 -0
  104. {mainsequence-9.0.9.dev46 → mainsequence-9.0.10}/tests/test_user_application_facts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 9.0.9.dev46
3
+ Version: 9.0.10
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License-Expression: MIT
@@ -138,11 +138,14 @@ remain projections of the same identity, not a separate resolver. Outside an
138
138
  authenticated request, including public routes, `User.get_logged_user()` raises
139
139
  `RequestIdentityError`.
140
140
 
141
- On a protected route, this identity is the human making the current HTTP
142
- request. It is not the release creator, deployment owner, runtime workload
143
- principal, or the process account returned by `User.get_authenticated_user_details()`.
144
- Caller authentication is not resource authorization; apply the endpoint's
145
- application policy using the verified user UID.
141
+ On a protected route, this identity is the caller of the current HTTP request:
142
+ a person, or the workload identity a deployed Job, FastAPI release or Agent runs
143
+ as. It is not the release creator, deployment owner, the application's own
144
+ runtime workload principal, or the process account returned by
145
+ `User.get_authenticated_user_details()`. To tell a workload caller from a
146
+ person, read `User.get_by_uid(uid)`: a workload identity has `identity_type`
147
+ `"workload"`. Caller authentication is not resource authorization; apply the
148
+ endpoint's application policy using the verified user UID.
146
149
 
147
150
  WebSockets retain the platform's gateway ticket/header contract rather than
148
151
  the HTTP Bearer/assertion flow; the getter is available while the authenticated
@@ -46,6 +46,21 @@ context. Never invent an Environment, use another branch as a fallback, or add
46
46
  inference environment variables. Provider secrets remain in existing server-side
47
47
  credential storage and must not appear in messages or `provider_options`.
48
48
 
49
+ ## Select a configured provider
50
+
51
+ Pass optional `custom_id="openai-work"` to `client.complete()` to select that named
52
+ credential within the caller's Environment. Omit it to retain the first available
53
+ owned credential, then the first shared credential. A missing or ambiguous explicit
54
+ name fails; do not retry using a different credential. Recorded requests retain the
55
+ exact credential UID for replay. Custom endpoints use their existing provider
56
+ identifier and do not accept `custom_id`.
57
+
58
+ Sharing a configured provider authorizes recipients, including workload users,
59
+ to use its credentials and receive them in local Tau. They may copy those credentials;
60
+ revoking a share cannot remove copies already delivered. Explain this before
61
+ guiding someone through sharing. This inference client executes on the server;
62
+ provider management remains with the existing provider clients.
63
+
49
64
  ## Construct a completion envelope
50
65
 
51
66
  Every call supplies complete ordered text messages: `system`, `user`, `assistant`.
@@ -727,6 +727,7 @@ class Agent(
727
727
  llm_provider: str | None = None,
728
728
  llm_model: str | None = None,
729
729
  llm_thinking: str | None = None,
730
+ custom_id: str | None = None,
730
731
  timeout=None,
731
732
  ) -> AgentSession:
732
733
  """
@@ -759,6 +760,10 @@ class Agent(
759
760
  "llm_provider": llm_provider,
760
761
  "llm_model": llm_model,
761
762
  "llm_thinking": llm_thinking,
763
+ "custom_id": (
764
+ custom_id
765
+ if custom_id is not None else None
766
+ ),
762
767
  }
763
768
  if resolved_session_uid and any(value is not None for value in creation_options.values()):
764
769
  raise ValueError("Creation options require handle_unique_id, not session_uid")
@@ -787,7 +792,14 @@ class Agent(
787
792
  session_payload = response.json()
788
793
  if not isinstance(session_payload, dict):
789
794
  raise TypeError("get_or_create_session response must be an AgentSession object")
790
- return AgentSession(**session_payload)
795
+ session = AgentSession(**session_payload)
796
+ if (
797
+ custom_id is not None
798
+ and session.custom_id
799
+ != creation_options["custom_id"]
800
+ ):
801
+ raise ValueError("The returned session does not use the requested configured provider")
802
+ return session
791
803
 
792
804
 
793
805
  class AgentSessionInsightsBase(BasePydanticModel):
@@ -1745,6 +1757,10 @@ class AgentSession(
1745
1757
  None,
1746
1758
  description="Timestamp when the session was archived.",
1747
1759
  )
1760
+ model_provider_credential_uid: str | None = None
1761
+ custom_id: str | None = Field(
1762
+ None, description="Resolved configured provider name; None for custom endpoints."
1763
+ )
1748
1764
  llm_provider: str = Field(
1749
1765
  ...,
1750
1766
  description="Resolved LLM provider actually used for this session. Unlike Agent defaults, this is intended to be the authoritative runtime record.",
@@ -20,6 +20,8 @@ class InferenceResult(BaseModel):
20
20
  sequence: int
21
21
  provider: str
22
22
  model: str
23
+ custom_id: str | None = None
24
+ model_provider_credential_uid: str | None = None
23
25
  status: str
24
26
  error_code: str
25
27
  content_available: bool
@@ -125,6 +127,7 @@ class InferenceClient:
125
127
  messages: list[dict[str, str]],
126
128
  idempotency_key: str,
127
129
  conversation_uid: str | UUID | None = None,
130
+ custom_id: str | None = None,
128
131
  thinking_level: str | None = None,
129
132
  provider_options: dict[str, Any] | None = None,
130
133
  provider_storage: dict[str, Any] | None = None,
@@ -142,6 +145,8 @@ class InferenceClient:
142
145
  "idempotency_key must contain 1–128 letters, digits, dots, underscores, colons or hyphens."
143
146
  )
144
147
  body = {"provider": provider, "model": model, "messages": deepcopy(messages)}
148
+ if custom_id is not None:
149
+ body["custom_id"] = custom_id
145
150
  if conversation_uid is not None:
146
151
  body["conversation_uid"] = str(UUID(str(conversation_uid)))
147
152
  for name, value in (
@@ -153,9 +158,15 @@ class InferenceClient:
153
158
  ):
154
159
  if value is not None:
155
160
  body[name] = deepcopy(value)
156
- return self._request(
161
+ result = self._request(
157
162
  "POST", "inference/completions/", body=body, key=idempotency_key, timeout=timeout
158
163
  )
164
+ if (
165
+ custom_id is not None
166
+ and result.custom_id != body["custom_id"]
167
+ ):
168
+ raise InferenceTransportError(idempotency_key)
169
+ return result
159
170
 
160
171
  def conversations(self, *, limit: int = 25, offset: int = 0) -> dict[str, Any]:
161
172
  return self._request(
@@ -23,14 +23,21 @@ from .value_sets import OpenValueSet
23
23
 
24
24
 
25
25
  class RequestUserIdentity(BaseModel):
26
- """Minimal identity of the human making the current runtime request."""
26
+ """Minimal identity of the caller of the current runtime request.
27
+
28
+ The caller is a person or a workload identity; ``User.get_by_uid(uid)``
29
+ reads its User.
30
+ """
27
31
 
28
32
  model_config = ConfigDict(extra="forbid", frozen=True)
29
33
 
30
34
  uid: str = Field(
31
35
  ...,
32
36
  title="User UID",
33
- description="Canonical public UUID of the human making the current request.",
37
+ description=(
38
+ "Canonical public UUID of the User making the current request, a person or a "
39
+ "workload identity."
40
+ ),
34
41
  )
35
42
  username: str | None = Field(
36
43
  None,
@@ -954,6 +961,17 @@ class ShareableAccessState(BasePydanticModel):
954
961
 
955
962
 
956
963
  class User(UserApiBaseObjectOrm, BasePydanticModel):
964
+ """A platform User: a person, or the workload identity a deployed Job,
965
+ FastAPI release or Agent runs as.
966
+
967
+ A workload identity is read with ``User.get_by_uid(uid)`` or listed with
968
+ ``User.filter(identity_type="workload")``. Its row carries ``uid``,
969
+ ``identity_type`` (``"workload"``), ``is_active`` and the UID of its
970
+ workload in ``job_uid``, ``resource_release_uid`` or ``agent_uid``, without
971
+ a username, email, join date or name. Fields the row does not carry are
972
+ ``None``, or empty for lists.
973
+ """
974
+
957
975
  # Platform facts, not application authorization policy. None means the
958
976
  # connected platform has not supplied the versioned additive contract.
959
977
  is_organization_admin: StrictBool | None = None
@@ -963,6 +981,7 @@ class User(UserApiBaseObjectOrm, BasePydanticModel):
963
981
  FILTERSET_FIELDS: ClassVar[dict[str, list[str]] | None] = {
964
982
  "uid": ["exact", "in"],
965
983
  "email": ["exact", "contains", "in"],
984
+ "identity_type": ["exact"],
966
985
  }
967
986
  FILTER_VALUE_NORMALIZERS: ClassVar[dict[str, str]] = {
968
987
  "uid": "uid",
@@ -970,6 +989,7 @@ class User(UserApiBaseObjectOrm, BasePydanticModel):
970
989
  "email": "str",
971
990
  "email__contains": "str",
972
991
  "email__in": "str",
992
+ "identity_type": "str",
973
993
  }
974
994
 
975
995
  id: int | None = Field(
@@ -985,6 +1005,35 @@ class User(UserApiBaseObjectOrm, BasePydanticModel):
985
1005
  description="Stable public unique identifier of the user.",
986
1006
  examples=["8f5d6b54-2f5e-4a8b-bb10-0b17f3f4c123"],
987
1007
  )
1008
+ identity_type: (
1009
+ OpenValueSet[Literal["human", "service_account", "deleted_user", "workload"]] | None
1010
+ ) = Field(
1011
+ None,
1012
+ title="Identity Type",
1013
+ description=(
1014
+ "Kind of identity: `workload` for the identity a deployed Job, FastAPI release "
1015
+ "or Agent runs as. None when the response does not carry it."
1016
+ ),
1017
+ examples=["workload"],
1018
+ )
1019
+ job_uid: str | None = Field(
1020
+ None,
1021
+ title="Job UID",
1022
+ description="UID of the Job a workload identity belongs to; None otherwise.",
1023
+ examples=["88888888-8888-4888-8888-888888888888"],
1024
+ )
1025
+ resource_release_uid: str | None = Field(
1026
+ None,
1027
+ title="Resource Release UID",
1028
+ description="UID of the release a workload identity belongs to; None otherwise.",
1029
+ examples=[None],
1030
+ )
1031
+ agent_uid: str | None = Field(
1032
+ None,
1033
+ title="Agent UID",
1034
+ description="UID of the Agent a workload identity belongs to; None otherwise.",
1035
+ examples=[None],
1036
+ )
988
1037
  profile_picture: str | None = Field(
989
1038
  None,
990
1039
  title="Profile Picture URL",
@@ -1022,10 +1071,13 @@ class User(UserApiBaseObjectOrm, BasePydanticModel):
1022
1071
  examples=["enterprise"],
1023
1072
  )
1024
1073
 
1025
- date_joined: datetime.datetime = Field(
1026
- ...,
1074
+ date_joined: datetime.datetime | None = Field(
1075
+ None,
1027
1076
  title="Date Joined",
1028
- description="Timestamp when the user account was created.",
1077
+ description=(
1078
+ "Timestamp when the user account was created. None when the response does not "
1079
+ "carry it, as for a workload identity."
1080
+ ),
1029
1081
  examples=["2025-01-10T08:15:00Z"],
1030
1082
  )
1031
1083
  is_active: bool = Field(
@@ -1034,16 +1086,22 @@ class User(UserApiBaseObjectOrm, BasePydanticModel):
1034
1086
  description="Whether the user account is active.",
1035
1087
  examples=[True],
1036
1088
  )
1037
- username: str = Field(
1038
- ...,
1089
+ username: str | None = Field(
1090
+ None,
1039
1091
  title="Username",
1040
- description="Unique username used by the platform.",
1092
+ description=(
1093
+ "Unique username used by the platform. None when the response does not carry it, "
1094
+ "as for a workload identity."
1095
+ ),
1041
1096
  examples=["jose@main-sequence.io"],
1042
1097
  )
1043
- email: str = Field(
1044
- ...,
1098
+ email: str | None = Field(
1099
+ None,
1045
1100
  title="Email",
1046
- description="Primary email address of the user.",
1101
+ description=(
1102
+ "Primary email address of the user. None when the response does not carry it, "
1103
+ "as for a workload identity."
1104
+ ),
1047
1105
  examples=["jose@main-sequence.io"],
1048
1106
  )
1049
1107
  first_name: str | None = Field(
@@ -1064,16 +1122,22 @@ class User(UserApiBaseObjectOrm, BasePydanticModel):
1064
1122
  description="Timestamp of the last successful login.",
1065
1123
  examples=["2026-03-15T11:20:00Z"],
1066
1124
  )
1067
- api_request_limit: int = Field(
1068
- ...,
1125
+ api_request_limit: int | None = Field(
1126
+ None,
1069
1127
  title="API Request Limit",
1070
- description="Maximum number of API requests available to the user in the current limit window.",
1128
+ description=(
1129
+ "Maximum number of API requests available to the user in the current limit "
1130
+ "window. None when the response does not carry it, as for a workload identity."
1131
+ ),
1071
1132
  examples=[10000],
1072
1133
  )
1073
- mfa_enabled: bool = Field(
1074
- ...,
1134
+ mfa_enabled: bool | None = Field(
1135
+ None,
1075
1136
  title="MFA Enabled",
1076
- description="Whether multi-factor authentication is enabled for the user.",
1137
+ description=(
1138
+ "Whether multi-factor authentication is enabled for the user. None when the "
1139
+ "response does not carry it, as for a workload identity."
1140
+ ),
1077
1141
  examples=[True],
1078
1142
  )
1079
1143
  organization: Organization | None = Field(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 9.0.9.dev46
3
+ Version: 9.0.10
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mainsequence"
7
- version = "9.0.9.dev46"
7
+ version = "9.0.10"
8
8
  description = "Main Sequence SDK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -165,3 +165,33 @@ def test_invalid_success_is_uncertain_and_never_retried(transport, invalid):
165
165
  assert exc.value.idempotency_key == "stable"
166
166
  assert session.request.call_count == 1
167
167
  assert session.request.call_args.kwargs["allow_redirects"] is False
168
+
169
+
170
+ @pytest.mark.parametrize("subscription_uid", [None, "11111111-1111-4111-8111-111111111111"])
171
+ def test_completion_subscription_is_optional_and_preserves_request_identity(transport, subscription_uid):
172
+ session, response, _ = transport
173
+ response.json.return_value = result(custom_id=subscription_uid)
174
+ answer = inference.InferenceClient().complete(
175
+ provider="openai", model="configured-model", messages=[], idempotency_key="stable",
176
+ custom_id=subscription_uid,
177
+ )
178
+ body = session.request.call_args.kwargs["json"]
179
+ assert answer.custom_id == subscription_uid
180
+ assert session.request.call_args.kwargs["headers"]["Idempotency-Key"] == "stable"
181
+ if subscription_uid is None:
182
+ assert "custom_id" not in body
183
+ else:
184
+ assert body["custom_id"] == subscription_uid
185
+
186
+
187
+ @pytest.mark.parametrize("resolved", [None, "22222222-2222-4222-8222-222222222222"])
188
+ def test_completion_does_not_accept_ignored_or_different_subscription(transport, resolved):
189
+ session, response, _ = transport
190
+ response.json.return_value = result(custom_id=resolved)
191
+ with pytest.raises(inference.InferenceTransportError) as error:
192
+ inference.InferenceClient().complete(
193
+ provider="openai", model="configured-model", messages=[], idempotency_key="stable",
194
+ custom_id="11111111-1111-4111-8111-111111111111",
195
+ )
196
+ assert error.value.idempotency_key == "stable"
197
+ assert session.request.call_count == 1