# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/under-what-conditions-must-an-instance-refuse-to-emit-a-genai-span-when-token-accounting-tool-call-attribution-and-model-provenance-cannot-be-reconcil.md
# Remnant Warrant:    https://corpusfetch.com/c/under-what-conditions-must-an-instance-refuse-to-emit-a-genai-span-when-token-accounting-tool-call-attribution-and-model-provenance-cannot-be-reconcil.warrant.json
# Markdown SHA-256:   9049777846b00f8b15e555fdb7c8abdb2f2b7fc6fcb6768908cf099913507ba5
# Code AST SHA-256:   0dd5e4c452629bf7b5b87e21d8fc5ed0d7a4f55251ed4462bfb5f2417d9e3bb3
# Verified Classes:   GenAISpanReconciliationError
# Defensive Guards:   2 fail-closed invariants (zero external deps)

class GenAISpanReconciliationError(RuntimeError):
    """Raised when a GenAI span would export mutually inconsistent
    token-accounting, tool-call, or model-provenance attributes."""
    pass


def close_genai_span(span, *, usage: dict | None, request_model: str,
                     response_model: str | None, response_model_observed: bool,
                     tool_calls: list[dict], tool_results: list[dict]) -> None:
    """Finalize a GenAI span, refusing to emit known-inconsistent claims.

    usage: e.g. {"input_tokens": int, "output_tokens": int}
    response_model_observed: True only if the terminal stream chunk was seen.
    """
    # 1. Tool-call attribution: every emitted call must have exactly one result.
    call_ids = [c["id"] for c in tool_calls]
    result_ids = [r["id"] for r in tool_results]
    unmatched_calls = [i for i in call_ids if result_ids.count(i) != 1]
    orphan_results = [i for i in result_ids if i not in call_ids]

    # 2. Provenance: response model must be observed, not inferred.
    provenance_inferred = response_model is not None and not response_model_observed

    # 3. Token claim present?
    token_claim = bool(usage) and any(
        usage.get(k) is not None for k in ("input_tokens", "output_tokens")
    )

    # Refuse only when a token/provenance claim is known-false.
    if token_claim and (unmatched_calls or orphan_results):
        raise GenAISpanReconciliationError(
            f"token claim present but tool-call attribution unresolved: "
            f"unmatched_calls={unmatched_calls} orphan_results={orphan_results}"
        )
    if token_claim and provenance_inferred:
        raise GenAISpanReconciliationError(
            f"token claim attributed to inferred model {response_model!r}; "
            f"terminal chunk not observed"
        )

    # Otherwise emit with omission: drop unsubstantiated attributes, keep the span.
    if usage:
        for k, v in usage.items():
            if v is not None:
                span.set_attribute(f"gen_ai.usage.{k}", v)
    span.set_attribute("gen_ai.request.model", request_model)
    if response_model_observed and response_model:
        span.set_attribute("gen_ai.response.model", response_model)
    # ... emit tool events for matched calls only ...
    span.end()
