Skip to content
Closed
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
1 change: 1 addition & 0 deletions sdk/nexent/core/model_service/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Model service contracts and provider adapters."""
1 change: 1 addition & 0 deletions sdk/nexent/core/model_service/profiles/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Local model profile definitions, loading and resolution."""
131 changes: 131 additions & 0 deletions sdk/nexent/core/model_service/profiles/contracts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""Strict profile primitives, independent of provider clients and persistence."""

import json
from typing import Annotated, Literal, Self

from pydantic import BaseModel, ConfigDict, Field, StringConstraints, model_validator


SupportStatus = Literal["supported", "unsupported", "unknown"]
NonBlankString = Annotated[str, StringConstraints(pattern=r"\S")]
PositiveTokens = Annotated[int, Field(gt=0)]


class ProfileContract(BaseModel):
"""Reject undeclared fields and coercion at the profile boundary."""

model_config = ConfigDict(strict=True, extra="forbid", frozen=True)


class ContextCapabilities(ProfileContract):
"""Effective service limits; missing limits remain unknown."""

combined_window: PositiveTokens | None = None
input_limit: PositiveTokens | None = None
output_limit: PositiveTokens | None = None

@model_validator(mode="after")
def validate_limits(self) -> Self:
if self.combined_window is not None:
for limit in (self.input_limit, self.output_limit):
if limit is not None and limit > self.combined_window:
raise ValueError("Service limits cannot exceed the combined context window")
return self


class ToggleControl(ProfileContract):
"""Explicitly supported thinking switch values, not the current setting."""

mode: Literal["toggle"] = "toggle"
parameter_key: Literal["enable_thinking"] = "enable_thinking"
values: list[bool] = Field(min_length=1)

@model_validator(mode="after")
def validate_values(self) -> Self:
if len(set(self.values)) != len(self.values):
raise ValueError("Toggle values must be unique")
return self


class EffortControl(ProfileContract):
"""Provider-declared effort values; no universal effort enum is inferred."""

mode: Literal["effort"] = "effort"
parameter_key: Literal["reasoning_effort"] = "reasoning_effort"
values: list[NonBlankString] = Field(min_length=1)

@model_validator(mode="after")
def validate_values(self) -> Self:
if len(set(self.values)) != len(self.values):
raise ValueError("Effort values must be unique")
return self


class BudgetTokensControl(ProfileContract):
"""Token bounds and explicitly declared sentinel values."""

mode: Literal["budget_tokens"] = "budget_tokens"
parameter_key: Literal["reasoning_budget_tokens"] = "reasoning_budget_tokens"
min: Annotated[int, Field(ge=0)]
max: Annotated[int, Field(ge=0)]
special_values: list[int] = Field(default_factory=list)

@model_validator(mode="after")
def validate_range(self) -> Self:
if self.min > self.max:
raise ValueError("Budget minimum cannot exceed maximum")
if len(set(self.special_values)) != len(self.special_values):
raise ValueError("Special budget values must be unique")
return self


ReasoningControl = Annotated[ToggleControl | EffortControl | BudgetTokensControl, Field(discriminator="mode")]


class ReasoningCapabilities(ProfileContract):
"""Capability declarations with unknown distinct from unsupported."""

status: SupportStatus = "unknown"
controls: list[ReasoningControl] | None = None
response_paths: list[NonBlankString] = Field(default_factory=list)

@model_validator(mode="after")
def validate_controls(self) -> Self:
if self.status == "unsupported" and (self.controls or self.response_paths):
raise ValueError("Unsupported reasoning cannot declare controls or response paths")
if len(set(self.response_paths)) != len(self.response_paths):
raise ValueError("Reasoning response paths must be unique")
if self.controls is not None:
modes = [control.mode for control in self.controls]
keys = [control.parameter_key for control in self.controls]
if len(set(modes)) != len(modes) or len(set(keys)) != len(keys):
raise ValueError("Reasoning control modes and parameter keys must be unique")
encoded = json.dumps(
[control.model_dump(mode="json") for control in self.controls],
ensure_ascii=False,
separators=(",", ":"),
).encode("utf-8")
if len(encoded) > 64 * 1024:
raise ValueError("Reasoning controls exceed the 64 KiB UTF-8 limit")
return self


class AtomicProfileReference(ProfileContract):
"""Provenance only; runtime loading never recomposes from this reference."""

key: NonBlankString
catalog_revision: NonBlankString
file_path: NonBlankString
content_sha256: Annotated[str, StringConstraints(pattern=r"^[a-f0-9]{64}$")]

@model_validator(mode="after")
def validate_source_path(self) -> Self:
parts = self.file_path.split("/")
if (
"\\" in self.file_path
or ":" in self.file_path
or any(ord(character) < 32 for character in self.file_path)
or any(part in ("", ".", "..") for part in parts)
):
raise ValueError("Source paths must be canonical relative POSIX paths")
return self
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
schema_version: '1.0'
change:
change_id: MODEL-PROFILE-CONTRACTS-20261013
change_type: requirement
requirement_id: MODEL-PROFILE-CONTRACTS-20261013
title: Add strict model profile contract primitives
description: First local implementation slice of the approved model-access / NativeAgent SPEC; no existing runtime is connected or modified.
affected_features:
existing: []
added: [SDK-MODEL-PROFILE-CONTRACTS]
modified: []
retired: []
affected_cases:
existing: []
added: []
modified: []
retired: []
traditional_ut:
decision: add
behaviors:
- Unknown capacity remains null and known limits reject booleans, numeric strings, zero, negatives and service limits above the combined window.
- Toggle, effort and budget controls preserve explicitly declared values and reject duplicate modes/keys, invalid types, ranges, unknown fields and unsupported-with-controls.
- Source references retain exact identity/digests, reject unsafe paths and round-trip through strict JSON validation without loading external sources.
reason: These are isolated domain validation contracts; existing model UI/runtime Cases do not exercise them, and a duplicate formal D1 would add no component evidence. Catalog parsing/composition and live runtime verification belong to subsequent slices.
test_selectors:
- test/sdk/core/model_service/test_profile_contracts.py
no_case_change_reason: No API, provider, database, tool execution or user journey is connected in this slice. Preserve all historical Cases; formal integration coverage will be added with the owning implementation.
notes: Implements only profile primitives under SPEC AC-01/02. Overall feature acceptance remains pending; neither formal runtime acceptance nor model compatibility is claimed.
5 changes: 5 additions & 0 deletions test-e2e/features/SDK-MODEL-PROFILE-CONTRACTS/cases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# SDK-MODEL-PROFILE-CONTRACTS 关联用例

此页由结构化用例生成,不手工编辑。

当前测试基线未保留关联用例。
20 changes: 20 additions & 0 deletions test-e2e/features/SDK-MODEL-PROFILE-CONTRACTS/feature.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
schema_version: '1.0'
module:
id: SDK-MODEL-SERVICE
name: SDK model service
feature:
feature_id: SDK-MODEL-PROFILE-CONTRACTS
name: Model profile contract primitives
status: planned
description: Strict provider-neutral capacity, reasoning and source-reference contracts used by the upcoming catalog and outlet layers.
business_rules:
- rule_id: SDK-MODEL-PROFILE-CONTRACTS-RULE-01
description: Unknown capacity and capability stay distinct from explicit unsupported; known token limits are positive strict integers.
- rule_id: SDK-MODEL-PROFILE-CONTRACTS-RULE-02
description: Reasoning controls use declared toggle, effort or token-budget domains; duplicates, invalid types, inconsistent support and unknown fields fail validation.
- rule_id: SDK-MODEL-PROFILE-CONTRACTS-RULE-03
description: Atomic references carry exact source identity and digests with relative source paths; they are traceability, not runtime recomposition instructions.
required_test_stages: []
source:
requirement_ids: [MODEL-PROFILE-CONTRACTS-20261013]
notes: SPEC AC-01 and AC-02 subset only; the current slice has no catalog, persistence, provider or Agent runtime integration. Traditional UT proves these domain contracts. Later slices supply independent component/API/runtime evidence.
184 changes: 184 additions & 0 deletions test/sdk/core/model_service/test_profile_contracts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
"""AC-01/02: strict profile primitives, without provider or database I/O."""

import pytest
from nexent.core.model_service.profiles.contracts import (
AtomicProfileReference,
BudgetTokensControl,
ContextCapabilities,
EffortControl,
ReasoningCapabilities,
ToggleControl,
)
from pydantic import ValidationError


def test_context_unknown_and_independent_limits():
assert ContextCapabilities().model_dump() == {
"combined_window": None,
"input_limit": None,
"output_limit": None,
}
profile = ContextCapabilities(
combined_window=100, input_limit=100, output_limit=100
)
assert ContextCapabilities.model_validate_json(profile.model_dump_json()) == profile
assert ContextCapabilities(output_limit=1).combined_window is None


@pytest.mark.parametrize("value", [True, False, "200000", 1.0, 0, -1])
@pytest.mark.parametrize("field", ["combined_window", "input_limit", "output_limit"])
def test_context_rejects_invalid_token_limits(field, value):
with pytest.raises(ValidationError):
ContextCapabilities(**{field: value})


@pytest.mark.parametrize("field", ["input_limit", "output_limit"])
def test_context_rejects_service_limit_above_window(field):
with pytest.raises(ValidationError):
ContextCapabilities(combined_window=100, **{field: 101})


def test_reasoning_controls_preserve_declared_values_and_round_trip():
profile = ReasoningCapabilities(
status="supported",
controls=[
ToggleControl(values=[False, True]),
EffortControl(values=["low", "xhigh", "provider-specific"]),
BudgetTokensControl(min=0, max=100, special_values=[-1]),
],
response_paths=["choices[].delta.reasoning_content"],
)
assert (
ReasoningCapabilities.model_validate_json(profile.model_dump_json()) == profile
)
assert profile.controls[0].values[0] is False
assert profile.controls[2].special_values == [-1]
assert "current_value" not in profile.model_dump_json()


def test_reasoning_unknown_and_explicit_no_controls_remain_distinct():
assert ReasoningCapabilities().controls is None
assert ReasoningCapabilities(status="unsupported", controls=[]).controls == []
assert (
ReasoningCapabilities(controls=[ToggleControl(values=[True])]).status
== "unknown"
)


@pytest.mark.parametrize(
"control",
[
{"mode": "toggle", "values": [1]},
{"mode": "toggle", "values": [False, False]},
{"mode": "toggle", "values": []},
{"mode": "toggle", "values": [True], "parameter_key": "reasoning"},
{"mode": "effort", "values": ["high", "high"]},
{"mode": "effort", "values": [" "]},
{"mode": "effort", "values": [1]},
{"mode": "effort", "values": []},
{"mode": "budget_tokens", "min": 10, "max": 9},
{"mode": "budget_tokens", "min": -1, "max": 9},
{"mode": "budget_tokens", "min": 0, "max": True},
{"mode": "budget_tokens", "min": 0, "max": 9, "special_values": [False]},
{"mode": "budget_tokens", "min": 0, "max": 9, "special_values": [-1, -1]},
{"mode": "unknown"},
{"mode": "toggle", "values": [True], "unexpected": 1},
],
)
def test_reasoning_rejects_invalid_controls(control):
with pytest.raises(ValidationError):
ReasoningCapabilities(controls=[control])


@pytest.mark.parametrize(
"payload",
[
{"status": "unsupported", "controls": [{"mode": "toggle", "values": [True]}]},
{"status": "unsupported", "response_paths": ["reasoning_content"]},
{
"status": "supported",
"controls": [
{"mode": "effort", "values": ["low"]},
{"mode": "effort", "values": ["high"]},
],
},
{"response_paths": ["reasoning_content", "reasoning_content"]},
{"response_paths": [""]},
{"status": True},
{"status": "unknown", "extra": 1},
],
)
def test_reasoning_rejects_conflicting_or_unknown_fields(payload):
with pytest.raises(ValidationError):
ReasoningCapabilities(**payload)


def test_reasoning_size_limit_counts_utf8_bytes():
ReasoningCapabilities(controls=[EffortControl(values=["a" * 65000])])
with pytest.raises(ValidationError, match="64 KiB"):
ReasoningCapabilities(controls=[EffortControl(values=["思" * 22000])])


def reference_payload():
return {
"key": "qwen/Qwen3.8",
"catalog_revision": "catalog-v1",
"file_path": "models/qwen/model.toml",
"content_sha256": "a" * 64,
}


def test_atomic_reference_round_trip_preserves_exact_identity():
profile = AtomicProfileReference(**reference_payload())
assert (
AtomicProfileReference.model_validate_json(profile.model_dump_json()) == profile
)
assert profile.key == "qwen/Qwen3.8"


@pytest.mark.parametrize(
"path",
[
"/tmp/model.toml",
"../x.toml",
"models/../x.toml",
"models/./x.toml",
"models//x.toml",
"C:/x.toml",
"models\\x.toml",
"",
"models/",
"models/\x00file.toml",
],
)
def test_atomic_reference_rejects_unsafe_paths(path):
with pytest.raises(ValidationError):
AtomicProfileReference(**{**reference_payload(), "file_path": path})


@pytest.mark.parametrize(
"updates",
[
{"content_sha256": "a" * 63},
{"content_sha256": "A" * 64},
{"content_sha256": "g" * 64},
{"key": " "},
{"catalog_revision": ""},
{"unknown": "field"},
],
)
def test_atomic_reference_rejects_invalid_provenance(updates):
with pytest.raises(ValidationError):
AtomicProfileReference(**{**reference_payload(), **updates})


def test_context_rejects_unknown_fields():
with pytest.raises(ValidationError):
ContextCapabilities(max_tokens=100)


def test_contract_fields_cannot_be_reassigned_without_validation():
profile = ContextCapabilities(combined_window=100)
with pytest.raises(ValidationError):
profile.combined_window = 0
assert profile.combined_window == 100
Loading