From 49c41b3af182db830e18a26d4c3ba9cd37318a43 Mon Sep 17 00:00:00 2001 From: Yansong Zhang <916125788@qq.com> Date: Tue, 10 Nov 2026 21:28:36 +0800 Subject: [PATCH] feat: support exact Gemini 0.9.13 credit source variants --- .../model_billing_gemini_v8_mapping.py | 159 +++++++++ ...billing_gemini_credit_source_v2_adapter.py | 317 ++++++++++++++++++ api/services/model_billing_mapping_service.py | 96 +++++- ...del_billing_migration_inventory_gateway.py | 86 +++-- api/services/model_billing_source_variants.py | 51 +++ .../unit/model_billing_gemini_v8_profiles.py | 104 ++++++ .../test_model_billing_gemini_v8_mapping.py | 304 +++++++++++++++++ .../gemini0913_source_profiles.packed.json | 1 + ...billing_gemini_credit_source_v2_adapter.py | 179 ++++++++++ ...del_billing_migration_inventory_gateway.py | 83 +++++ .../test_model_billing_source_variants.py | 178 ++++++++++ 11 files changed, 1526 insertions(+), 32 deletions(-) create mode 100644 api/services/entities/model_billing_gemini_v8_mapping.py create mode 100644 api/services/model_billing_gemini_credit_source_v2_adapter.py create mode 100644 api/tests/helpers/unit/model_billing_gemini_v8_profiles.py create mode 100644 api/tests/unit_tests/services/entities/test_model_billing_gemini_v8_mapping.py create mode 100644 api/tests/unit_tests/services/fixtures/gemini0913_source_profiles.packed.json create mode 100644 api/tests/unit_tests/services/test_model_billing_gemini_credit_source_v2_adapter.py diff --git a/api/services/entities/model_billing_gemini_v8_mapping.py b/api/services/entities/model_billing_gemini_v8_mapping.py new file mode 100644 index 00000000000..e74b723aa47 --- /dev/null +++ b/api/services/entities/model_billing_gemini_v8_mapping.py @@ -0,0 +1,159 @@ +"""Closed V8 Gemini source shapes, separate from immutable V6 declarations. + +This DTO validates declaration shape only. A syntactically valid digest is not +reviewed source, SDK/runtime, installed package or target capability evidence. +No registry dispatch, factory, credential, SYSTEM/BYOK or upgrade authority is +added here. Real 0913 contracts must be reviewed before this family is wired. +""" + +from dataclasses import dataclass +from typing import Annotated, Literal, Self + +from pydantic import Field, model_validator + +from services.entities.model_billing_gemini_mapping import ( + GEMINI_SOURCE_UID, + GeminiMappingSource, + GeminiMappingTarget, + GeminiSourceSettings, + gemini_source_operations, +) +from services.entities.model_billing_mapping import _StrictModel + +GEMINI_0913_SOURCE_UID = "langgenius/gemini:0.9.13@4378187cfc50d4253573f4d96baa13228d8ed2eb746ac3bae492a8eaee2da458" + +type GeminiV8SourceUID = Literal[ + "langgenius/gemini:0.9.12@9bb9eff3f9c99a8f1f87bdd5e0f178d5493918939dd0e02825ae8383bd0fd7ed", + "langgenius/gemini:0.9.13@4378187cfc50d4253573f4d96baa13228d8ed2eb746ac3bae492a8eaee2da458", +] + + +class GeminiV8SourceAdapter(_StrictModel): + id: Literal["gemini_credit_source_v1", "gemini_credit_source_v2"] + revision: Literal["1"] + rules_digest: str = Field(pattern=r"^sha256:[0-9a-f]{64}$") + + +class GeminiV8SourceVariant(_StrictModel): + variant_id: str = Field(min_length=1) + source_plugin_uid: GeminiV8SourceUID + source_settings: GeminiSourceSettings + source_schema_digest: str = Field(pattern=r"^sha256:[0-9a-f]{64}$") + source_runtime_schema_digest: str = Field(pattern=r"^sha256:[0-9a-f]{64}$") + source_profile_id: str = Field(min_length=1) + parameter_adapter: GeminiV8SourceAdapter + target: GeminiMappingTarget + operations: list[str] = Field(min_length=3, max_length=4) + conformance_fixture_ids: list[Annotated[str, Field(min_length=1)]] = Field(min_length=1) + + @model_validator(mode="after") + def closed_capability(self) -> Self: + prefix, adapter = ( + ("gemini0912-native-v1:", "gemini_credit_source_v1") + if self.source_plugin_uid == GEMINI_SOURCE_UID + else ("gemini0913-native-v1:", "gemini_credit_source_v2") + ) + if ( + self.source_profile_id != prefix + self.target.model + or self.parameter_adapter.id != adapter + or not self.source_settings.matches_model_type(self.target.model_type) + or set(self.operations) != gemini_source_operations(self.target.model) + or len(set(self.operations)) != len(self.operations) + or len(set(self.conformance_fixture_ids)) != len(self.conformance_fixture_ids) + ): + raise ValueError("Invalid bounded V8 Gemini source capability") + return self + + +class SourceGeminiV8VariantMapping(_StrictModel): + schema_version: Literal[8] + mapping_revision: str = Field(min_length=1) + source: GeminiMappingSource + settlement_owner: Literal["shared_credit_pool"] + pool_types: list[Literal["paid", "trial"]] = Field(min_length=1) + variants: list[GeminiV8SourceVariant] = Field(min_length=1, max_length=2) + + @model_validator(mode="before") + @classmethod + def exact_version(cls, value: object) -> object: + if type(value) is not dict or type(value.get("schema_version")) is not int or value["schema_version"] != 8: + raise ValueError("Gemini schema version must be integer 8") + return value + + @model_validator(mode="after") + def finite_exact_sources(self) -> Self: + if len(set(self.pool_types)) != len(self.pool_types): + raise ValueError("Duplicate V8 Gemini pools") + if len({item.variant_id for item in self.variants}) != len(self.variants): + raise ValueError("Duplicate V8 Gemini variant identity") + selectors = {(item.source_plugin_uid, item.source_settings.model_dump_json()) for item in self.variants} + if len(selectors) != len(self.variants): + raise ValueError("Ambiguous V8 Gemini source selector") + if any( + item.target.model != self.source.model or item.target.model_type is not self.source.model_type + for item in self.variants + ): + raise ValueError("V8 Gemini source/target identity changed") + return self + + +class SourceGeminiV8VariantSelection(_StrictModel): + schema_version: Literal[8] + variant_id: str = Field(min_length=1) + source_plugin_uid: GeminiV8SourceUID + source_settings: GeminiSourceSettings + source_schema_digest: str = Field(pattern=r"^sha256:[0-9a-f]{64}$") + source_runtime_schema_digest: str = Field(pattern=r"^sha256:[0-9a-f]{64}$") + registry_digest: str = Field(pattern=r"^sha256:[0-9a-f]{64}$") + + @model_validator(mode="before") + @classmethod + def exact_version(cls, value: object) -> object: + if type(value) is not dict or type(value.get("schema_version")) is not int or value["schema_version"] != 8: + raise ValueError("Gemini schema version must be integer 8") + return value + + +@dataclass(frozen=True, slots=True) +class SelectedGeminiV8SourceVariant: + """Future registry-selected wrapper; constructing it grants no authority.""" + + envelope: SourceGeminiV8VariantMapping + variant: GeminiV8SourceVariant + registry_digest: str + + @property + def mapping_revision(self) -> str: + return self.envelope.mapping_revision + + @property + def source(self) -> GeminiMappingSource: + return self.envelope.source + + @property + def settlement_owner(self) -> Literal["shared_credit_pool"]: + return self.envelope.settlement_owner + + @property + def pool_types(self) -> list[Literal["paid", "trial"]]: + return self.envelope.pool_types + + @property + def source_schema_digest(self) -> str: + return self.variant.source_schema_digest + + @property + def parameter_adapter(self) -> GeminiV8SourceAdapter: + return self.variant.parameter_adapter + + @property + def target(self) -> GeminiMappingTarget: + return self.variant.target + + @property + def operations(self) -> list[str]: + return self.variant.operations + + @property + def conformance_fixture_ids(self) -> list[str]: + return self.variant.conformance_fixture_ids diff --git a/api/services/model_billing_gemini_credit_source_v2_adapter.py b/api/services/model_billing_gemini_credit_source_v2_adapter.py new file mode 100644 index 00000000000..c52407cbfbb --- /dev/null +++ b/api/services/model_billing_gemini_credit_source_v2_adapter.py @@ -0,0 +1,317 @@ +"""Pure exact Gemini0913 source factory, separate from immutable0912/V6. + +Pins bind the frozen v2 packet fd9d7680 / raw8c32f42e to the actual Source +catalog/runtime Core entities. They prove no installed target, current SDK, +network, credential, migration, billing or release authority. None runtime +schema remains frozen/prospective restoration, never observed RPC evidence. +""" + +from __future__ import annotations + +import hmac +import re +from dataclasses import dataclass +from typing import Never + +from graphon.model_runtime.entities.model_entities import AIModelEntity, ModelPropertyKey, ModelType +from services.entities.model_billing_gemini_mapping import ( + GeminiSourceSettings, + gemini_source_model_type, +) +from services.entities.model_billing_gemini_v8_mapping import GEMINI_0913_SOURCE_UID +from services.entities.model_billing_mapping import SourceParameterContract, model_schema_digest + +GEMINI_CREDIT_SOURCE_ADAPTER_ID = "gemini_credit_source_v2" +GEMINI_CREDIT_SOURCE_ADAPTER_REVISION = "1" +GEMINI_CREDIT_SOURCE_PROVIDER = "langgenius/gemini/google" + + +class GeminiCreditSourceV2AdapterError(ValueError): + """Fixed safe denial; unknown settings/schema never authorize a route.""" + + +@dataclass(frozen=True, slots=True) +class GeminiCreditSourceV2Contract(SourceParameterContract): + """An exact immutable reviewed profile, not a routing authorization.""" + + +_REVIEWED_CONTRACTS = ( + GeminiCreditSourceV2Contract( + profile_id="gemini0913-native-v1:gemini-2.5-flash", + source_plugin_unique_identifier="langgenius/gemini:0.9.13@4378187cfc50d4253573f4d96baa13228d8ed2eb746ac3bae492a8eaee2da458", + source_model="gemini-2.5-flash", + source_schema_digest="sha256:9dfef26ae8e27359f74312f87ca295e08ec59f187c889d4fc5eed747f22682ef", + source_runtime_schema_digest="sha256:e3a9e5a6146b14480e5f9948993afeab62e640b1559bd953fea98651ee1d5a21", + target_model="gemini-2.5-flash", + rules_digest="sha256:13ca4eb8ba52db4537a9177114d1b41f5a468ee79ecbdbc65b6ad940979d79ce", + adapter_id="gemini_credit_source_v2", + algorithm_revision="1", + ), + GeminiCreditSourceV2Contract( + profile_id="gemini0913-native-v1:gemini-3.1-flash-lite", + source_plugin_unique_identifier="langgenius/gemini:0.9.13@4378187cfc50d4253573f4d96baa13228d8ed2eb746ac3bae492a8eaee2da458", + source_model="gemini-3.1-flash-lite", + source_schema_digest="sha256:26dc99d59bdab5b1ff139aad894dae0fc15c9baf06777af67b9462f113193504", + source_runtime_schema_digest="sha256:0ee6cea0716356f3d3951066ec994ba34b50cf48fecf8d07c9e9611861e627a4", + target_model="gemini-3.1-flash-lite", + rules_digest="sha256:53574a2d8920b48b3152f97fccc70217e831dfba10d60c97690f9862ac2123ce", + adapter_id="gemini_credit_source_v2", + algorithm_revision="1", + ), + GeminiCreditSourceV2Contract( + profile_id="gemini0913-native-v1:gemini-3.1-pro-preview", + source_plugin_unique_identifier="langgenius/gemini:0.9.13@4378187cfc50d4253573f4d96baa13228d8ed2eb746ac3bae492a8eaee2da458", + source_model="gemini-3.1-pro-preview", + source_schema_digest="sha256:9aceca99ac88649944f9d78102241853c8144d1a11f4c086bb5f27bc38766e34", + source_runtime_schema_digest="sha256:3a2ce7dce3477bd042314941826492a5496aeb8bdb047285514a305d810e918e", + target_model="gemini-3.1-pro-preview", + rules_digest="sha256:8fb951c69113cc92eedb5e564817c897804b151f089851e783b0fd2dcc68a3eb", + adapter_id="gemini_credit_source_v2", + algorithm_revision="1", + ), + GeminiCreditSourceV2Contract( + profile_id="gemini0913-native-v1:gemini-3.5-flash", + source_plugin_unique_identifier="langgenius/gemini:0.9.13@4378187cfc50d4253573f4d96baa13228d8ed2eb746ac3bae492a8eaee2da458", + source_model="gemini-3.5-flash", + source_schema_digest="sha256:fa5fbdb6fa5ef69496f62f6dc2f4ead6b34d82f26cb6fc793fabf25577b667aa", + source_runtime_schema_digest="sha256:54aefc309ce57ff15f93ee606bb0919f1eb9649d057957cb6ea1fb2b60a3ab44", + target_model="gemini-3.5-flash", + rules_digest="sha256:cd75ed07076e1eebc099e93ada8110754bfd95767f05b1099b688f54db61b1b3", + adapter_id="gemini_credit_source_v2", + algorithm_revision="1", + ), + GeminiCreditSourceV2Contract( + profile_id="gemini0913-native-v1:gemini-3.5-flash-lite", + source_plugin_unique_identifier="langgenius/gemini:0.9.13@4378187cfc50d4253573f4d96baa13228d8ed2eb746ac3bae492a8eaee2da458", + source_model="gemini-3.5-flash-lite", + source_schema_digest="sha256:230f022992a7882a800fc31d4b754ca51032a2342a4644ce250f58abc4bf07c9", + source_runtime_schema_digest="sha256:613acb7a29a51dffcf3ffd506f306ba6002599d5765a1baffb66d3ade9257355", + target_model="gemini-3.5-flash-lite", + rules_digest="sha256:601f024ab221e97fc8a80381e068ec3cd948704c57d88d722363ae59a22aad6f", + adapter_id="gemini_credit_source_v2", + algorithm_revision="1", + ), + GeminiCreditSourceV2Contract( + profile_id="gemini0913-native-v1:gemini-3.7-flash", + source_plugin_unique_identifier="langgenius/gemini:0.9.13@4378187cfc50d4253573f4d96baa13228d8ed2eb746ac3bae492a8eaee2da458", + source_model="gemini-3.7-flash", + source_schema_digest="sha256:c718fa257170b98e87fd7a0c00c56613cb611d4d18ad1256dd375c9eaf908676", + source_runtime_schema_digest="sha256:7b1cd0a9ea9fbe14386d8458a6bec68833343cdbf4383c460bf0ac2a20f68189", + target_model="gemini-3.7-flash", + rules_digest="sha256:48ea4bb566bf12eb497eea6e23c8360c1fc3635b9ba9582a829a82c2ed5cfe70", + adapter_id="gemini_credit_source_v2", + algorithm_revision="1", + ), + GeminiCreditSourceV2Contract( + profile_id="gemini0913-native-v1:gemini-3-flash-preview", + source_plugin_unique_identifier="langgenius/gemini:0.9.13@4378187cfc50d4253573f4d96baa13228d8ed2eb746ac3bae492a8eaee2da458", + source_model="gemini-3-flash-preview", + source_schema_digest="sha256:7a0371459f73f0ba41d20f0ef274923e131032931fb55c9865c7478b42a10f2b", + source_runtime_schema_digest="sha256:ccaa15b0d9e20840ab62d077c5c0a62fd9f5339d04f4d4ab0a978962ac83dab1", + target_model="gemini-3-flash-preview", + rules_digest="sha256:1d608a9072491a4e052a54b3447fce10cd0d6b1083a49fa70e37925736e20a28", + adapter_id="gemini_credit_source_v2", + algorithm_revision="1", + ), + GeminiCreditSourceV2Contract( + profile_id="gemini0913-native-v1:gemini-embedding-001", + source_plugin_unique_identifier="langgenius/gemini:0.9.13@4378187cfc50d4253573f4d96baa13228d8ed2eb746ac3bae492a8eaee2da458", + source_model="gemini-embedding-001", + source_schema_digest="sha256:470e3397c5097df3b54673901fc321ce298e6081369bc731432e042daa04fb5d", + source_runtime_schema_digest="sha256:470e3397c5097df3b54673901fc321ce298e6081369bc731432e042daa04fb5d", + target_model="gemini-embedding-001", + rules_digest="sha256:c2d37d9c263af1eeedce8e2c94cb648f995bf1c0c83cd9c7d69baf70bab05d4e", + adapter_id="gemini_credit_source_v2", + algorithm_revision="1", + ), + GeminiCreditSourceV2Contract( + profile_id="gemini0913-native-v1:gemini-embedding-2-preview", + source_plugin_unique_identifier="langgenius/gemini:0.9.13@4378187cfc50d4253573f4d96baa13228d8ed2eb746ac3bae492a8eaee2da458", + source_model="gemini-embedding-2-preview", + source_schema_digest="sha256:b16129e398a864dd0baee0c957c4c42573ef888b785c6aaec9d78ac2785e063b", + source_runtime_schema_digest="sha256:b16129e398a864dd0baee0c957c4c42573ef888b785c6aaec9d78ac2785e063b", + target_model="gemini-embedding-2-preview", + rules_digest="sha256:e13153f791fe3f15ea8cf2c7e71df72a76e2046e876d37c66e0e3fe2785a540a", + adapter_id="gemini_credit_source_v2", + algorithm_revision="1", + ), +) +_MARKER_FIELDS = frozenset( + { + "schema_version", + "profile_id", + "source_plugin_uid", + "source_model", + "source_schema_digest", + "rules_digest", + } +) + + +def _deny() -> Never: + raise GeminiCreditSourceV2AdapterError("Gemini source evidence is not reviewed") + + +def _digest(value: object) -> bool: + return type(value) is str and re.fullmatch(r"sha256:[0-9a-f]{64}", value) is not None + + +def validate_gemini_credit_source_identity( + *, + source_provider: str, + source_model: str, + source_plugin_unique_identifier: str, + model_type: ModelType, + target_model: str, +) -> None: + if ( + type(source_provider) is not str + or source_provider != GEMINI_CREDIT_SOURCE_PROVIDER + or type(source_plugin_unique_identifier) is not str + or source_plugin_unique_identifier != GEMINI_0913_SOURCE_UID + or type(source_model) is not str + or type(target_model) is not str + or source_model != target_model + ): + _deny() + try: + expected = gemini_source_model_type(source_model) + except ValueError: + _deny() + else: + if model_type is not expected: + _deny() + + +def _settings(settings: GeminiSourceSettings, model: str) -> None: + if type(settings) is not GeminiSourceSettings or not settings.matches_model_type(gemini_source_model_type(model)): + _deny() + + +def _schema_digest(schema: AIModelEntity, model: str) -> str: + if ( + not isinstance(schema, AIModelEntity) + or schema.model != model + or schema.model_type is not gemini_source_model_type(model) + or schema.model_type is ModelType.LLM + and schema.model_properties.get(ModelPropertyKey.MODE) != "chat" + ): + _deny() + return model_schema_digest(schema) + + +def _runtime_pin(contract: GeminiCreditSourceV2Contract, expected: str, observed: AIModelEntity | None) -> None: + if not _digest(expected) or not hmac.compare_digest(expected, contract.source_runtime_schema_digest or ""): + _deny() + # None is frozen/prospective restoration, NEVER observed runtime evidence. + if observed is not None and not hmac.compare_digest(_schema_digest(observed, contract.source_model), expected): + _deny() + + +def gemini_credit_source_marker( + contract: GeminiCreditSourceV2Contract, + *, + source_provider: str, + source_model: str, + model_type: ModelType, + target_model: str, +) -> dict[str, object]: + if type(contract) is not GeminiCreditSourceV2Contract or contract not in _REVIEWED_CONTRACTS: + _deny() + validate_gemini_credit_source_identity( + source_provider=source_provider, + source_model=source_model, + source_plugin_unique_identifier=contract.source_plugin_unique_identifier, + model_type=model_type, + target_model=target_model, + ) + if source_model != contract.source_model or target_model != contract.target_model: + _deny() + return { + "schema_version": 1, + "profile_id": contract.profile_id, + "source_plugin_uid": contract.source_plugin_unique_identifier, + "source_model": contract.source_model, + "source_schema_digest": contract.source_schema_digest, + "rules_digest": contract.rules_digest, + } + + +def build_gemini_credit_source_contract( + *, + adapter_id: str, + adapter_revision: str, + expected_rules_digest: str, + source_provider: str, + source_model: str, + source_plugin_unique_identifier: str, + source_schema: AIModelEntity, + expected_source_schema_digest: str, + expected_source_runtime_schema_digest: str, + target_model: str, + source_settings: GeminiSourceSettings, + source_runtime_schema: AIModelEntity | None = None, +) -> GeminiCreditSourceV2Contract: + if not isinstance(source_schema, AIModelEntity): + _deny() + validate_gemini_credit_source_identity( + source_provider=source_provider, + source_model=source_model, + source_plugin_unique_identifier=source_plugin_unique_identifier, + model_type=source_schema.model_type, + target_model=target_model, + ) + _settings(source_settings, source_model) + actual = _schema_digest(source_schema, source_model) + contract = next((row for row in _REVIEWED_CONTRACTS if row.source_model == source_model), None) + if ( + contract is None + or type(adapter_id) is not str + or adapter_id != GEMINI_CREDIT_SOURCE_ADAPTER_ID + or type(adapter_revision) is not str + or adapter_revision != GEMINI_CREDIT_SOURCE_ADAPTER_REVISION + or not _digest(expected_rules_digest) + or not _digest(expected_source_schema_digest) + or not hmac.compare_digest(expected_rules_digest, contract.rules_digest) + or not hmac.compare_digest(expected_source_schema_digest, actual) + or not hmac.compare_digest(actual, contract.source_schema_digest) + ): + _deny() + _runtime_pin(contract, expected_source_runtime_schema_digest, source_runtime_schema) + return contract + + +def restore_gemini_credit_source_contract( + marker: object, + *, + source_provider: str, + source_model: str, + model_type: ModelType, + target_model: str, + source_settings: GeminiSourceSettings, + expected_source_runtime_schema_digest: str, + source_runtime_schema: AIModelEntity | None = None, +) -> GeminiCreditSourceV2Contract: + if ( + type(marker) is not dict + or set(marker) != _MARKER_FIELDS + or type(marker.get("schema_version")) is not int + or marker.get("schema_version") != 1 + or any(type(value) is not str for key, value in marker.items() if key != "schema_version") + ): + _deny() + contract = next((row for row in _REVIEWED_CONTRACTS if row.profile_id == marker["profile_id"]), None) + if contract is None: + _deny() + expected = gemini_credit_source_marker( + contract, + source_provider=source_provider, + source_model=source_model, + model_type=model_type, + target_model=target_model, + ) + if marker != expected: + _deny() + _settings(source_settings, source_model) + _runtime_pin(contract, expected_source_runtime_schema_digest, source_runtime_schema) + return contract diff --git a/api/services/model_billing_mapping_service.py b/api/services/model_billing_mapping_service.py index 794541afc16..7c02a95d10d 100644 --- a/api/services/model_billing_mapping_service.py +++ b/api/services/model_billing_mapping_service.py @@ -43,6 +43,10 @@ from services.entities.model_billing_gemini_mapping import ( gemini_source_operations, ) from services.entities.model_billing_gemini_mapping import SelectedGeminiSourceVariant as SelectedGeminiSourceVariant # noqa: PLC0414 +from services.entities.model_billing_gemini_v8_mapping import GEMINI_0913_SOURCE_UID, SourceGeminiV8VariantMapping +from services.entities.model_billing_gemini_v8_mapping import ( + SelectedGeminiV8SourceVariant as SelectedGeminiV8SourceVariant, # noqa: PLC0414 +) from services.entities.model_billing_mapping import ModelMapping, SourceVariantSettings from services.entities.model_billing_mapping import SelectedSourceVariant as SelectedSourceVariant # noqa: PLC0414 from services.entities.model_billing_mapping import SourceParameterContract as SourceParameterContract # noqa: PLC0414 @@ -69,6 +73,16 @@ from services.model_billing_gemini_credit_source_adapter import ( gemini_credit_source_marker, restore_gemini_credit_source_contract, ) +from services.model_billing_gemini_credit_source_v2_adapter import GeminiCreditSourceV2Contract +from services.model_billing_gemini_credit_source_v2_adapter import ( + build_gemini_credit_source_contract as build_gemini_credit_v2_source_contract, +) +from services.model_billing_gemini_credit_source_v2_adapter import ( + gemini_credit_source_marker as gemini_credit_v2_source_marker, +) +from services.model_billing_gemini_credit_source_v2_adapter import ( + restore_gemini_credit_source_contract as restore_gemini_credit_v2_source_contract, +) from services.model_billing_migration_inventory import saved_dependency_inventory_policy from services.model_billing_openai_audio_adapter import ( OPENAI_AUDIO_SOURCE_UIDS, @@ -134,6 +148,7 @@ from services.model_billing_source_variants import ( AudioSourceVariantContext, EmbeddingSourceVariantContext, GeminiSourceVariantContext, + GeminiV8SourceVariantContext, MappingDeclaration, ModelMappingRevision, SelectedModelMapping, @@ -215,6 +230,22 @@ def build_source_adapter_contract( if tongyi_contract != restore_variant_source_contract(mapping): raise ValueError("Tongyi source variant evidence changed") return tongyi_contract + if isinstance(mapping, SelectedGeminiV8SourceVariant): + build_gemini_v8 = ( + build_gemini_credit_v2_source_contract + if mapping.variant.source_plugin_uid == GEMINI_0913_SOURCE_UID + else build_gemini_credit_source_contract + ) + gemini_v8_contract = build_gemini_v8( + **arguments, + source_schema=source_schema, + expected_source_runtime_schema_digest=mapping.variant.source_runtime_schema_digest, + target_model=mapping.target.model, + source_settings=mapping.variant.source_settings, + ) + if gemini_v8_contract != restore_variant_source_contract(mapping): + raise ValueError("V8 Gemini source variant evidence changed") + return gemini_v8_contract if isinstance(mapping, SelectedGeminiSourceVariant): gemini_contract = build_gemini_credit_source_contract( **arguments, @@ -348,6 +379,8 @@ def source_parameter_marker( } if isinstance(contract, TongyiCreditSourceContract): return tongyi_credit_source_marker(contract, **arguments, model_type=model_type) + if isinstance(contract, GeminiCreditSourceV2Contract): + return gemini_credit_v2_source_marker(contract, **arguments, model_type=model_type) if isinstance(contract, GeminiCreditSourceContract): return gemini_credit_source_marker(contract, **arguments, model_type=model_type) if isinstance(contract, OpenAIAudioSourceContract): @@ -639,7 +672,7 @@ def mapped_dynamic_source_candidates( def mapping_has_required_operations(mapping: SelectedModelMapping) -> bool: if isinstance(mapping, SelectedTongyiSourceVariant): return set(mapping.operations) == tongyi_source_operations(mapping.source.model) - if isinstance(mapping, SelectedGeminiSourceVariant): + if isinstance(mapping, (SelectedGeminiSourceVariant, SelectedGeminiV8SourceVariant)): return set(mapping.operations) == gemini_source_operations(mapping.source.model) required = {"model/schema", f"{mapping.source.model_type.value}/invoke"} if mapping.source.model_type == ModelType.LLM: @@ -1197,13 +1230,13 @@ def hosted_gemini_source_fact( ) -> HostedGeminiSourceFact | None: """Capture actual SYSTEM dictionary classifications, before substitution. - Source0912 LLMs use only the reviewed None/exact Google-root base, + Source0912/0913 LLMs use only the reviewed None/exact Google-root base, None/disabled metadata and None/empty file prefix. Embeddings do not consume these three fields. Nothing retains a key, raw URL or credential dictionary. """ if ( str(ModelProviderID(provider)) != "langgenius/gemini/google" - or source_uid != GEMINI_SOURCE_UID + or source_uid not in {GEMINI_SOURCE_UID, GEMINI_0913_SOURCE_UID} or type(credentials) is not dict ): return None @@ -1273,6 +1306,47 @@ def gemini_source_variant_context( return GeminiSourceVariantContext(source_uid, settings) +def gemini_v8_source_variant_context( + fact: HostedGeminiSourceFact | None, + *, + provider: str, + source_uid: str, + model_type: ModelType, + schema: AIModelEntity | None = None, +) -> GeminiV8SourceVariantContext | None: + if ( + type(fact) is not HostedGeminiSourceFact + or fact.provider != str(ModelProviderID(provider)) + or fact.plugin_unique_identifier != source_uid + or source_uid not in {GEMINI_SOURCE_UID, GEMINI_0913_SOURCE_UID} + or model_type not in {ModelType.LLM, ModelType.TEXT_EMBEDDING} + ): + return None + if schema is not None and [ + digest + for kind, model, digest in fact.catalog_schema_digests + if (kind, model) == (schema.model_type, schema.model) + ] != [model_schema_digest(schema)]: + return None + if model_type is ModelType.LLM: + if (fact.llm_source_base_policy, fact.llm_request_metadata_policy, fact.llm_file_url_policy) != ( + "unset_or_exact_google_root", + "system_false_only", + "actual_system_absent_or_empty_only", + ): + return None + settings = GeminiSourceSettings( + source_base_policy="unset_or_exact_google_root", + request_metadata_policy="system_false_only", + file_url_policy="actual_system_absent_or_empty_only", + ) + else: + settings = GeminiSourceSettings( + source_base_policy="not_used", request_metadata_policy="not_used", file_url_policy="not_used" + ) + return GeminiV8SourceVariantContext(source_uid, settings) + + def hosted_source_variant_fact( *, provider: str, source_uid: str, models: Sequence[AIModelEntity], credentials: object ) -> HostedSourceVariantFact | None: @@ -1370,6 +1444,18 @@ def restore_variant_source_contract( source_settings=mapping.variant.source_settings, expected_source_runtime_schema_digest=variant.source_runtime_schema_digest, ) + if isinstance(mapping, SelectedGeminiV8SourceVariant): + restore_gemini_v8 = ( + restore_gemini_credit_v2_source_contract + if mapping.variant.source_plugin_uid == GEMINI_0913_SOURCE_UID + else restore_gemini_credit_source_contract + ) + return restore_gemini_v8( + marker, + **arguments, + source_settings=mapping.variant.source_settings, + expected_source_runtime_schema_digest=variant.source_runtime_schema_digest, + ) if isinstance(mapping, SelectedGeminiSourceVariant): return restore_gemini_credit_source_contract( marker, @@ -1552,6 +1638,10 @@ def resolve_source_mapping( context = tongyi_source_variant_context( tongyi_fact, provider=identity[0], source_uid=source_uid, model_type=identity[1], schema=schema ) + elif isinstance(declaration, SourceGeminiV8VariantMapping): + context = gemini_v8_source_variant_context( + gemini_fact, provider=identity[0], source_uid=source_uid, model_type=identity[1], schema=schema + ) elif isinstance(declaration, SourceGeminiVariantMapping): context = gemini_source_variant_context( gemini_fact, provider=identity[0], source_uid=source_uid, model_type=identity[1], schema=schema diff --git a/api/services/model_billing_migration_inventory_gateway.py b/api/services/model_billing_migration_inventory_gateway.py index 65d9810b090..18246375bee 100644 --- a/api/services/model_billing_migration_inventory_gateway.py +++ b/api/services/model_billing_migration_inventory_gateway.py @@ -36,11 +36,12 @@ from core.plugin.entities.plugin_daemon import PluginInstallTaskStatus from core.plugin.impl.model import PluginModelClient from core.plugin.impl.plugin import PluginInstaller from extensions import ext_hosting_provider -from graphon.model_runtime.entities.model_entities import ModelPropertyKey, ModelType +from graphon.model_runtime.entities.model_entities import AIModelEntity, ModelPropertyKey, ModelType from models.provider_ids import ModelProviderID from repositories.model_billing_dependency_repository import ModelBillingDependencyRepository from services.entities.model_billing_embedding_mapping import SelectedEmbeddingSourceVariant from services.entities.model_billing_gemini_mapping import SelectedGeminiSourceVariant +from services.entities.model_billing_gemini_v8_mapping import SelectedGeminiV8SourceVariant from services.entities.model_billing_mapping import SourceParameterContract from services.entities.model_billing_tongyi_mapping import SelectedTongyiSourceVariant from services.model_billing_dependency_inventory import DependencyInventoryPolicy, SavedModelReference @@ -73,7 +74,7 @@ from services.model_billing_mapping_service import ( source_parameter_public_settings, ) from services.model_billing_migration_inventory import RequiredSavedCreditModel, SavedDependencySourceEvidence -from services.model_billing_source_variants import selection_payload +from services.model_billing_source_variants import SelectedModelMapping, selection_payload class RedirectCapabilities(BaseModel): @@ -574,21 +575,7 @@ class SavedDependencyMigrationEvidenceGateway: ) if schema is None or schema.model_type != kind: raise ModelRouteUnmapped - if ( - isinstance( - mapping, - ( - SelectedAudioSourceVariant, - SelectedXAISourceVariant, - SelectedGeminiSourceVariant, - SelectedTongyiSourceVariant, - ), - ) - and schema.model != mapping.target.model - ): - # These closed families have no alias exception. Do not erase - # an unexpected target identity by normalizing the hash below. - raise ModelRouteUnmapped + _assert_target_source_identity(mapping, schema) if mapping.parameter_adapter.id == "openai_credit_llm_source_v2" and schema.model not in { mapping.source.model, mapping.target.model, @@ -612,21 +599,62 @@ class SavedDependencyMigrationEvidenceGateway: # Its exact versioned runtime schema was checked above; selected # source rows still retain the original dimension-contract check. # Neither path maps or infers a vector width from a model name. - if ( - kind == ModelType.TEXT_EMBEDDING - and not ( - isinstance( - mapping, - (SelectedEmbeddingSourceVariant, SelectedGeminiSourceVariant, SelectedTongyiSourceVariant), - ) - and prospective_only - ) - and source["embedding_dimensions"] != embedding_dimensions_contract(schema) - ): - raise ModelRouteUnmapped + _assert_target_embedding_dimensions( + mapping=mapping, + kind=kind, + prospective_only=prospective_only, + source=source, + schema=schema, + ) return expected +def _assert_target_source_identity(mapping: SelectedModelMapping, schema: AIModelEntity) -> None: + """Closed families have no alias exception; never normalize a wrong target.""" + if ( + isinstance( + mapping, + ( + SelectedAudioSourceVariant, + SelectedXAISourceVariant, + SelectedGeminiSourceVariant, + SelectedGeminiV8SourceVariant, + SelectedTongyiSourceVariant, + ), + ) + and schema.model != mapping.target.model + ): + raise ModelRouteUnmapped + + +def _assert_target_embedding_dimensions( + *, + mapping: SelectedModelMapping, + kind: ModelType, + prospective_only: bool, + source: Mapping[str, object], + schema: AIModelEntity, +) -> None: + """Only prospective closed declarations omit the installed-source dimension fact.""" + if ( + kind == ModelType.TEXT_EMBEDDING + and not ( + isinstance( + mapping, + ( + SelectedEmbeddingSourceVariant, + SelectedGeminiSourceVariant, + SelectedGeminiV8SourceVariant, + SelectedTongyiSourceVariant, + ), + ) + and prospective_only + ) + and source["embedding_dimensions"] != embedding_dimensions_contract(schema) + ): + raise ModelRouteUnmapped + + def _name(plugin_id: str, provider: str) -> str: return str(ModelProviderID(provider if provider.count("/") == 2 else plugin_id + "/" + provider)) diff --git a/api/services/model_billing_source_variants.py b/api/services/model_billing_source_variants.py index a49cea183d5..1f448e951fb 100644 --- a/api/services/model_billing_source_variants.py +++ b/api/services/model_billing_source_variants.py @@ -41,6 +41,11 @@ from services.entities.model_billing_gemini_mapping import ( SourceGeminiVariantMapping, SourceGeminiVariantSelection, ) +from services.entities.model_billing_gemini_v8_mapping import ( + SelectedGeminiV8SourceVariant, + SourceGeminiV8VariantMapping, + SourceGeminiV8VariantSelection, +) from services.entities.model_billing_mapping import ( ModelMapping, SelectedSourceVariant, @@ -70,6 +75,7 @@ type MappingDeclaration = ( | SourceAudioVariantMapping | SourceXAIVariantMapping | SourceGeminiVariantMapping + | SourceGeminiV8VariantMapping | SourceTongyiVariantMapping ) type SelectedSourceVariantMapping = ( @@ -78,6 +84,7 @@ type SelectedSourceVariantMapping = ( | SelectedAudioSourceVariant | SelectedXAISourceVariant | SelectedGeminiSourceVariant + | SelectedGeminiV8SourceVariant | SelectedTongyiSourceVariant ) type SelectedModelMapping = ModelMapping | SelectedSourceVariantMapping @@ -87,6 +94,7 @@ type SourceVariantSelectionPayload = ( | SourceAudioVariantSelection | SourceXAIVariantSelection | SourceGeminiVariantSelection + | SourceGeminiV8VariantSelection | SourceTongyiVariantSelection ) @@ -96,6 +104,7 @@ SOURCE_VARIANT_DECLARATION_TYPES = ( SourceAudioVariantMapping, SourceXAIVariantMapping, SourceGeminiVariantMapping, + SourceGeminiV8VariantMapping, SourceTongyiVariantMapping, ) SELECTED_SOURCE_VARIANT_TYPES = ( @@ -104,6 +113,7 @@ SELECTED_SOURCE_VARIANT_TYPES = ( SelectedAudioSourceVariant, SelectedXAISourceVariant, SelectedGeminiSourceVariant, + SelectedGeminiV8SourceVariant, SelectedTongyiSourceVariant, ) @@ -162,6 +172,14 @@ class GeminiSourceVariantContext: settings: GeminiSourceSettings +@dataclass(frozen=True, slots=True) +class GeminiV8SourceVariantContext: + """Exact V8 facts; never accepted by the immutable V6 selector.""" + + source_plugin_uid: str + settings: GeminiSourceSettings + + @dataclass(frozen=True, slots=True) class TongyiSourceVariantContext: source_plugin_uid: str @@ -174,6 +192,7 @@ type SourceContext = ( | AudioSourceVariantContext | XAISourceVariantContext | GeminiSourceVariantContext + | GeminiV8SourceVariantContext | TongyiSourceVariantContext ) @@ -233,6 +252,17 @@ class ModelMappingRevision: if len(tongyi_matches) != 1: raise ValueError("Unknown or ambiguous Tongyi source variant") selected = SelectedTongyiSourceVariant(declaration, tongyi_matches[0], self.digest) + elif isinstance(declaration, SourceGeminiV8VariantMapping): + if type(context) is not GeminiV8SourceVariantContext: + raise ValueError("Source V8 Gemini fact family changed") + gemini_v8_matches = [ + item + for item in declaration.variants + if item.source_plugin_uid == context.source_plugin_uid and item.source_settings == context.settings + ] + if len(gemini_v8_matches) != 1: + raise ValueError("Unknown or ambiguous V8 Gemini source variant") + selected = SelectedGeminiV8SourceVariant(declaration, gemini_v8_matches[0], self.digest) elif isinstance(declaration, SourceGeminiVariantMapping): if type(context) is not GeminiSourceVariantContext: raise ValueError("Source Gemini fact family changed") @@ -305,6 +335,11 @@ class ModelMappingRevision: SelectedTongyiSourceVariant(declaration, variant, self.digest) for variant in sorted(declaration.variants, key=lambda item: item.variant_id) ) + elif isinstance(declaration, SourceGeminiV8VariantMapping): + result.extend( + SelectedGeminiV8SourceVariant(declaration, variant, self.digest) + for variant in sorted(declaration.variants, key=lambda item: item.variant_id) + ) elif isinstance(declaration, SourceGeminiVariantMapping): result.extend( SelectedGeminiSourceVariant(declaration, variant, self.digest) @@ -344,6 +379,11 @@ class ModelMappingRevision: selected = self.resolve( identity, context=TongyiSourceVariantContext(record.source_plugin_uid, record.source_settings) ) + elif isinstance(declaration, SourceGeminiV8VariantMapping): + record = SourceGeminiV8VariantSelection.model_validate(payload) + selected = self.resolve( + identity, context=GeminiV8SourceVariantContext(record.source_plugin_uid, record.source_settings) + ) elif isinstance(declaration, SourceGeminiVariantMapping): record = SourceGeminiVariantSelection.model_validate(payload) selected = self.resolve( @@ -390,6 +430,16 @@ def selection_payload( source_runtime_schema_digest=selected.variant.source_runtime_schema_digest, registry_digest=selected.registry_digest, ) + if isinstance(selected, SelectedGeminiV8SourceVariant): + return SourceGeminiV8VariantSelection( + schema_version=8, + variant_id=selected.variant.variant_id, + source_plugin_uid=selected.variant.source_plugin_uid, + source_settings=selected.variant.source_settings, + source_schema_digest=selected.variant.source_schema_digest, + source_runtime_schema_digest=selected.variant.source_runtime_schema_digest, + registry_digest=selected.registry_digest, + ) if isinstance(selected, SelectedGeminiSourceVariant): return SourceGeminiVariantSelection( schema_version=6, @@ -456,6 +506,7 @@ def source_mapping_revision( | SourceAudioVariantMapping | SourceXAIVariantMapping | SourceGeminiVariantMapping + | SourceGeminiV8VariantMapping | SourceTongyiVariantMapping, Field(discriminator="schema_version"), ] diff --git a/api/tests/helpers/unit/model_billing_gemini_v8_profiles.py b/api/tests/helpers/unit/model_billing_gemini_v8_profiles.py new file mode 100644 index 00000000000..4524e0cc9ac --- /dev/null +++ b/api/tests/helpers/unit/model_billing_gemini_v8_profiles.py @@ -0,0 +1,104 @@ +"""Real frozen Source profiles; target/revision strings are unit-only, never release authority.""" + +import base64 +import gzip +import hashlib +import json +from itertools import starmap +from pathlib import Path +from typing import Any, cast + +from graphon.model_runtime.entities.model_entities import AIModelEntity, ModelType +from services.entities.model_billing_gemini_mapping import GEMINI_SOURCE_UID, gemini_source_operations +from services.entities.model_billing_gemini_v8_mapping import GEMINI_0913_SOURCE_UID +from tests.helpers.unit.model_billing_gemini_profiles import GeminiProfile +from tests.helpers.unit.model_billing_gemini_profiles import profiles as profiles0912 + +SOURCE_PROVIDER = "langgenius/gemini/google" +REVISION = "offline-gemini-v8-never-published" +SOURCE_UIDS = (GEMINI_SOURCE_UID, GEMINI_0913_SOURCE_UID) + + +def profiles(source_uid: str) -> list[GeminiProfile]: + if source_uid == GEMINI_SOURCE_UID: + return profiles0912() + if source_uid != GEMINI_0913_SOURCE_UID: + raise ValueError("Unknown exact source UID") + path = Path(__file__).resolve().parents[2] / "unit_tests/services/fixtures/gemini0913_source_profiles.packed.json" + blob = path.read_bytes() + if hashlib.sha256(blob).hexdigest() != "fd9d76809e7b973da9a1f5081db4880321e88514618ad262459942afef079f81": + raise ValueError("Frozen0913 packet changed") + packed = json.loads(blob) + raw = gzip.decompress(base64.b64decode(packed["payload_base64"], validate=True)) + if hashlib.sha256(raw).hexdigest() != packed["raw_sha256"] or packed["raw_sha256"] != ( + "8c32f42e9a623fae60f0da32633425938fb6557c4c93ea6975b7f9714dfb3dd7" + ): + raise ValueError("Frozen0913 raw profile changed") + rows = cast(list[GeminiProfile], json.loads(raw)["profiles"]) + if len(rows) != 9 or any(row["adapter_id"] != "gemini_credit_source_v2" for row in rows): + raise ValueError("Frozen0913 profile set changed") + return rows + + +def schema(row: GeminiProfile, *, runtime: bool = False) -> AIModelEntity: + return AIModelEntity.model_validate(row["source_daemon_schema" if runtime else "source_catalog_schema"]) + + +def profile_digest(row: GeminiProfile) -> str: + return ( + "sha256:" + + hashlib.sha256( + json.dumps(row, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode() + ).hexdigest() + ) + + +def variant_record(row: GeminiProfile, source_uid: str) -> dict[str, Any]: + kind = ModelType(row["model_type"]) + return { + "variant_id": row["profile_id"], + "source_plugin_uid": source_uid, + "source_settings": row["source_settings"], + "source_schema_digest": row["source_schema_digest"], + "source_runtime_schema_digest": row["source_runtime_schema_digest"], + "source_profile_id": row["profile_id"], + "parameter_adapter": { + "id": row["adapter_id"], + "revision": row["adapter_revision"], + "rules_digest": profile_digest(row), + }, + "target": { + "plugin_id": "langgenius/tokener", + "provider": "tokener", + "model_type": kind.value, + "model": row["target_model"], + "catalog_revision": "unit-only-no-target-release", + "minimum_plugin_version": "999.0.0.dev1", + }, + "operations": sorted(gemini_source_operations(row["source_model"])), + "conformance_fixture_ids": ["core-unit-source-profile:" + row["profile_id"]], + } + + +def mapping_record(model: str, source_uids: tuple[str, ...] = SOURCE_UIDS) -> dict[str, Any]: + rows = [next(row for row in profiles(uid) if row["source_model"] == model) for uid in source_uids] + if not rows: + raise ValueError("Source profiles required") + kind = ModelType(rows[0]["model_type"]) + return { + "schema_version": 8, + "mapping_revision": REVISION, + "source": { + "provider_id": SOURCE_PROVIDER, + "model_type": kind.value, + "model": model, + "mode": "chat" if kind is ModelType.LLM else "text-embedding", + }, + "settlement_owner": "shared_credit_pool", + "pool_types": ["paid", "trial"], + "variants": list(starmap(variant_record, zip(rows, source_uids, strict=True))), + } + + +def registry(source_uids: tuple[str, ...] = SOURCE_UIDS) -> str: + return json.dumps([mapping_record(row["source_model"], source_uids) for row in profiles(SOURCE_UIDS[0])]) diff --git a/api/tests/unit_tests/services/entities/test_model_billing_gemini_v8_mapping.py b/api/tests/unit_tests/services/entities/test_model_billing_gemini_v8_mapping.py new file mode 100644 index 00000000000..493b4775e73 --- /dev/null +++ b/api/tests/unit_tests/services/entities/test_model_billing_gemini_v8_mapping.py @@ -0,0 +1,304 @@ +"""Synthetic V8 shapes only; never a reviewed SDK/profile or release packet.""" + +import hashlib +import json +from copy import deepcopy +from dataclasses import FrozenInstanceError +from typing import cast + +import pytest +from pydantic import ValidationError +from typing_extensions import TypedDict + +from services.entities.model_billing_gemini_mapping import ( + GEMINI_SOURCE_EMBEDDING_MODELS, + GEMINI_SOURCE_LLM_MODELS, + GEMINI_SOURCE_UID, + SourceGeminiVariantMapping, + gemini_source_model_type, + gemini_source_operations, +) +from services.entities.model_billing_gemini_v8_mapping import ( + GEMINI_0913_SOURCE_UID, + SelectedGeminiV8SourceVariant, + SourceGeminiV8VariantMapping, + SourceGeminiV8VariantSelection, +) +from tests.helpers.unit.model_billing_gemini_profiles import profiles, variant_record + + +class SyntheticAdapter(TypedDict): + id: str + revision: str + rules_digest: str + + +class SyntheticTarget(TypedDict): + plugin_id: str + provider: str + model_type: str + model: str + catalog_revision: str + minimum_plugin_version: str + + +class SyntheticVariant(TypedDict): + variant_id: str + source_plugin_uid: str + source_settings: dict[str, str] + source_schema_digest: str + source_runtime_schema_digest: str + source_profile_id: str + parameter_adapter: SyntheticAdapter + target: SyntheticTarget + operations: list[str] + conformance_fixture_ids: list[str] + + +class SyntheticRecord(TypedDict): + schema_version: int + mapping_revision: str + source: dict[str, str] + settlement_owner: str + pool_types: list[str] + variants: list[SyntheticVariant] + + +def synthetic_record(model: str = "gemini-2.5-flash") -> SyntheticRecord: + kind = gemini_source_model_type(model) + llm = model in GEMINI_SOURCE_LLM_MODELS + settings = ( + { + "source_base_policy": "unset_or_exact_google_root", + "request_metadata_policy": "system_false_only", + "file_url_policy": "actual_system_absent_or_empty_only", + } + if llm + else {"source_base_policy": "not_used", "request_metadata_policy": "not_used", "file_url_policy": "not_used"} + ) + return { + "schema_version": 8, + "mapping_revision": "synthetic-v8-shape-not-published", + "source": { + "provider_id": "langgenius/gemini/google", + "model_type": kind.value, + "model": model, + "mode": "chat" if llm else "text-embedding", + }, + "settlement_owner": "shared_credit_pool", + "pool_types": ["paid", "trial"], + "variants": [ + { + "variant_id": f"synthetic-{version}-{model}", + "source_plugin_uid": uid, + "source_settings": deepcopy(settings), + # Syntax examples, not real source or target digest evidence. + "source_schema_digest": "sha256:" + "a" * 64, + "source_runtime_schema_digest": "sha256:" + "b" * 64, + "source_profile_id": f"gemini{version}-native-v1:{model}", + "parameter_adapter": {"id": adapter, "revision": "1", "rules_digest": "sha256:" + "c" * 64}, + "target": { + "plugin_id": "langgenius/tokener", + "provider": "tokener", + "model_type": kind.value, + "model": model, + "catalog_revision": "synthetic-not-a-live-catalog", + "minimum_plugin_version": "0.0.0.dev0", + }, + "operations": sorted(gemini_source_operations(model)), + "conformance_fixture_ids": ["synthetic-shape-only"], + } + for version, uid, adapter in ( + ("0912", GEMINI_SOURCE_UID, "gemini_credit_source_v1"), + ("0913", GEMINI_0913_SOURCE_UID, "gemini_credit_source_v2"), + ) + ], + } + + +def selection(record: SyntheticRecord) -> dict[str, object]: + row = record["variants"][0] + return { + "schema_version": 8, + **{ + key: row[key] + for key in ( + "variant_id", + "source_plugin_uid", + "source_settings", + "source_schema_digest", + "source_runtime_schema_digest", + ) + }, + "registry_digest": "sha256:" + "d" * 64, + } + + +@pytest.mark.parametrize("model", sorted(GEMINI_SOURCE_LLM_MODELS | GEMINI_SOURCE_EMBEDDING_MODELS)) +def test_exact_two_uid_shapes_and_each_single_variant_round_trip(model: str) -> None: + raw = synthetic_record(model) + parsed = SourceGeminiV8VariantMapping.model_validate(raw) + assert parsed.model_dump(mode="json") == raw + assert SourceGeminiV8VariantMapping.model_validate_json(parsed.model_dump_json()) == parsed + for row in raw["variants"]: + one: SyntheticRecord = {**raw, "variants": [row]} + assert ( + SourceGeminiV8VariantMapping.model_validate(one).variants[0].source_plugin_uid == row["source_plugin_uid"] + ) + frozen = SourceGeminiV8VariantSelection.model_validate(selection(one)) + assert SourceGeminiV8VariantSelection.model_validate_json(frozen.model_dump_json()) == frozen + + +@pytest.mark.parametrize("value", [True, False, 8.0, "8", 6, None]) +def test_mapping_and_selection_require_integer_eight_without_coercion(value: object) -> None: + raw = synthetic_record() + with pytest.raises(ValidationError, match="integer 8"): + SourceGeminiV8VariantMapping.model_validate({**raw, "schema_version": value}) + with pytest.raises(ValidationError, match="integer 8"): + SourceGeminiV8VariantSelection.model_validate({**selection(raw), "schema_version": value}) + + +@pytest.mark.parametrize("mutation", ["none", "three", "variant_id", "selector", "pools"]) +def test_finite_variant_and_selector_uniqueness(mutation: str) -> None: + raw = synthetic_record() + if mutation == "none": + raw["variants"] = [] + elif mutation == "three": + raw["variants"].append(deepcopy(raw["variants"][0])) + elif mutation == "variant_id": + raw["variants"][1]["variant_id"] = raw["variants"][0]["variant_id"] + elif mutation == "selector": + duplicate = deepcopy(raw["variants"][0]) + duplicate["variant_id"] = "different-id-same-selector" + raw["variants"][1] = duplicate + else: + raw["pool_types"] = ["trial", "trial"] + with pytest.raises(ValidationError): + SourceGeminiV8VariantMapping.model_validate(raw) + + +@pytest.mark.parametrize("index", [0, 1]) +@pytest.mark.parametrize("field", ["uid", "profile", "adapter", "revision"]) +def test_uid_profile_and_adapter_identity_cannot_be_crossed(index: int, field: str) -> None: + raw = synthetic_record() + item, other = raw["variants"][index], raw["variants"][1 - index] + if field == "uid": + item["source_plugin_uid"] = other["source_plugin_uid"] + elif field == "profile": + item["source_profile_id"] = other["source_profile_id"] + elif field == "adapter": + item["parameter_adapter"]["id"] = other["parameter_adapter"]["id"] + else: + item["parameter_adapter"]["revision"] = "2" + with pytest.raises(ValidationError): + SourceGeminiV8VariantMapping.model_validate(raw) + + +@pytest.mark.parametrize("where", ["root", "source", "variant", "settings", "adapter", "target"]) +def test_extra_fields_cannot_add_origin_credentials_or_byok_authority(where: str) -> None: + raw = synthetic_record() + row = raw["variants"][0] + target = cast( + dict[str, object], + { + "root": raw, + "source": raw["source"], + "variant": row, + "settings": row["source_settings"], + "adapter": row["parameter_adapter"], + "target": row["target"], + }[where], + ) + target["byok_or_origin_override"] = "not-a-routing-authority" + with pytest.raises(ValidationError, match="Extra inputs"): + SourceGeminiV8VariantMapping.model_validate(raw) + + +@pytest.mark.parametrize( + "mutation", + [ + "provider", + "unknown_uid", + "source_type", + "source_mode", + "target_type", + "target_model", + "settings", + "missing_op", + "extra_op", + "duplicate_op", + "duplicate_fixture", + "bool_digest", + ], +) +@pytest.mark.parametrize("model", ["gemini-2.5-flash", "gemini-embedding-001", "gemini-embedding-2-preview"]) +def test_model_type_mode_operations_and_settings_remain_closed(mutation: str, model: str) -> None: + raw = synthetic_record(model) + row = raw["variants"][0] + if mutation == "provider": + raw["source"]["provider_id"] = "langgenius/openai/openai" + elif mutation == "unknown_uid": + row["source_plugin_uid"] = GEMINI_SOURCE_UID.replace("0.9.12", "0.9.13") + elif mutation == "source_type": + raw["source"]["model_type"] = "text-embedding" if model in GEMINI_SOURCE_LLM_MODELS else "llm" + elif mutation == "source_mode": + raw["source"]["mode"] = "text-embedding" if model in GEMINI_SOURCE_LLM_MODELS else "chat" + elif mutation == "target_type": + row["target"]["model_type"] = "text-embedding" if model in GEMINI_SOURCE_LLM_MODELS else "llm" + elif mutation == "target_model": + row["target"]["model"] = "gemini-3.1-flash-lite" if model != "gemini-3.1-flash-lite" else "gemini-2.5-flash" + elif mutation == "settings": + row["source_settings"]["source_base_policy"] = ( + "not_used" if model in GEMINI_SOURCE_LLM_MODELS else "unset_or_exact_google_root" + ) + elif mutation == "missing_op": + row["operations"].pop() + elif mutation == "extra_op": + row["operations"].append("tts/invoke") + elif mutation == "duplicate_op": + row["operations"].append(row["operations"][0]) + elif mutation == "duplicate_fixture": + row["conformance_fixture_ids"] *= 2 + else: + cast(dict[str, object], row)["source_schema_digest"] = True + with pytest.raises(ValidationError): + SourceGeminiV8VariantMapping.model_validate(raw) + + +def test_selected_wrapper_is_frozen_and_only_projects_its_declaration() -> None: + parsed = SourceGeminiV8VariantMapping.model_validate(synthetic_record()) + row = parsed.variants[1] + selected = SelectedGeminiV8SourceVariant(parsed, row, "sha256:" + "d" * 64) + assert (selected.mapping_revision, selected.source, selected.settlement_owner, selected.pool_types) == ( + parsed.mapping_revision, + parsed.source, + parsed.settlement_owner, + parsed.pool_types, + ) + assert ( + selected.source_schema_digest, + selected.parameter_adapter, + selected.target, + selected.operations, + selected.conformance_fixture_ids, + ) == (row.source_schema_digest, row.parameter_adapter, row.target, row.operations, row.conformance_fixture_ids) + with pytest.raises(FrozenInstanceError): + selected.registry_digest = "sha256:" + "e" * 64 # type: ignore[read-only] + + +def test_existing_v6_real_fixtures_keep_c9_parse_dump_bytes_and_digest() -> None: + # Captured from unchanged C9 before authoring this DTO, not generated from + # the new family or made-up 0913 source profiles. + old = [SourceGeminiVariantMapping.model_validate(variant_record(row)) for row in profiles()] + canonical = json.dumps( + [row.model_dump(mode="json") for row in old], sort_keys=True, separators=(",", ":"), ensure_ascii=False + ).encode() + assert hashlib.sha256(canonical).hexdigest() == "553c65d987506bad246c040a54b74827ec23f56396daa7694f4ecdd14bc8ccd7" + assert hashlib.sha256("\n".join(row.model_dump_json() for row in old).encode()).hexdigest() == ( + "97acbdb6bfc3fe0914bad33de975ee2959480fdc4ee136119c91b5ea80d78d8b" + ) + for item in old: + raw = item.model_dump(mode="json") + raw["variants"][0]["source_plugin_uid"] = GEMINI_0913_SOURCE_UID + with pytest.raises(ValidationError): + SourceGeminiVariantMapping.model_validate(raw) diff --git a/api/tests/unit_tests/services/fixtures/gemini0913_source_profiles.packed.json b/api/tests/unit_tests/services/fixtures/gemini0913_source_profiles.packed.json new file mode 100644 index 00000000000..e37b4425cc0 --- /dev/null +++ b/api/tests/unit_tests/services/fixtures/gemini0913_source_profiles.packed.json @@ -0,0 +1 @@ +{"encoding":"gzip+base64","raw_sha256":"8c32f42e9a623fae60f0da32633425938fb6557c4c93ea6975b7f9714dfb3dd7","payload_base64":"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"} diff --git a/api/tests/unit_tests/services/test_model_billing_gemini_credit_source_v2_adapter.py b/api/tests/unit_tests/services/test_model_billing_gemini_credit_source_v2_adapter.py new file mode 100644 index 00000000000..49059446f53 --- /dev/null +++ b/api/tests/unit_tests/services/test_model_billing_gemini_credit_source_v2_adapter.py @@ -0,0 +1,179 @@ +"""The nine real Source0913 schemas/pins; no target installation or model call.""" + +from dataclasses import replace +from operator import itemgetter +from typing import cast + +import pytest +from typing_extensions import TypedDict + +from graphon.model_runtime.entities.model_entities import AIModelEntity, ModelType +from services import model_billing_gemini_credit_source_v2_adapter as adapter +from services.entities.model_billing_gemini_mapping import GEMINI_SOURCE_UID, GeminiSourceSettings +from services.entities.model_billing_gemini_v8_mapping import GEMINI_0913_SOURCE_UID +from services.entities.model_billing_mapping import SourceParameterContract, model_schema_digest +from tests.helpers.unit.model_billing_gemini_profiles import GeminiProfile +from tests.helpers.unit.model_billing_gemini_v8_profiles import SOURCE_PROVIDER, profile_digest, profiles, schema + + +class BuildArguments(TypedDict, closed=True): + adapter_id: str + adapter_revision: str + expected_rules_digest: str + source_provider: str + source_model: str + source_plugin_unique_identifier: str + source_schema: AIModelEntity + expected_source_schema_digest: str + expected_source_runtime_schema_digest: str + target_model: str + source_settings: GeminiSourceSettings + source_runtime_schema: AIModelEntity | None + + +class MarkerArguments(TypedDict, closed=True): + source_provider: str + source_model: str + model_type: ModelType + target_model: str + + +def arguments(row: GeminiProfile) -> BuildArguments: + return { + "adapter_id": row["adapter_id"], + "adapter_revision": row["adapter_revision"], + "expected_rules_digest": profile_digest(row), + "source_provider": SOURCE_PROVIDER, + "source_model": row["source_model"], + "source_plugin_unique_identifier": GEMINI_0913_SOURCE_UID, + "source_schema": schema(row), + "expected_source_schema_digest": row["source_schema_digest"], + "expected_source_runtime_schema_digest": row["source_runtime_schema_digest"], + "target_model": row["target_model"], + "source_settings": GeminiSourceSettings(**row["source_settings"]), + "source_runtime_schema": schema(row, runtime=True), + } + + +@pytest.mark.parametrize("row", profiles(GEMINI_0913_SOURCE_UID), ids=itemgetter("source_model")) +def test_real_catalog_runtime_rules_marker_and_prospective_restore(row: GeminiProfile) -> None: + args = arguments(row) + contract = adapter.build_gemini_credit_source_contract(**args) + assert type(contract) is adapter.GeminiCreditSourceV2Contract + assert contract.profile_id == row["profile_id"] + assert contract.adapter_id == "gemini_credit_source_v2" + assert contract.rules_digest == profile_digest(row) + assert model_schema_digest(args["source_schema"]) == contract.source_schema_digest + runtime = args["source_runtime_schema"] + assert runtime is not None + assert model_schema_digest(runtime) == contract.source_runtime_schema_digest + marker = adapter.gemini_credit_source_marker( + contract, + source_provider=SOURCE_PROVIDER, + source_model=row["source_model"], + model_type=ModelType(row["model_type"]), + target_model=row["target_model"], + ) + assert set(marker) == { + "schema_version", + "profile_id", + "source_plugin_uid", + "source_model", + "source_schema_digest", + "rules_digest", + } + assert marker["source_plugin_uid"] == GEMINI_0913_SOURCE_UID + assert ( + adapter.restore_gemini_credit_source_contract( + marker, + source_provider=SOURCE_PROVIDER, + source_model=row["source_model"], + model_type=ModelType(row["model_type"]), + target_model=row["target_model"], + source_settings=args["source_settings"], + expected_source_runtime_schema_digest=row["source_runtime_schema_digest"], + source_runtime_schema=runtime, + ) + == contract + ) + # None means frozen/prospective, never a live observed source RPC. + prospective = args.copy() + prospective["source_runtime_schema"] = None + assert adapter.build_gemini_credit_source_contract(**prospective) == contract + + +@pytest.mark.parametrize("row", profiles(GEMINI_0913_SOURCE_UID), ids=itemgetter("source_model")) +@pytest.mark.parametrize( + ("field", "value"), + [ + ("adapter_id", "gemini_credit_source_v1"), + ("adapter_revision", "2"), + ("source_provider", "langgenius/openai/openai"), + ("source_plugin_unique_identifier", GEMINI_SOURCE_UID), + ("source_plugin_unique_identifier", GEMINI_0913_SOURCE_UID.replace("0.9.13", "0.9.14")), + ("source_model", "unreviewed-model"), + ("target_model", "unreviewed-target"), + ("expected_rules_digest", "sha256:" + "a" * 64), + ("expected_source_schema_digest", "sha256:" + "b" * 64), + ("expected_source_runtime_schema_digest", "sha256:" + "c" * 64), + ], +) +def test_each_independent_pin_and_identity_is_fail_closed(row: GeminiProfile, field: str, value: str) -> None: + args = arguments(row) + cast(dict[str, object], args)[field] = value + with pytest.raises(adapter.GeminiCreditSourceV2AdapterError): + adapter.build_gemini_credit_source_contract(**args) + + +@pytest.mark.parametrize("row", profiles(GEMINI_0913_SOURCE_UID), ids=itemgetter("source_model")) +@pytest.mark.parametrize("runtime", [False, True]) +def test_real_entity_full_metadata_drift_is_not_erased(row: GeminiProfile, runtime: bool) -> None: + args = arguments(row) + key = "source_runtime_schema" if runtime else "source_schema" + observed = args[key] + assert isinstance(observed, AIModelEntity) + changed = observed.model_dump(mode="json") + changed["label"] = {"en_US": "changed actual Source metadata"} + cast(dict[str, object], args)[key] = AIModelEntity.model_validate(changed) + with pytest.raises(adapter.GeminiCreditSourceV2AdapterError): + adapter.build_gemini_credit_source_contract(**args) + + +@pytest.mark.parametrize("row", profiles(GEMINI_0913_SOURCE_UID), ids=itemgetter("source_model")) +def test_marker_requires_exact_concrete_factory_and_never_adopts_other_family(row: GeminiProfile) -> None: + args = arguments(row) + contract = adapter.build_gemini_credit_source_contract(**args) + kwargs: MarkerArguments = { + "source_provider": SOURCE_PROVIDER, + "source_model": row["source_model"], + "model_type": ModelType(row["model_type"]), + "target_model": row["target_model"], + } + generic = SourceParameterContract( + profile_id=contract.profile_id, + source_plugin_unique_identifier=contract.source_plugin_unique_identifier, + source_model=contract.source_model, + source_schema_digest=contract.source_schema_digest, + target_model=contract.target_model, + rules_digest=contract.rules_digest, + adapter_id=contract.adapter_id, + algorithm_revision=contract.algorithm_revision, + source_runtime_schema_digest=contract.source_runtime_schema_digest, + ) + for forged in (replace(contract, rules_digest="sha256:" + "0" * 64), generic): + with pytest.raises(adapter.GeminiCreditSourceV2AdapterError): + adapter.gemini_credit_source_marker(cast(adapter.GeminiCreditSourceV2Contract, forged), **kwargs) + marker = adapter.gemini_credit_source_marker(contract, **kwargs) + for changed in ( + {**marker, "schema_version": True}, + {**marker, "profile_id": str(marker["profile_id"]).replace("0913", "0912")}, + {**marker, "source_plugin_uid": GEMINI_SOURCE_UID}, + {**marker, "extra": "not-authority"}, + ): + with pytest.raises(adapter.GeminiCreditSourceV2AdapterError): + adapter.restore_gemini_credit_source_contract( + changed, + **kwargs, + source_settings=args["source_settings"], + expected_source_runtime_schema_digest=row["source_runtime_schema_digest"], + ) diff --git a/api/tests/unit_tests/services/test_model_billing_migration_inventory_gateway.py b/api/tests/unit_tests/services/test_model_billing_migration_inventory_gateway.py index 77f70305d30..aafecf008b7 100644 --- a/api/tests/unit_tests/services/test_model_billing_migration_inventory_gateway.py +++ b/api/tests/unit_tests/services/test_model_billing_migration_inventory_gateway.py @@ -1736,3 +1736,86 @@ def test_credit_llm_family_checks_selected_source_and_every_prospective_target( gateway.validate_target("tenant", _CREDIT_LLM_REVISION, inventory) with pytest.raises(ModelRouteUnmapped): gateway.validate_target("tenant", _CREDIT_LLM_REVISION, {**inventory, "prospective_variants": []}) + + +@pytest.mark.parametrize( + "source_uid", + [ + "langgenius/gemini:0.9.12@9bb9eff3f9c99a8f1f87bdd5e0f178d5493918939dd0e02825ae8383bd0fd7ed", + "langgenius/gemini:0.9.13@4378187cfc50d4253573f4d96baa13228d8ed2eb746ac3bae492a8eaee2da458", + ], +) +@pytest.mark.parametrize("model", ["gemini-2.5-flash", "gemini-embedding-001", "gemini-embedding-2-preview"]) +def test_v8_real_target_identity_guard_never_normalizes_wrong_models(source_uid: str, model: str) -> None: + from services.entities.model_billing_gemini_mapping import GeminiSourceSettings + from services.model_billing_source_variants import GeminiV8SourceVariantContext, source_mapping_revision + from tests.helpers.unit import model_billing_gemini_v8_profiles as gemini + + row = next(item for item in gemini.profiles(source_uid) if item["source_model"] == model) + observed = gemini.schema(row) + registry = source_mapping_revision(gemini.registry(), gemini.REVISION, normalize_provider=str) + selected = registry.resolve( + (gemini.SOURCE_PROVIDER, observed.model_type, model), + context=GeminiV8SourceVariantContext(source_uid, GeminiSourceSettings(**row["source_settings"])), + ) + target = gemini.schema(row, runtime=True) + module._assert_target_source_identity(selected, target) + changed = target.model_dump(mode="json") + changed["model"] = "unreviewed-target-identity" + with pytest.raises(ModelRouteUnmapped): + module._assert_target_source_identity(selected, AIModelEntity.model_validate(changed)) + + +@pytest.mark.parametrize("version", [6, 8]) +@pytest.mark.parametrize("model", ["gemini-embedding-001", "gemini-embedding-2-preview"]) +def test_real_v6_v8_dimension_guards_keep_selected_and_prospective_distinct(version: int, model: str) -> None: + from services.entities.model_billing_gemini_mapping import GeminiSourceSettings + from services.model_billing_source_variants import ( + GeminiSourceVariantContext, + GeminiV8SourceVariantContext, + source_mapping_revision, + ) + from tests.helpers.unit import model_billing_gemini_profiles as legacy + from tests.helpers.unit import model_billing_gemini_v8_profiles as current + + if version == 6: + row = next(item for item in legacy.profiles() if item["source_model"] == model) + registry = source_mapping_revision(legacy.registry(), legacy.REVISION, normalize_provider=str) + context = GeminiSourceVariantContext(legacy.SOURCE_UID, GeminiSourceSettings(**row["source_settings"])) + else: + uid = current.SOURCE_UIDS[1] + row = next(item for item in current.profiles(uid) if item["source_model"] == model) + registry = source_mapping_revision(current.registry(), current.REVISION, normalize_provider=str) + context = GeminiV8SourceVariantContext(uid, GeminiSourceSettings(**row["source_settings"])) + target = current.schema(row, runtime=True) + selected = registry.resolve((current.SOURCE_PROVIDER, target.model_type, model), context=context) + module._assert_target_source_identity(selected, target) + wrong_target = target.model_dump(mode="json") + wrong_target["model"] = "unreviewed-target-identity" + with pytest.raises(ModelRouteUnmapped): + module._assert_target_source_identity(selected, AIModelEntity.model_validate(wrong_target)) + actual = module.embedding_dimensions_contract(target) + module._assert_target_embedding_dimensions( + mapping=selected, + kind=target.model_type, + prospective_only=False, + source={"embedding_dimensions": actual}, + schema=target, + ) + with pytest.raises(ModelRouteUnmapped): + module._assert_target_embedding_dimensions( + mapping=selected, + kind=target.model_type, + prospective_only=False, + source={"embedding_dimensions": "unreviewed-vector-space"}, + schema=target, + ) + # There is no installed-source dimension fact for a prospective row. The + # production path independently verifies its exact runtime schema first. + module._assert_target_embedding_dimensions( + mapping=selected, + kind=target.model_type, + prospective_only=True, + source={}, + schema=target, + ) diff --git a/api/tests/unit_tests/services/test_model_billing_source_variants.py b/api/tests/unit_tests/services/test_model_billing_source_variants.py index 96b0d956269..cfa9e7e24fd 100644 --- a/api/tests/unit_tests/services/test_model_billing_source_variants.py +++ b/api/tests/unit_tests/services/test_model_billing_source_variants.py @@ -16,6 +16,7 @@ from core.entities.provider_entities import ProviderQuotaType, QuotaConfiguratio from core.hosting_configuration import HostingProvider, PaidHostingQuota from graphon.model_runtime.entities.model_entities import AIModelEntity, ModelType from services.entities.model_billing_embedding_mapping import SelectedEmbeddingSourceVariant +from services.entities.model_billing_gemini_v8_mapping import SelectedGeminiV8SourceVariant from services.entities.model_billing_mapping import ( ModelMapping, SelectedSourceVariant, @@ -44,6 +45,7 @@ from services.model_billing_openai_provider110_adapter import ( from services.model_billing_openai_provider110_adapter import _REVIEWED_CONTRACTS from services.model_billing_openai_provider110_adapter import _REVIEWED_CONTRACTS as CURRENT_SOURCE_CONTRACTS from services.model_billing_source_variants import SourceVariantContext, selection_payload, source_endpoint_class +from tests.helpers.unit import model_billing_gemini_v8_profiles as gemini_v8 from tests.helpers.unit.model_billing_source_profiles import ( DAEMON_REVISION, EMBEDDING_MODELS, @@ -2133,3 +2135,179 @@ def test_credit_llm_family_append_preserves_all_prior_registry_shapes_and_digest drifted.restore_selection( (SOURCE_PROVIDER, ModelType.LLM, frozen.source.model), selection_payload(frozen).model_dump(mode="json") ) + + +@pytest.mark.parametrize("source_uid", gemini_v8.SOURCE_UIDS) +@pytest.mark.parametrize("model", [row["source_model"] for row in gemini_v8.profiles(gemini_v8.SOURCE_UIDS[0])]) +def test_v8_real_two_uid_resolution_factory_restore_and_prospective(source_uid: str, model: str) -> None: + from services import model_billing_gemini_credit_source_adapter as legacy + from services import model_billing_gemini_credit_source_v2_adapter as current + from services.entities.model_billing_gemini_v8_mapping import SourceGeminiV8VariantSelection + from services.model_billing_mapping_service import hosted_gemini_source_fact, mapping_has_required_operations + + rows = gemini_v8.profiles(source_uid) + row = next(item for item in rows if item["source_model"] == model) + schemas = [gemini_v8.schema(item) for item in rows] + observed = gemini_v8.schema(row) + registry = source_mapping_revision(gemini_v8.registry(), gemini_v8.REVISION) + fact = hosted_gemini_source_fact( + provider=gemini_v8.SOURCE_PROVIDER, + source_uid=source_uid, + models=schemas, + credentials={"google_api_key": "unit-source-key-never-forwarded"}, + ) + selected = resolve_source_mapping( + registry, + (gemini_v8.SOURCE_PROVIDER, observed.model_type, model), + source_uid=source_uid, + fact=None, + gemini_fact=fact, + schema=observed, + ) + assert type(selected) is SelectedGeminiV8SourceVariant + assert selected.variant.source_plugin_uid == source_uid + assert mapping_has_required_operations(selected) + contract = build_source_adapter_contract( + selected, + source_provider=gemini_v8.SOURCE_PROVIDER, + source_plugin_unique_identifier=source_uid, + source_schema=observed, + source_credentials=None, + ) + expected_type = ( + legacy.GeminiCreditSourceContract + if source_uid == gemini_v8.SOURCE_UIDS[0] + else current.GeminiCreditSourceV2Contract + ) + assert type(contract) is expected_type + assert contract == restore_variant_source_contract(selected) + marker = source_parameter_marker( + contract, + source_provider=gemini_v8.SOURCE_PROVIDER, + source_model=model, + model_type=observed.model_type, + target_model=model, + ) + assert marker["source_plugin_uid"] == source_uid + assert "unit-source-key" not in json.dumps(marker) + frozen = selection_payload(selected) + assert type(frozen) is SourceGeminiV8VariantSelection + assert frozen.schema_version == 8 + assert ( + registry.restore_selection( + (gemini_v8.SOURCE_PROVIDER, observed.model_type, model), frozen.model_dump(mode="json") + ) + == selected + ) + assert len(registry.selected_prospective()) == 18 + validate_frozen_mapping_evidence( + selected, + provider=gemini_v8.SOURCE_PROVIDER, + model=model, + model_type=observed.model_type, + target_model=model, + operations=tuple(selected.operations), + source_contract=contract, + tts_contract=None, + ) + + +@pytest.mark.parametrize("source_uid", gemini_v8.SOURCE_UIDS) +def test_v8_fresh_facts_unknown_uid_drift_and_wrong_context_cannot_authorize(source_uid: str) -> None: + from core.entities.provider_entities import HostedGeminiSourceFact + from services.entities.model_billing_gemini_mapping import GeminiSourceSettings + from services.model_billing_mapping_service import ( + gemini_source_variant_context, + gemini_v8_source_variant_context, + hosted_gemini_source_fact, + ) + from services.model_billing_source_variants import GeminiSourceVariantContext, GeminiV8SourceVariantContext + + row = gemini_v8.profiles(source_uid)[0] + observed = gemini_v8.schema(row) + identity = (gemini_v8.SOURCE_PROVIDER, observed.model_type, row["source_model"]) + registry = source_mapping_revision(gemini_v8.registry(), gemini_v8.REVISION) + fact = hosted_gemini_source_fact( + provider=gemini_v8.SOURCE_PROVIDER, + source_uid=source_uid, + models=[observed], + credentials={}, + ) + assert fact is not None + + def context(value: HostedGeminiSourceFact | None) -> GeminiV8SourceVariantContext | None: + return gemini_v8_source_variant_context( + value, + provider=gemini_v8.SOURCE_PROVIDER, + source_uid=source_uid, + model_type=observed.model_type, + schema=observed, + ) + + assert context(fact) is not None + assert context(None) is None + assert context(replace(fact, plugin_unique_identifier="unknown")) is None + assert context(replace(fact, catalog_schema_digests=())) is None + assert context(replace(fact, llm_file_url_policy="unreviewed")) is None + assert ( + hosted_gemini_source_fact( + provider=gemini_v8.SOURCE_PROVIDER, + source_uid=source_uid + "x", + models=[observed], + credentials={}, + ) + is None + ) + assert ( + hosted_gemini_source_fact( + provider=gemini_v8.SOURCE_PROVIDER, + source_uid=source_uid, + models=[observed], + credentials=None, + ) + is None + ) + with pytest.raises(ValueError, match="V8 Gemini fact family"): + registry.resolve( + identity, context=GeminiSourceVariantContext(source_uid, GeminiSourceSettings(**row["source_settings"])) + ) + if source_uid == gemini_v8.SOURCE_UIDS[1]: + assert ( + gemini_source_variant_context( + fact, + provider=gemini_v8.SOURCE_PROVIDER, + source_uid=source_uid, + model_type=observed.model_type, + schema=observed, + ) + is None + ) + + +@pytest.mark.parametrize( + ("field", "error"), + [ + ("schema_version", "integer 8"), + ("variant_id", "Frozen source variant changed"), + ("source_plugin_uid", "literal_error"), + ("registry_digest", "string_pattern_mismatch"), + ("source_runtime_schema_digest", "string_pattern_mismatch"), + ], +) +def test_v8_saved_selection_tampering_is_not_a_revision_rebase(field: str, error: str) -> None: + from services.entities.model_billing_gemini_mapping import GeminiSourceSettings + from services.model_billing_source_variants import GeminiV8SourceVariantContext + + source_uid = gemini_v8.SOURCE_UIDS[1] + row = gemini_v8.profiles(source_uid)[0] + observed = gemini_v8.schema(row) + identity = (gemini_v8.SOURCE_PROVIDER, observed.model_type, row["source_model"]) + registry = source_mapping_revision(gemini_v8.registry(), gemini_v8.REVISION) + selected = registry.resolve( + identity, context=GeminiV8SourceVariantContext(source_uid, GeminiSourceSettings(**row["source_settings"])) + ) + assert isinstance(selected, SelectedGeminiV8SourceVariant) + payload = selection_payload(selected).model_dump(mode="json") + payload[field] = True if field == "schema_version" else "changed" + with pytest.raises(ValueError, match=error): + registry.restore_selection(identity, payload)