"""Export the unreleased Core migration wire contract from its Pydantic owners. Run from the repository root with ``uv run --project api python api/dev/generate_model_billing_migration_contract.py``. This only writes static schemas/fixtures; it performs no migration, Stripe action or financial call. """ from __future__ import annotations import argparse import copy import json import sys from pathlib import Path from typing import Any API_ROOT = Path(__file__).resolve().parents[1] if str(API_ROOT) not in sys.path: sys.path.insert(0, str(API_ROOT)) from controllers.inner_api.model_billing_migration import ( # noqa: E402 - standalone CLI first adds the API import root CurrentMigrationStatusResponse, MigrationErrorResponse, MigrationOperationResult, MigrationStatusResponse, ) from services.entities.model_billing_migration import ( # noqa: E402 - standalone CLI first adds the API import root ActivateMigrationPayload, ClaimFreeMigrationPayload, CommandReceipt, ContractEventPayload, DecideCyclePayload, FreeGrantManifest, FundingReceiptsPayload, LeasePayload, MigrationWindow, PrepareMigrationPayload, WindowManifest, canonical_hash, ) MODELS = { "PrepareRequest": PrepareMigrationPayload, "DecideCycleRequest": DecideCyclePayload, "LeaseRequest": LeasePayload, "FundingReceiptsRequest": FundingReceiptsPayload, "ContractEventRequest": ContractEventPayload, "ActivateRequest": ActivateMigrationPayload, "Window": MigrationWindow, "WindowManifest": WindowManifest, "CommandReceipt": CommandReceipt, "OperationResult": MigrationOperationResult, "StatusResponse": MigrationStatusResponse, "ErrorResponse": MigrationErrorResponse, } # The paid v1 model map and emitted snapshot remain byte-for-byte frozen. Free # funding extends current status through a separate owner and additive contract. FREE_MODELS = { "ClaimFreeRequest": ClaimFreeMigrationPayload, "FreeGrantManifest": FreeGrantManifest, "FreeStatusResponse": CurrentMigrationStatusResponse, "FundingReceiptsRequest": FundingReceiptsPayload, "ActivateRequest": ActivateMigrationPayload, } def build_free_schema() -> dict[str, Any]: definitions: dict[str, Any] = {} for alias, model in FREE_MODELS.items(): schema = model.model_json_schema(ref_template="#/$defs/{model}") for name, definition in schema.pop("$defs", {}).items(): if name in definitions and definitions[name] != definition: raise ValueError(f"Conflicting free contract owner: {name}") definitions[name] = definition definitions[model.__name__] = schema if alias != model.__name__: definitions[alias] = {"$ref": "#/$defs/" + model.__name__} # Mirror the current status owner's cross-field validator without changing # the historical paid StatusResponse definition or its snapshot bytes. definitions["CurrentMigrationStatusResponse"]["not"] = {"required": ["manifest", "free_grant_manifest"]} return { "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Dify one-time free model-billing migration manifest v2", "x-contract-revision": "1.2", "$comment": ( "Additive free grant contract; HTTP api_version remains 1. Paid WindowManifest v1 is unchanged. " "Runtime additionally enforces exact grant identity/body hash, lease, row-lock/CAS, " "mutually exclusive funding kinds and verified readback before activation." ), "$defs": _wire_schema(definitions), } def free_fixture() -> dict[str, Any]: manifest = FreeGrantManifest( schema_version=2, funding_kind="one_time_trial_grant", tenant_id="11111111-1111-4111-8111-111111111111", migration_id="22222222-2222-4222-8222-222222222222", plan_revision=1, grant_id="trial.v1.11111111-1111-4111-8111-111111111111", external_ref="11111111-1111-4111-8111-111111111111", amount_usd_micro="250000", occurred_at="2026-09-02T01:02:03.456Z", amount_policy_version="signup-trial-v1", model_mapping_version="hosted-v1", ) data = manifest.model_dump(mode="json") return { "manifest": data, "manifest_hash": canonical_hash(data), "partner_payload": manifest.partner_payload(), "partner_payload_hash": canonical_hash(manifest.partner_payload()), "command_id": "grant:" + manifest.grant_id, "remote_id": manifest.grant_id, } def _wire_schema(value: Any) -> Any: """Omitted optionals are never explicit null on the actual wire. Request StrictPayload rejects supplied null; responses use exclude_none=True. Pydantic's internal optional defaults remain useful without advertising null as an accepted wire value. No additionalProperties prohibition is removed. """ if isinstance(value, list): return [_wire_schema(item) for item in value] if not isinstance(value, dict): return value result = {key: _wire_schema(item) for key, item in value.items() if not (key == "default" and item is None)} if "anyOf" in result: choices = [item for item in result["anyOf"] if item != {"type": "null"}] if len(choices) == 1: result.pop("anyOf") result.update(choices[0]) else: result["anyOf"] = choices return result def build_schema() -> dict[str, Any]: definitions: dict[str, Any] = {} for alias, model in MODELS.items(): schema = model.model_json_schema(ref_template="#/$defs/{model}") for name, definition in schema.pop("$defs", {}).items(): if name in definitions and definitions[name] != definition: raise ValueError(f"Conflicting Pydantic schema owner: {name}") definitions[name] = definition definitions[model.__name__] = schema if alias != model.__name__: definitions[alias] = {"$ref": "#/$defs/" + model.__name__} definitions = _wire_schema(definitions) # These mirror the owners' model_validator cross-field shape constraints; # date ordering, hashes, ownership and transactional checks remain runtime. definitions["DecideCyclePayload"]["allOf"] = [ { "if": {"properties": {"decision": {"const": "tokener"}}, "required": ["decision"]}, "then": {"required": ["manifest", "manifest_hash"]}, "else": {"not": {"anyOf": [{"required": ["manifest"]}, {"required": ["manifest_hash"]}]}}, } ] definitions["LeasePayload"]["allOf"] = [ { "if": {"properties": {"action": {"enum": ["renew", "release"]}}, "required": ["action"]}, "then": {"required": ["lease_token"]}, } ] definitions["ContractEventPayload"]["dependentRequired"] = { "replacement_manifest": ["replacement_manifest_hash"], "replacement_manifest_hash": ["replacement_manifest"], } definitions["ContractEventPayload"]["allOf"] = [ { "if": {"properties": {"terminal": {"const": True}}, "required": ["terminal"]}, "then": { "not": {"anyOf": [{"required": ["replacement_manifest"]}, {"required": ["replacement_manifest_hash"]}]}, "properties": { "command_payloads": { "items": { "properties": { "target_amount_usd_micro": {"const": "0"}, "delta_usd_micro": {"pattern": "^(0|-[1-9][0-9]*)$"}, } } } }, }, }, { "if": {"properties": {"affected_window_ids": {"minItems": 1}}, "required": ["affected_window_ids"]}, "then": {"required": ["command_payloads"], "properties": {"command_payloads": {"minItems": 1}}}, }, { "if": {"properties": {"manual_replan": {"const": True}}, "required": ["manual_replan"]}, "then": { "required": ["replacement_manifest", "replacement_manifest_hash"], "properties": {"event_type": {"const": "plan_change"}, "terminal": {"const": False}}, }, }, ] return { "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Dify model-billing migration API v1 — implementation contract revision 1.1", "x-contract-revision": "1.1", "$comment": ( "Generated from Pydantic request/response owners. API api_version remains 1 " "because this feature is unreleased. " "POST returns current authoritative status; replayed_operation is the immutable original result. " "Lease replay consumers must not adopt a successor token from current status. " "Runtime additionally enforces date ordering, hash equality, command ownership, terminal fences and CAS." ), "$defs": definitions, } def implementation_vectors(existing: list[dict[str, Any]]) -> list[dict[str, Any]]: """Preserve existing examples and add concrete Go-compatible v1.1 records.""" result = copy.deepcopy(existing) for vector in result: if vector.get("definition") == "StatusResponse" and vector.get("valid"): state = vector["value"] state.setdefault( "model_mapping_version", state.get("manifest", {}).get("model_mapping_version", "hosted-v1") ) state.setdefault("inventory_hash", "sha256:" + "1" * 64) state.setdefault("cycle_decisions", []) state.setdefault("ever_activated", state.get("phase") == "active") state.setdefault("contract_events", {}) state.setdefault("prior_manifests", []) state.setdefault("terminated_contract_refs", []) source = next(item["value"] for item in result if item.get("id") == "valid-tokener-decision") window = copy.deepcopy(source["manifest"]["windows"][0]) command = { "command_id": "dify.correction.v2.fixture", "window": window, "delta_usd_micro": "-5000000", "target_amount_usd_micro": "0", } command["payload_hash"] = canonical_hash(command) event = { "api_version": 1, "operation_id": "44444444-4444-4444-8444-444444444444", "expected_revision": 8, "business_event_ref": "re_fixture_actual_refund", "contract_ref": source["contract_ref"], "billing_receipt_ref": "receipt-fixture", "event_type": "refund", "terminal": True, "affected_window_ids": [window["window_id"]], "command_payloads": [command], } additions = [ ("implementation-terminal-frozen-command", "ContractEventRequest", True, event), ("implementation-reject-extra-event-field", "ContractEventRequest", False, {**event, "wallet_balance": "0"}), ( "implementation-reject-event-without-type", "ContractEventRequest", False, {k: v for k, v in event.items() if k != "event_type"}, ), ("implementation-reject-unfrozen-correction", "ContractEventRequest", False, {**event, "command_payloads": []}), ] state = copy.deepcopy(next(item["value"] for item in result if item.get("id") == "recoverable-status")) state.update( phase="blocked", blocked_from="active", ever_activated=True, pending_contract_event=event, contract_events={event["business_event_ref"]: event}, terminated_contract_refs=[event["contract_ref"]], ) additions.append(("implementation-status-with-pending-terminal-event", "StatusResponse", True, state)) replay = copy.deepcopy(state) replay.update( replayed=True, replayed_operation={ "operation_id": "55555555-5555-4555-8555-555555555555", "request_hash": "sha256:" + "2" * 64, "result_code": 200, "result_revision": 5, "result_summary": {"phase": "claimed", "route_epoch": 1}, "lease_action": "acquire", "lease_token": "66666666-6666-4666-8666-666666666666", "lease_expires_at": "2027-06-17T00:01:00Z", }, ) additions.append(("implementation-replayed-original-lease-receipt", "StatusResponse", True, replay)) result = [item for item in result if not item["id"].startswith("implementation-")] result.extend( {"id": name, "schema": "migration.schema.json", "definition": definition, "valid": valid, "value": value} for name, definition, valid, value in additions ) return result def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--output", type=Path, default=API_ROOT / "contracts/model-billing-migration-v1.schema.json") parser.add_argument("--research-pack", type=Path) parser.add_argument("--go-fixtures-dir", type=Path) parser.add_argument( "--free-output", type=Path, default=API_ROOT / "contracts/model-billing-migration-free-v2.schema.json" ) parser.add_argument( "--free-fixture-output", type=Path, default=API_ROOT / "contracts/fixtures/tokener_migration_free_grant_v2.json", ) parser.add_argument("--check", action="store_true") args = parser.parse_args() content = json.dumps(build_schema(), ensure_ascii=False, indent=2) + "\n" free_content = json.dumps(build_free_schema(), ensure_ascii=False, indent=2) + "\n" fixture_content = json.dumps(free_fixture(), ensure_ascii=False, indent=2) + "\n" if args.check: if not args.output.exists() or args.output.read_text() != content: raise SystemExit("Migration contract is stale; regenerate from its Pydantic owners") if not args.free_output.exists() or args.free_output.read_text() != free_content: raise SystemExit("Free migration contract is stale; regenerate from its Pydantic owners") if not args.free_fixture_output.exists() or args.free_fixture_output.read_text() != fixture_content: raise SystemExit("Free migration fixture is stale; regenerate from its Pydantic owners") return args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(content) args.free_output.parent.mkdir(parents=True, exist_ok=True) args.free_output.write_text(free_content) args.free_fixture_output.parent.mkdir(parents=True, exist_ok=True) args.free_fixture_output.write_text(fixture_content) if args.research_pack: (args.research_pack / "migration.schema.json").write_text(content) vectors = implementation_vectors(json.loads((args.research_pack / "schema-vectors.json").read_text())) (args.research_pack / "schema-vectors.json").write_text( json.dumps(vectors, ensure_ascii=False, indent=2) + "\n" ) destination = API_ROOT / "contracts/model-billing-migration-v1.fixtures.json" destination.write_text( json.dumps([v for v in vectors if v["schema"] == "migration.schema.json"], ensure_ascii=False, indent=2) + "\n" ) if args.go_fixtures_dir: fixtures = API_ROOT / "contracts/fixtures" fixtures.mkdir(parents=True, exist_ok=True) for name in ("tokener_migration_manifest_v1.json", "tokener_manual_replan_v1.json"): value = json.loads((args.go_fixtures_dir / name).read_text()) (fixtures / name).write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n") if __name__ == "__main__": main()