refactor(domain): extract shared model_config into a base Pydantic model
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Failing after 19s
CI / typecheck (pull_request) Failing after 56s
CI / security (pull_request) Failing after 56s
CI / coverage (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Has been skipped
CI / unit_tests (pull_request) Failing after 2m4s
CI / docker (pull_request) Has been skipped
CI / build (pull_request) Successful in 16s
CI / helm (pull_request) Successful in 23s
CI / quality (pull_request) Successful in 3m42s
CI / e2e_tests (pull_request) Failing after 15m14s
CI / integration_tests (pull_request) Failing after 22m3s
CI / status-check (pull_request) Failing after 1s

Introduce DomainBaseModel in src/cleveragents/domain/models/base.py that
defines the single shared model_config (str_strip_whitespace, validate_assignment,
arbitrary_types_allowed=False, populate_by_name, use_enum_values) previously
duplicated verbatim across five domain model files.

Update 14 classes across five files to inherit from DomainBaseModel instead
of pydantic.BaseModel directly, removing all inline model_config duplication:
- aimodelscredentials/ai_models_credentials.py (ModelProviderOption)
- aimodelserrors/ai_models_errors.py (ModelError, FallbackResult)
- aimodelsproviders/ai_models_providers.py (ModelProviderExtraAuthVars,
  ModelProviderConfigSchema)
- auth/auth.py (AuthHeader, TrialPlansExceededError, TrialMessagesExceededError,
  BillingError, ApiError, ClientAccount, ClientAuth)
- planconfig/plan_config.py (PlanConfig, ConfigSetting)

Pure structural refactor — no behavioral changes. Models with different
configurations (acms, aimodels_custom, etc.) are left untouched.

Add BDD feature (22 scenarios) and Robot integration tests (8 test cases)
verifying inheritance, config correctness, and no-duplication invariants.

ISSUES CLOSED: #1941
This commit is contained in:
2026-04-03 02:01:45 +00:00
parent 921c13f410
commit 650b1038b6
10 changed files with 798 additions and 131 deletions
+138
View File
@@ -0,0 +1,138 @@
Feature: DomainBaseModel shared configuration
As a developer working on the domain layer
I want all domain models to inherit from DomainBaseModel
So that the shared model_config is defined in exactly one place
Scenario: DomainBaseModel is importable from the domain models package
Given the cleveragents package is available
When I import DomainBaseModel from the domain models base module
Then the DomainBaseModel class should be available
And DomainBaseModel should be a subclass of pydantic BaseModel
Scenario: DomainBaseModel carries the expected model_config
Given the cleveragents package is available
When I inspect the DomainBaseModel model_config
Then the DomainBaseModel str_strip_whitespace should be True
And the DomainBaseModel validate_assignment should be True
And the DomainBaseModel arbitrary_types_allowed should be False
And the DomainBaseModel populate_by_name should be True
And the DomainBaseModel use_enum_values should be True
Scenario: ModelProviderOption inherits from DomainBaseModel
Given the cleveragents package is available
When I import ModelProviderOption from aimodelscredentials
Then ModelProviderOption should be a subclass of DomainBaseModel
And the ModelProviderOption model_config should match DomainBaseModel
Scenario: ModelError inherits from DomainBaseModel
Given the cleveragents package is available
When I import ModelError from aimodelserrors
Then ModelError should be a subclass of DomainBaseModel
And the ModelError model_config should match DomainBaseModel
Scenario: FallbackResult inherits from DomainBaseModel
Given the cleveragents package is available
When I import FallbackResult from aimodelserrors
Then FallbackResult should be a subclass of DomainBaseModel
And the FallbackResult model_config should match DomainBaseModel
Scenario: ModelProviderExtraAuthVars inherits from DomainBaseModel
Given the cleveragents package is available
When I import ModelProviderExtraAuthVars from aimodelsproviders
Then ModelProviderExtraAuthVars should be a subclass of DomainBaseModel
And the ModelProviderExtraAuthVars model_config should match DomainBaseModel
Scenario: ModelProviderConfigSchema inherits from DomainBaseModel
Given the cleveragents package is available
When I import ModelProviderConfigSchema from aimodelsproviders
Then ModelProviderConfigSchema should be a subclass of DomainBaseModel
And the ModelProviderConfigSchema model_config should match DomainBaseModel
Scenario: AuthHeader inherits from DomainBaseModel
Given the cleveragents package is available
When I import AuthHeader from auth
Then AuthHeader should be a subclass of DomainBaseModel
And the AuthHeader model_config should match DomainBaseModel
Scenario: TrialPlansExceededError inherits from DomainBaseModel
Given the cleveragents package is available
When I import TrialPlansExceededError from auth
Then TrialPlansExceededError should be a subclass of DomainBaseModel
And the TrialPlansExceededError model_config should match DomainBaseModel
Scenario: TrialMessagesExceededError inherits from DomainBaseModel
Given the cleveragents package is available
When I import TrialMessagesExceededError from auth
Then TrialMessagesExceededError should be a subclass of DomainBaseModel
And the TrialMessagesExceededError model_config should match DomainBaseModel
Scenario: BillingError inherits from DomainBaseModel
Given the cleveragents package is available
When I import BillingError from auth
Then BillingError should be a subclass of DomainBaseModel
And the BillingError model_config should match DomainBaseModel
Scenario: ApiError inherits from DomainBaseModel
Given the cleveragents package is available
When I import ApiError from auth
Then ApiError should be a subclass of DomainBaseModel
And the ApiError model_config should match DomainBaseModel
Scenario: ClientAccount inherits from DomainBaseModel
Given the cleveragents package is available
When I import ClientAccount from auth
Then ClientAccount should be a subclass of DomainBaseModel
And the ClientAccount model_config should match DomainBaseModel
Scenario: ClientAuth inherits from DomainBaseModel
Given the cleveragents package is available
When I import ClientAuth from auth
Then ClientAuth should be a subclass of DomainBaseModel
And the ClientAuth model_config should match DomainBaseModel
Scenario: PlanConfig inherits from DomainBaseModel
Given the cleveragents package is available
When I import PlanConfig from planconfig
Then PlanConfig should be a subclass of DomainBaseModel
And the PlanConfig model_config should match DomainBaseModel
Scenario: ConfigSetting inherits from DomainBaseModel
Given the cleveragents package is available
When I import ConfigSetting from planconfig
Then ConfigSetting should be a subclass of DomainBaseModel
And the ConfigSetting model_config should match DomainBaseModel
Scenario: DomainBaseModel config is applied to ModelProviderOption instances
Given the cleveragents package is available
When I create a ModelProviderOption with only priority set to 1
Then the ModelProviderOption should have priority 1
And the ModelProviderOption str_strip_whitespace should be True
And the ModelProviderOption validate_assignment should be True
And the ModelProviderOption arbitrary_types_allowed should be False
And the ModelProviderOption populate_by_name should be True
And the ModelProviderOption use_enum_values should be True
Scenario: No inline model_config duplication in aimodelscredentials
Given the cleveragents package is available
When I inspect the ModelProviderOption class definition
Then ModelProviderOption should not define its own model_config
Scenario: No inline model_config duplication in aimodelserrors
Given the cleveragents package is available
When I inspect the ModelError class definition
Then ModelError should not define its own model_config
Scenario: No inline model_config duplication in aimodelsproviders
Given the cleveragents package is available
When I inspect the ModelProviderConfigSchema class definition
Then ModelProviderConfigSchema should not define its own model_config
Scenario: No inline model_config duplication in auth
Given the cleveragents package is available
When I inspect the AuthHeader class definition
Then AuthHeader should not define its own model_config
Scenario: No inline model_config duplication in planconfig
Given the cleveragents package is available
When I inspect the PlanConfig class definition
Then PlanConfig should not define its own model_config
+296
View File
@@ -0,0 +1,296 @@
"""Step definitions for DomainBaseModel shared configuration tests.
Only steps that are NOT already defined in other step modules are defined here.
Steps shared with aimodelscredentials_steps.py, aimodelserrors_steps.py, and
aimodelsproviders_steps.py are reused automatically by Behave's global step
registry.
"""
from behave import then, when
from pydantic import BaseModel
from cleveragents.domain.models.base import DomainBaseModel
# ---------------------------------------------------------------------------
# Shared expected config values
# ---------------------------------------------------------------------------
_EXPECTED_CONFIG: dict[str, object] = {
"str_strip_whitespace": True,
"validate_assignment": True,
"arbitrary_types_allowed": False,
"populate_by_name": True,
"use_enum_values": True,
}
# ---------------------------------------------------------------------------
# When — new imports (not defined in existing step files)
# ---------------------------------------------------------------------------
@when("I import DomainBaseModel from the domain models base module")
def step_import_domain_base_model(context: object) -> None:
"""Import DomainBaseModel and store on context."""
context.domain_base_model_class = DomainBaseModel
context.imported_class = DomainBaseModel
@when("I inspect the DomainBaseModel model_config")
def step_inspect_domain_base_model_config(context: object) -> None:
"""Store the DomainBaseModel model_config on context."""
context.inspected_config = DomainBaseModel.model_config
@when("I import ModelError from aimodelserrors")
def step_import_model_error(context: object) -> None:
"""Import ModelError."""
from cleveragents.domain.models.aimodelserrors.ai_models_errors import ModelError
context.imported_class = ModelError
@when("I import FallbackResult from aimodelserrors")
def step_import_fallback_result(context: object) -> None:
"""Import FallbackResult."""
from cleveragents.domain.models.aimodelserrors.ai_models_errors import (
FallbackResult,
)
context.imported_class = FallbackResult
@when("I import ModelProviderExtraAuthVars from aimodelsproviders")
def step_import_model_provider_extra_auth_vars(context: object) -> None:
"""Import ModelProviderExtraAuthVars."""
from cleveragents.domain.models.aimodelsproviders.ai_models_providers import (
ModelProviderExtraAuthVars,
)
context.imported_class = ModelProviderExtraAuthVars
@when("I import ModelProviderConfigSchema from aimodelsproviders")
def step_import_model_provider_config_schema(context: object) -> None:
"""Import ModelProviderConfigSchema."""
from cleveragents.domain.models.aimodelsproviders.ai_models_providers import (
ModelProviderConfigSchema,
)
context.imported_class = ModelProviderConfigSchema
@when("I import AuthHeader from auth")
def step_import_auth_header(context: object) -> None:
"""Import AuthHeader."""
from cleveragents.domain.models.auth.auth import AuthHeader
context.imported_class = AuthHeader
@when("I import TrialPlansExceededError from auth")
def step_import_trial_plans_exceeded_error(context: object) -> None:
"""Import TrialPlansExceededError."""
from cleveragents.domain.models.auth.auth import TrialPlansExceededError
context.imported_class = TrialPlansExceededError
@when("I import TrialMessagesExceededError from auth")
def step_import_trial_messages_exceeded_error(context: object) -> None:
"""Import TrialMessagesExceededError."""
from cleveragents.domain.models.auth.auth import TrialMessagesExceededError
context.imported_class = TrialMessagesExceededError
@when("I import BillingError from auth")
def step_import_billing_error(context: object) -> None:
"""Import BillingError."""
from cleveragents.domain.models.auth.auth import BillingError
context.imported_class = BillingError
@when("I import ApiError from auth")
def step_import_api_error(context: object) -> None:
"""Import ApiError."""
from cleveragents.domain.models.auth.auth import ApiError
context.imported_class = ApiError
@when("I import ClientAccount from auth")
def step_import_client_account(context: object) -> None:
"""Import ClientAccount."""
from cleveragents.domain.models.auth.auth import ClientAccount
context.imported_class = ClientAccount
@when("I import ClientAuth from auth")
def step_import_client_auth(context: object) -> None:
"""Import ClientAuth."""
from cleveragents.domain.models.auth.auth import ClientAuth
context.imported_class = ClientAuth
@when("I import PlanConfig from planconfig")
def step_import_plan_config(context: object) -> None:
"""Import PlanConfig."""
from cleveragents.domain.models.planconfig.plan_config import PlanConfig
context.imported_class = PlanConfig
@when("I import ConfigSetting from planconfig")
def step_import_config_setting(context: object) -> None:
"""Import ConfigSetting."""
from cleveragents.domain.models.planconfig.plan_config import ConfigSetting
context.imported_class = ConfigSetting
# When — class definition inspection
@when("I inspect the ModelProviderOption class definition")
def step_inspect_model_provider_option(context: object) -> None:
"""Store ModelProviderOption for inspection."""
from cleveragents.domain.models.aimodelscredentials.ai_models_credentials import (
ModelProviderOption,
)
context.inspected_class = ModelProviderOption
@when("I inspect the ModelError class definition")
def step_inspect_model_error(context: object) -> None:
"""Store ModelError for inspection."""
from cleveragents.domain.models.aimodelserrors.ai_models_errors import ModelError
context.inspected_class = ModelError
@when("I inspect the ModelProviderConfigSchema class definition")
def step_inspect_model_provider_config_schema_def(context: object) -> None:
"""Store ModelProviderConfigSchema for inspection."""
from cleveragents.domain.models.aimodelsproviders.ai_models_providers import (
ModelProviderConfigSchema,
)
context.inspected_class = ModelProviderConfigSchema
@when("I inspect the AuthHeader class definition")
def step_inspect_auth_header(context: object) -> None:
"""Store AuthHeader for inspection."""
from cleveragents.domain.models.auth.auth import AuthHeader
context.inspected_class = AuthHeader
@when("I inspect the PlanConfig class definition")
def step_inspect_plan_config(context: object) -> None:
"""Store PlanConfig for inspection."""
from cleveragents.domain.models.planconfig.plan_config import PlanConfig
context.inspected_class = PlanConfig
# ---------------------------------------------------------------------------
# Then — DomainBaseModel assertions
# ---------------------------------------------------------------------------
@then("the DomainBaseModel class should be available")
def step_domain_base_model_available(context: object) -> None:
"""Verify DomainBaseModel is available."""
assert context.domain_base_model_class is not None
assert context.domain_base_model_class.__name__ == "DomainBaseModel"
@then("DomainBaseModel should be a subclass of pydantic BaseModel")
def step_domain_base_model_is_pydantic(context: object) -> None:
"""Verify DomainBaseModel inherits from pydantic BaseModel."""
assert issubclass(context.domain_base_model_class, BaseModel)
@then("the DomainBaseModel str_strip_whitespace should be True")
def step_domain_base_model_str_strip(context: object) -> None:
"""Verify str_strip_whitespace on DomainBaseModel."""
assert context.inspected_config.get("str_strip_whitespace") is True
@then("the DomainBaseModel validate_assignment should be True")
def step_domain_base_model_validate_assignment(context: object) -> None:
"""Verify validate_assignment on DomainBaseModel."""
assert context.inspected_config.get("validate_assignment") is True
@then("the DomainBaseModel arbitrary_types_allowed should be False")
def step_domain_base_model_arbitrary_types(context: object) -> None:
"""Verify arbitrary_types_allowed on DomainBaseModel."""
assert context.inspected_config.get("arbitrary_types_allowed") is False
@then("the DomainBaseModel populate_by_name should be True")
def step_domain_base_model_populate_by_name(context: object) -> None:
"""Verify populate_by_name on DomainBaseModel."""
assert context.inspected_config.get("populate_by_name") is True
@then("the DomainBaseModel use_enum_values should be True")
def step_domain_base_model_use_enum_values(context: object) -> None:
"""Verify use_enum_values on DomainBaseModel."""
assert context.inspected_config.get("use_enum_values") is True
# ---------------------------------------------------------------------------
# Then — subclass assertions (generic, reused for all domain models)
# ---------------------------------------------------------------------------
@then("{class_name} should be a subclass of DomainBaseModel")
def step_is_subclass_of_domain_base_model(context: object, class_name: str) -> None:
"""Verify the imported class is a subclass of DomainBaseModel."""
assert issubclass(context.imported_class, DomainBaseModel), (
f"{class_name} is not a subclass of DomainBaseModel"
)
@then("the {class_name} model_config should match DomainBaseModel")
def step_config_matches_domain_base_model(context: object, class_name: str) -> None:
"""Verify the imported class inherits the expected model_config values."""
cfg = context.imported_class.model_config
for key, expected in _EXPECTED_CONFIG.items():
actual = cfg.get(key)
assert actual == expected, (
f"{class_name}.model_config[{key!r}] = {actual!r}, expected {expected!r}"
)
# ---------------------------------------------------------------------------
# Then — no inline model_config duplication assertions
# ---------------------------------------------------------------------------
@then("{class_name} should not define its own model_config")
def step_no_own_model_config(context: object, class_name: str) -> None:
"""Verify the class source does not contain an inline model_config assignment.
Pydantic's metaclass copies model_config into every subclass __dict__
automatically, so we inspect the source code instead of __dict__ to
confirm no *explicit* duplication exists.
"""
import inspect
source = inspect.getsource(context.inspected_class)
# Remove the class header line so we only look at the class body
body_lines = source.splitlines()[1:]
body = "\n".join(body_lines)
assert "model_config" not in body, (
f"{class_name} has an explicit model_config in its source — "
"should inherit from DomainBaseModel without redefinition"
)
+61
View File
@@ -0,0 +1,61 @@
*** Settings ***
Documentation Integration smoke tests for DomainBaseModel refactor.
...
... Verifies that extracting the shared model_config into
... DomainBaseModel has not changed the observable behaviour of
... any affected domain model class.
Resource ${CURDIR}/common.resource
Suite Setup Setup Test Environment
Suite Teardown Cleanup Test Environment
*** Variables ***
${HELPER_SCRIPT} robot/helper_domain_base_model.py
*** Test Cases ***
DomainBaseModel Is Importable
[Documentation] DomainBaseModel can be imported and is a pydantic BaseModel subclass
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} importable cwd=${WORKSPACE}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} domain-base-model-importable-ok
DomainBaseModel Carries Expected Config
[Documentation] DomainBaseModel model_config has all expected settings
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} config cwd=${WORKSPACE}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} domain-base-model-config-ok
ModelProviderOption Behaviour Unchanged
[Documentation] ModelProviderOption works correctly after inheriting from DomainBaseModel
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} model-provider-option cwd=${WORKSPACE}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} model-provider-option-ok
ModelError Behaviour Unchanged
[Documentation] ModelError works correctly after inheriting from DomainBaseModel
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} model-error cwd=${WORKSPACE}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} model-error-ok
FallbackResult Behaviour Unchanged
[Documentation] FallbackResult works correctly after inheriting from DomainBaseModel
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} fallback-result cwd=${WORKSPACE}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} fallback-result-ok
Auth Models Behaviour Unchanged
[Documentation] All auth domain models work correctly after inheriting from DomainBaseModel
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} auth-models cwd=${WORKSPACE}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} auth-models-ok
PlanConfig Models Behaviour Unchanged
[Documentation] PlanConfig and ConfigSetting work correctly after inheriting from DomainBaseModel
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} plan-config-models cwd=${WORKSPACE}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} plan-config-models-ok
Whitespace Stripping Active On Refactored Models
[Documentation] str_strip_whitespace config is active on all refactored models
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} whitespace-stripping cwd=${WORKSPACE}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} whitespace-stripping-ok
+239
View File
@@ -0,0 +1,239 @@
"""Helper script for DomainBaseModel Robot integration tests.
Verifies that the structural refactor (extracting shared model_config into
DomainBaseModel) has not changed the observable behaviour of any affected
domain model class.
"""
from __future__ import annotations
import sys
from pydantic import BaseModel
from cleveragents.domain.models.base import DomainBaseModel
def _test_domain_base_model_importable() -> None:
"""DomainBaseModel is importable and is a pydantic BaseModel subclass."""
assert issubclass(DomainBaseModel, BaseModel)
print("domain-base-model-importable-ok")
def _test_domain_base_model_config() -> None:
"""DomainBaseModel carries the expected shared model_config."""
cfg = DomainBaseModel.model_config
assert cfg.get("str_strip_whitespace") is True
assert cfg.get("validate_assignment") is True
assert cfg.get("arbitrary_types_allowed") is False
assert cfg.get("populate_by_name") is True
assert cfg.get("use_enum_values") is True
print("domain-base-model-config-ok")
def _test_model_provider_option() -> None:
"""ModelProviderOption behaviour is unchanged after refactor."""
from cleveragents.domain.models.aimodelscredentials.ai_models_credentials import (
ModelProviderOption,
)
from cleveragents.domain.models.aimodelsproviders.ai_models_providers import (
ModelProviderConfigSchema,
)
from cleveragents.domain.models.core.enums import ModelProvider, ModelPublisher
assert issubclass(ModelProviderOption, DomainBaseModel)
config = ModelProviderConfigSchema(
provider=ModelProvider.OPENAI, baseUrl="https://api.openai.com/v1"
)
publishers = {"primary": {ModelPublisher.OPENAI: True}}
model = ModelProviderOption(publishers=publishers, config=config, priority=1)
assert model.priority == 1
assert model.config is not None
assert model.config.base_url == "https://api.openai.com/v1"
# Verify str_strip_whitespace is active via inherited config
assert model.model_config.get("str_strip_whitespace") is True
print("model-provider-option-ok")
def _test_model_error() -> None:
"""ModelError behaviour is unchanged after refactor."""
from cleveragents.domain.models.aimodelserrors.ai_models_errors import ModelError
from cleveragents.domain.models.core.enums import ModelErrKind
assert issubclass(ModelError, DomainBaseModel)
err = ModelError(kind=ModelErrKind.OVERLOADED, retriable=True, retryafterseconds=30)
assert err.retriable is True
assert err.retryafterseconds == 30
print("model-error-ok")
def _test_fallback_result() -> None:
"""FallbackResult behaviour is unchanged after refactor."""
from cleveragents.domain.models.aimodelserrors.ai_models_errors import (
FallbackResult,
)
from cleveragents.domain.models.core.enums import FallbackType
assert issubclass(FallbackResult, DomainBaseModel)
result = FallbackResult(
fallbacktype=FallbackType.ERROR,
isfallback=True,
modelroleconfig=None,
basemodelconfig=None,
)
assert result.isfallback is True
print("fallback-result-ok")
def _test_auth_models() -> None:
"""Auth model behaviour is unchanged after refactor."""
from cleveragents.domain.models.auth.auth import (
ApiError,
ApiErrorType,
AuthHeader,
BillingError,
ClientAccount,
ClientAuth,
TrialMessagesExceededError,
TrialPlansExceededError,
)
for cls in (
AuthHeader,
TrialPlansExceededError,
TrialMessagesExceededError,
BillingError,
ApiError,
ClientAccount,
ClientAuth,
):
assert issubclass(cls, DomainBaseModel), f"{cls.__name__} not DomainBaseModel"
header = AuthHeader(token="tok", orgId="org-1", hash="abc")
assert header.org_id == "org-1"
trial_plans = TrialPlansExceededError(maxPlans=5)
assert trial_plans.max_plans == 5
trial_msgs = TrialMessagesExceededError(maxMessages=100)
assert trial_msgs.max_messages == 100
billing = BillingError(hasBillingPermission=False, isTrial=True)
assert billing.is_trial is True
api_err = ApiError(type_=ApiErrorType.AUTH, status=401, msg="Unauthorized")
assert api_err.status == 401
account = ClientAccount(
isCloud=True,
host="app.cleverthis.com",
email="user@example.com",
userName="testuser",
userId="uid-1",
token="tok-1",
isLocalMode=False,
isTrial=False,
)
assert account.is_cloud is True
auth = ClientAuth(
orgId="org-1",
orgName="Test Org",
orgIsTrial=False,
integratedModelsMode=True,
)
assert auth.org_name == "Test Org"
print("auth-models-ok")
def _test_plan_config_models() -> None:
"""PlanConfig and ConfigSetting behaviour is unchanged after refactor."""
from cleveragents.domain.models.planconfig.plan_config import (
AutoModeType,
ConfigSetting,
PlanConfig,
)
assert issubclass(PlanConfig, DomainBaseModel)
assert issubclass(ConfigSetting, DomainBaseModel)
plan = PlanConfig(
autoMode=AutoModeType.AUTO,
editor="vim",
editorCommand="vim",
editorOpenManually=False,
autoContinue=True,
autoBuild=False,
autoUpdateContext=True,
autoContext=True,
smartContext=True,
autoApply=False,
autoCommit=False,
skipCommit=False,
canExec=True,
autoExec=False,
autoDebug=False,
autoDebugTries=3,
autoRevertOnRewind=False,
skipChangesMenu=False,
)
assert plan.editor == "vim"
assert plan.auto_mode == AutoModeType.AUTO
setting = ConfigSetting(
name="autoMode",
desc="Auto mode setting",
hascustomchoice=False,
sortkey="a",
)
assert setting.name == "autoMode"
print("plan-config-models-ok")
def _test_whitespace_stripping() -> None:
"""str_strip_whitespace is active on all refactored models."""
from cleveragents.domain.models.aimodelsproviders.ai_models_providers import (
ModelProviderConfigSchema,
)
from cleveragents.domain.models.core.enums import ModelProvider
schema = ModelProviderConfigSchema(
provider=ModelProvider.OPENAI,
baseUrl=" https://api.openai.com/v1 ",
)
assert schema.base_url == "https://api.openai.com/v1", (
f"Expected stripped URL, got: {schema.base_url!r}"
)
print("whitespace-stripping-ok")
_COMMANDS: dict[str, object] = {
"importable": _test_domain_base_model_importable,
"config": _test_domain_base_model_config,
"model-provider-option": _test_model_provider_option,
"model-error": _test_model_error,
"fallback-result": _test_fallback_result,
"auth-models": _test_auth_models,
"plan-config-models": _test_plan_config_models,
"whitespace-stripping": _test_whitespace_stripping,
}
def main() -> None:
if len(sys.argv) < 2:
raise SystemExit("Expected command argument")
command = sys.argv[1]
fn = _COMMANDS.get(command)
if fn is None:
raise SystemExit(f"Unknown helper command: {command!r}")
fn() # type: ignore[operator]
if __name__ == "__main__":
main()
@@ -1,22 +1,15 @@
"""Domain models for CleverAgents - auto-generated from Phase 0 stubs."""
from pydantic import BaseModel, ConfigDict, Field
from pydantic import Field
from ..aimodelsproviders.ai_models_providers import ModelProviderConfigSchema
from ..base import DomainBaseModel
from ..core.enums import ModelPublisher
class ModelProviderOption(BaseModel):
class ModelProviderOption(DomainBaseModel):
"""Data contract for ModelProviderOption."""
publishers: dict[str, dict[ModelPublisher, bool]] = Field(default_factory=dict)
config: ModelProviderConfigSchema | None = Field(default=None)
priority: int = Field(...)
model_config = ConfigDict(
str_strip_whitespace=True,
validate_assignment=True,
arbitrary_types_allowed=False,
populate_by_name=True,
use_enum_values=True,
)
@@ -1,39 +1,24 @@
"""Domain models for CleverAgents - auto-generated from Phase 0 stubs."""
from pydantic import BaseModel, ConfigDict, Field
from pydantic import Field
from ..aimodelsdatamodels import BaseModelConfig, ModelRoleConfig
from ..base import DomainBaseModel
from ..core.enums import FallbackType, ModelErrKind
class ModelError(BaseModel):
class ModelError(DomainBaseModel):
"""Data contract for ModelError."""
kind: ModelErrKind = Field(...)
retriable: bool = Field(...)
retryafterseconds: int = Field(...)
model_config = ConfigDict(
str_strip_whitespace=True,
validate_assignment=True,
arbitrary_types_allowed=False,
populate_by_name=True,
use_enum_values=True,
)
class FallbackResult(BaseModel):
class FallbackResult(DomainBaseModel):
"""Data contract for FallbackResult."""
modelroleconfig: ModelRoleConfig | None = Field(default=None)
isfallback: bool = Field(...)
fallbacktype: FallbackType = Field(...)
basemodelconfig: BaseModelConfig | None = Field(default=None)
model_config = ConfigDict(
str_strip_whitespace=True,
validate_assignment=True,
arbitrary_types_allowed=False,
populate_by_name=True,
use_enum_values=True,
)
@@ -1,11 +1,12 @@
"""Domain models for CleverAgents - auto-generated from Phase 0 stubs."""
from pydantic import BaseModel, ConfigDict, Field
from pydantic import Field
from ..base import DomainBaseModel
from ..core.enums import ModelProvider
class ModelProviderExtraAuthVars(BaseModel):
class ModelProviderExtraAuthVars(DomainBaseModel):
"""Data contract for ModelProviderExtraAuthVars."""
var: str = Field(...)
@@ -15,16 +16,8 @@ class ModelProviderExtraAuthVars(BaseModel):
required: bool | None = Field(default=None)
default: str | None = Field(default=None)
model_config = ConfigDict(
str_strip_whitespace=True,
validate_assignment=True,
arbitrary_types_allowed=False,
populate_by_name=True,
use_enum_values=True,
)
class ModelProviderConfigSchema(BaseModel):
class ModelProviderConfigSchema(DomainBaseModel):
"""Data contract for ModelProviderConfigSchema."""
provider: ModelProvider = Field(...)
@@ -38,11 +31,3 @@ class ModelProviderConfigSchema(BaseModel):
extra_auth_vars: list[ModelProviderExtraAuthVars] | None = Field(
alias="extraAuthVars", default=None
)
model_config = ConfigDict(
str_strip_whitespace=True,
validate_assignment=True,
arbitrary_types_allowed=False,
populate_by_name=True,
use_enum_values=True,
)
+10 -64
View File
@@ -2,7 +2,9 @@
from enum import StrEnum
from pydantic import BaseModel, ConfigDict, Field
from pydantic import Field
from ..base import DomainBaseModel
class ApiErrorType(StrEnum):
@@ -16,66 +18,34 @@ class ApiErrorType(StrEnum):
NOT_FOUND = "not_found"
class AuthHeader(BaseModel):
class AuthHeader(DomainBaseModel):
"""Data contract for AuthHeader."""
token: str = Field(...)
org_id: str = Field(..., alias="orgId")
hash: str = Field(...)
model_config = ConfigDict(
str_strip_whitespace=True,
validate_assignment=True,
arbitrary_types_allowed=False,
populate_by_name=True,
use_enum_values=True,
)
class TrialPlansExceededError(BaseModel):
class TrialPlansExceededError(DomainBaseModel):
"""Data contract for TrialPlansExceededError."""
max_plans: int = Field(..., alias="maxPlans")
model_config = ConfigDict(
str_strip_whitespace=True,
validate_assignment=True,
arbitrary_types_allowed=False,
populate_by_name=True,
use_enum_values=True,
)
class TrialMessagesExceededError(BaseModel):
class TrialMessagesExceededError(DomainBaseModel):
"""Data contract for TrialMessagesExceededError."""
max_messages: int = Field(..., alias="maxMessages")
model_config = ConfigDict(
str_strip_whitespace=True,
validate_assignment=True,
arbitrary_types_allowed=False,
populate_by_name=True,
use_enum_values=True,
)
class BillingError(BaseModel):
class BillingError(DomainBaseModel):
"""Data contract for BillingError."""
has_billing_permission: bool = Field(..., alias="hasBillingPermission")
is_trial: bool = Field(..., alias="isTrial")
model_config = ConfigDict(
str_strip_whitespace=True,
validate_assignment=True,
arbitrary_types_allowed=False,
populate_by_name=True,
use_enum_values=True,
)
class ApiError(BaseModel):
class ApiError(DomainBaseModel):
"""Data contract for ApiError."""
type_: ApiErrorType = Field(...)
@@ -89,16 +59,8 @@ class ApiError(BaseModel):
)
billing_error: BillingError | None = Field(alias="billingError", default=None)
model_config = ConfigDict(
str_strip_whitespace=True,
validate_assignment=True,
arbitrary_types_allowed=False,
populate_by_name=True,
use_enum_values=True,
)
class ClientAccount(BaseModel):
class ClientAccount(DomainBaseModel):
"""Data contract for ClientAccount."""
is_cloud: bool = Field(..., alias="isCloud")
@@ -110,27 +72,11 @@ class ClientAccount(BaseModel):
is_local_mode: bool = Field(..., alias="isLocalMode")
is_trial: bool = Field(..., alias="isTrial")
model_config = ConfigDict(
str_strip_whitespace=True,
validate_assignment=True,
arbitrary_types_allowed=False,
populate_by_name=True,
use_enum_values=True,
)
class ClientAuth(BaseModel):
class ClientAuth(DomainBaseModel):
"""Data contract for ClientAuth."""
org_id: str = Field(..., alias="orgId")
org_name: str = Field(..., alias="orgName")
org_is_trial: bool = Field(..., alias="orgIsTrial")
integrated_models_mode: bool = Field(..., alias="integratedModelsMode")
model_config = ConfigDict(
str_strip_whitespace=True,
validate_assignment=True,
arbitrary_types_allowed=False,
populate_by_name=True,
use_enum_values=True,
)
+38
View File
@@ -0,0 +1,38 @@
"""Shared base Pydantic model for the domain layer.
All domain models that share the standard configuration should inherit from
``DomainBaseModel`` instead of ``pydantic.BaseModel`` directly. This ensures
the shared ``model_config`` is defined in exactly one place and that any future
change to the configuration is automatically propagated to every consumer.
"""
from pydantic import BaseModel, ConfigDict
__all__ = ["DomainBaseModel"]
class DomainBaseModel(BaseModel):
"""Base class for all standard domain-layer Pydantic models.
Provides the shared ``model_config`` that was previously duplicated across
every model file in the domain layer:
- ``str_strip_whitespace``: leading/trailing whitespace is stripped from
all ``str`` fields on assignment and validation.
- ``validate_assignment``: field assignments after construction are
validated just like constructor arguments.
- ``arbitrary_types_allowed``: disabled — all field types must be
Pydantic-compatible to keep the domain layer clean.
- ``populate_by_name``: models can be constructed using either the Python
field name or the JSON alias.
- ``use_enum_values``: enum fields are stored and serialised as their
underlying primitive values rather than as enum instances.
"""
model_config = ConfigDict(
str_strip_whitespace=True,
validate_assignment=True,
arbitrary_types_allowed=False,
populate_by_name=True,
use_enum_values=True,
)
@@ -2,7 +2,9 @@
from enum import StrEnum
from pydantic import BaseModel, ConfigDict, Field
from pydantic import Field
from ..base import DomainBaseModel
class AutoModeType(StrEnum):
@@ -13,7 +15,7 @@ class AutoModeType(StrEnum):
GUIDED = "guided"
class PlanConfig(BaseModel):
class PlanConfig(DomainBaseModel):
"""Data contract for PlanConfig."""
auto_mode: AutoModeType = Field(..., alias="autoMode")
@@ -36,16 +38,8 @@ class PlanConfig(BaseModel):
auto_revert_on_rewind: bool = Field(..., alias="autoRevertOnRewind")
skip_changes_menu: bool = Field(..., alias="skipChangesMenu")
model_config = ConfigDict(
str_strip_whitespace=True,
validate_assignment=True,
arbitrary_types_allowed=False,
populate_by_name=True,
use_enum_values=True,
)
class ConfigSetting(BaseModel):
class ConfigSetting(DomainBaseModel):
"""Data contract for ConfigSetting."""
name: str = Field(...)
@@ -53,11 +47,3 @@ class ConfigSetting(BaseModel):
choices: list[str] | None = Field(default=None)
hascustomchoice: bool = Field(...)
sortkey: str = Field(...)
model_config = ConfigDict(
str_strip_whitespace=True,
validate_assignment=True,
arbitrary_types_allowed=False,
populate_by_name=True,
use_enum_values=True,
)