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
4 changes: 4 additions & 0 deletions sdk-endpoints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ POST /v1/auth/session # dashboard-only
# it exists for a browser deciding what to render. An SDK already knows.
GET /v1/bootstrap # dashboard-only
DELETE /v1/auth/session # dashboard-only
# Setting the dashboard sign-in password: the other half of the same browser
# flow. An SDK authenticates with the master key or an API key and never holds
# a password, so there is nothing here for one to wrap.
PUT /v1/auth/password # dashboard-only
# OTLP ingest: OpenTelemetry collector receivers, not an SDK surface.
POST /v1/logs # otel ingest
POST /v1/traces # otel ingest
Expand Down
4 changes: 4 additions & 0 deletions src/otari/_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@
"OrganizationModelPricingUpdate",
"OrganizationModelPricingsPublic",
"OrganizationPublic",
"PasswordResponse",
"PolicyRequest",
"PolicyResponse",
"PoolStatus",
Expand Down Expand Up @@ -385,6 +386,7 @@
"SendTestMailRequest",
"SendTestMailResponse",
"SessionResponse",
"SetPasswordRequest",
"SetPricingRequest",
"Source",
"Source1",
Expand Down Expand Up @@ -790,6 +792,7 @@
from otari._client.models.organization_model_pricing_update import OrganizationModelPricingUpdate as OrganizationModelPricingUpdate
from otari._client.models.organization_model_pricings_public import OrganizationModelPricingsPublic as OrganizationModelPricingsPublic
from otari._client.models.organization_public import OrganizationPublic as OrganizationPublic
from otari._client.models.password_response import PasswordResponse as PasswordResponse
from otari._client.models.policy_request import PolicyRequest as PolicyRequest
from otari._client.models.policy_response import PolicyResponse as PolicyResponse
from otari._client.models.pool_status import PoolStatus as PoolStatus
Expand Down Expand Up @@ -828,6 +831,7 @@
from otari._client.models.send_test_mail_request import SendTestMailRequest as SendTestMailRequest
from otari._client.models.send_test_mail_response import SendTestMailResponse as SendTestMailResponse
from otari._client.models.session_response import SessionResponse as SessionResponse
from otari._client.models.set_password_request import SetPasswordRequest as SetPasswordRequest
from otari._client.models.set_pricing_request import SetPricingRequest as SetPricingRequest
from otari._client.models.source import Source as Source
from otari._client.models.source1 import Source1 as Source1
Expand Down
286 changes: 283 additions & 3 deletions src/otari/_client/api/auth_api.py

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions src/otari/_client/api/bootstrap_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def get_bootstrap_v1_bootstrap_get(
) -> DeploymentBootstrap:
"""Get Bootstrap

Return the deployment context the dashboard shell renders from. Public: the shell fetches this before it knows whether it can authenticate.
Return the deployment context the dashboard shell renders from. Public: the shell fetches this before it knows whether it can authenticate. That is also why ``sign_in_methods`` is answered here rather than behind a credential, and it publishes nothing an unauthenticated caller could not already learn by trying both credentials against the sign-in endpoint. The one database read is a ``LIMIT 1`` probe for any identity holding a password, over a table a standalone deployment keeps one row per person in. It runs only in standalone mode: a hybrid gateway has no session to describe, and ``get_db_if_needed`` hands it no session to read one from.

:param _request_timeout: timeout setting for this request. If one
number provided, it will be total request
Expand Down Expand Up @@ -116,7 +116,7 @@ def get_bootstrap_v1_bootstrap_get_with_http_info(
) -> ApiResponse[DeploymentBootstrap]:
"""Get Bootstrap

Return the deployment context the dashboard shell renders from. Public: the shell fetches this before it knows whether it can authenticate.
Return the deployment context the dashboard shell renders from. Public: the shell fetches this before it knows whether it can authenticate. That is also why ``sign_in_methods`` is answered here rather than behind a credential, and it publishes nothing an unauthenticated caller could not already learn by trying both credentials against the sign-in endpoint. The one database read is a ``LIMIT 1`` probe for any identity holding a password, over a table a standalone deployment keeps one row per person in. It runs only in standalone mode: a hybrid gateway has no session to describe, and ``get_db_if_needed`` hands it no session to read one from.

:param _request_timeout: timeout setting for this request. If one
number provided, it will be total request
Expand Down Expand Up @@ -179,7 +179,7 @@ def get_bootstrap_v1_bootstrap_get_without_preload_content(
) -> RESTResponseType:
"""Get Bootstrap

Return the deployment context the dashboard shell renders from. Public: the shell fetches this before it knows whether it can authenticate.
Return the deployment context the dashboard shell renders from. Public: the shell fetches this before it knows whether it can authenticate. That is also why ``sign_in_methods`` is answered here rather than behind a credential, and it publishes nothing an unauthenticated caller could not already learn by trying both credentials against the sign-in endpoint. The one database read is a ``LIMIT 1`` probe for any identity holding a password, over a table a standalone deployment keeps one row per person in. It runs only in standalone mode: a hybrid gateway has no session to describe, and ``get_db_if_needed`` hands it no session to read one from.

:param _request_timeout: timeout setting for this request. If one
number provided, it will be total request
Expand Down
2 changes: 2 additions & 0 deletions src/otari/_client/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@
from otari._client.models.organization_model_pricing_update import OrganizationModelPricingUpdate
from otari._client.models.organization_model_pricings_public import OrganizationModelPricingsPublic
from otari._client.models.organization_public import OrganizationPublic
from otari._client.models.password_response import PasswordResponse
from otari._client.models.policy_request import PolicyRequest
from otari._client.models.policy_response import PolicyResponse
from otari._client.models.pool_status import PoolStatus
Expand Down Expand Up @@ -335,6 +336,7 @@
from otari._client.models.send_test_mail_request import SendTestMailRequest
from otari._client.models.send_test_mail_response import SendTestMailResponse
from otari._client.models.session_response import SessionResponse
from otari._client.models.set_password_request import SetPasswordRequest
from otari._client.models.set_pricing_request import SetPricingRequest
from otari._client.models.source import Source
from otari._client.models.source1 import Source1
Expand Down
32 changes: 26 additions & 6 deletions src/otari/_client/models/create_session_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,21 @@
import re # noqa: F401
import json

from pydantic import BaseModel, ConfigDict, Field, StrictStr
from typing import Any, ClassVar, Dict, List
from pydantic import BaseModel, ConfigDict, Field
from typing import Any, ClassVar, Dict, List, Optional
from typing_extensions import Annotated
from typing import Optional, Set
from typing_extensions import Self
from pydantic_core import to_jsonable_python

class CreateSessionRequest(BaseModel):
"""
Sign in to the dashboard by proving possession of the master key.
Sign in to the dashboard with exactly one credential. A flat body with an optional field per credential, rather than a tagged union: it is one extra key on the wire, it generates a client type a hand-written form can fill in, and the validator below makes the two forms exclusive anyway. The example carries one credential, because a generated example is a body somebody will post: the schema alone would produce every field at once, which is the one shape the validator below refuses.
""" # noqa: E501
master_key: StrictStr = Field(description="The gateway master key; verified once and never stored by the browser.")
__properties: ClassVar[List[str]] = ["master_key"]
email: Optional[Annotated[str, Field(strict=True, max_length=255)]] = Field(default=None, description="The identity's sign-in address.")
master_key: Optional[Annotated[str, Field(strict=True, max_length=512)]] = Field(default=None, description="The gateway master key; verified once and never stored by the browser. Accepted only while no identity on this deployment has a password (see GET /v1/bootstrap).")
password: Optional[Annotated[str, Field(strict=True, max_length=72)]] = Field(default=None, description="The identity's password.")
__properties: ClassVar[List[str]] = ["email", "master_key", "password"]

model_config = ConfigDict(
validate_by_name=True,
Expand Down Expand Up @@ -69,6 +72,21 @@ def to_dict(self) -> Dict[str, Any]:
exclude=excluded_fields,
exclude_none=True,
)
# set to None if email (nullable) is None
# and model_fields_set contains the field
if self.email is None and "email" in self.model_fields_set:
_dict['email'] = None

# set to None if master_key (nullable) is None
# and model_fields_set contains the field
if self.master_key is None and "master_key" in self.model_fields_set:
_dict['master_key'] = None

# set to None if password (nullable) is None
# and model_fields_set contains the field
if self.password is None and "password" in self.model_fields_set:
_dict['password'] = None

return _dict

@classmethod
Expand All @@ -81,7 +99,9 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
return cls.model_validate(obj)

_obj = cls.model_validate({
"master_key": obj.get("master_key")
"email": obj.get("email"),
"master_key": obj.get("master_key"),
"password": obj.get("password")
})
return _obj

Expand Down
14 changes: 12 additions & 2 deletions src/otari/_client/models/deployment_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ class DeploymentBootstrap(BaseModel):
deployment_type: StrictStr = Field(description="Which deployment serves this URL. 'standalone' owns its own data; 'hosted' is otari.ai; 'hybrid' is a gateway attached to otari.ai, which is data-plane only and holds no management surface of its own.")
mail_ready: StrictBool = Field(description="Whether this deployment can deliver a message carrying a link back to itself (an invitation's accept link, and the verification and reset links to come), not merely whether a transport is configured: it also needs to know its own public URL to put in one. Lets the dashboard disable or hide a mail-dependent affordance instead of offering one that would fail at send time. Every message this control plane sends carries such a link, which is why this is one flag and not one per feature. False for a hybrid gateway, whose control plane is otari.ai and which sends no mail of its own.")
management_url: Optional[StrictStr] = Field(description="Where the authoritative control plane lives when it is not this deployment. Set for a hybrid gateway so its landing page can link to otari.ai; null otherwise.")
session_type: StrictStr = Field(description="The kind of session this deployment issues, not whether the caller holds one. 'local_operator' is the standalone master-key sign-in, 'hosted_user' an otari.ai account, and 'none' a deployment that issues no management session at all.")
session_type: StrictStr = Field(description="The kind of session this deployment issues, not whether the caller holds one. 'local_operator' is the standalone operator sign-in (see sign_in_methods for which credential it currently accepts), 'hosted_user' an otari.ai account, and 'none' a deployment that issues no management session at all.")
sign_in_methods: List[StrictStr] = Field(description="How POST /v1/auth/session may be authenticated right now, sorted. 'master_key' is the first-boot credential and is offered until some identity on this deployment has a password; 'password' replaces it from then on, and the master key stays the credential for the management API. Empty for a hybrid gateway, which issues no session. The login page renders from this rather than trying a credential to find out.")
surfaces: List[StrictStr] = Field(description="Management API groups this deployment serves, sorted, which is what its dashboard pages gate on. Named surfaces, not capabilities: capability is otari.ai's word for the entitlement (licensing) axis, and this is the deployment (topology) axis. Empty for a hybrid gateway.")
__properties: ClassVar[List[str]] = ["deployment_type", "mail_ready", "management_url", "session_type", "surfaces"]
__properties: ClassVar[List[str]] = ["deployment_type", "mail_ready", "management_url", "session_type", "sign_in_methods", "surfaces"]

@field_validator('deployment_type')
def deployment_type_validate_enum(cls, value):
Expand All @@ -48,6 +49,14 @@ def session_type_validate_enum(cls, value):
raise ValueError("must be one of enum values ('local_operator', 'hosted_user', 'none')")
return value

@field_validator('sign_in_methods')
def sign_in_methods_validate_enum(cls, value):
"""Validates the enum"""
for i in value:
if i not in set(['master_key', 'password']):
raise ValueError("each list item must be one of ('master_key', 'password')")
return value

model_config = ConfigDict(
validate_by_name=True,
validate_by_alias=True,
Expand Down Expand Up @@ -108,6 +117,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"mail_ready": obj.get("mail_ready"),
"management_url": obj.get("management_url"),
"session_type": obj.get("session_type"),
"sign_in_methods": obj.get("sign_in_methods"),
"surfaces": obj.get("surfaces")
})
return _obj
Expand Down
90 changes: 90 additions & 0 deletions src/otari/_client/models/password_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# coding: utf-8

"""
otari

Otari, an OpenAI-compatible LLM gateway with API key management

The version of the OpenAPI document: 0.0.0-dev
Generated by OpenAPI Generator (https://openapi-generator.tech)

Do not edit the class manually.
""" # noqa: E501


from __future__ import annotations
import pprint
import re # noqa: F401
import json

from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr
from typing import Any, ClassVar, Dict, List
from typing import Optional, Set
from typing_extensions import Self
from pydantic_core import to_jsonable_python

class PasswordResponse(BaseModel):
"""
What the identity signs in with now.
""" # noqa: E501
email: StrictStr = Field(description="The address this identity signs in with.")
master_key_sign_in_retired: StrictBool = Field(description="Always true once this succeeds: some identity on this deployment now has a password, so POST /v1/auth/session no longer accepts the master key. It stays the credential for the management API.")
__properties: ClassVar[List[str]] = ["email", "master_key_sign_in_retired"]

model_config = ConfigDict(
validate_by_name=True,
validate_by_alias=True,
validate_assignment=True,
protected_namespaces=(),
)


def to_str(self) -> str:
"""Returns the string representation of the model using alias"""
return pprint.pformat(self.model_dump(by_alias=True))

def to_json(self) -> str:
"""Returns the JSON representation of the model using alias"""
return json.dumps(to_jsonable_python(self.to_dict()))

@classmethod
def from_json(cls, json_str: str) -> Optional[Self]:
"""Create an instance of PasswordResponse from a JSON string"""
return cls.from_dict(json.loads(json_str))

def to_dict(self) -> Dict[str, Any]:
"""Return the dictionary representation of the model using alias.

This has the following differences from calling pydantic's
`self.model_dump(by_alias=True)`:

* `None` is only added to the output dict for nullable fields that
were set at model initialization. Other fields with value `None`
are ignored.
"""
excluded_fields: Set[str] = set([
])

_dict = self.model_dump(
by_alias=True,
exclude=excluded_fields,
exclude_none=True,
)
return _dict

@classmethod
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of PasswordResponse from a dict"""
if obj is None:
return None

if not isinstance(obj, dict):
return cls.model_validate(obj)

_obj = cls.model_validate({
"email": obj.get("email"),
"master_key_sign_in_retired": obj.get("master_key_sign_in_retired")
})
return _obj


Loading
Loading