Skip to main content
Lesson 22 of 22 · Responses API: OpenAI Fundamentals

Compliance and Audit Trail

Lesson 22 of 22 Updated July 29, 2026 Developer
On this page
  1. Compliance and Audit Trail
  2. Why an Audit Trail?
  3. Structure of an Audit Record
  4. Complete Audit System
  5. Audit Queries
  6. Retention and Archiving
  7. European AI Act: What Changes
  8. Compliance Checklist
  9. Key Takeaways
  10. Test Your Knowledge

Compliance and Audit Trail

For AI applications in production, traceability is essential. A complete audit trail protects you during an inspection, a dispute, or an incident. This lesson shows you how to implement a robust audit system, closing out the course by bringing together everything you’ve built so far—logging, moderation, and data protection—into a single, defensible framework.

Why an Audit Trail?

Four distinct needs converge on the same infrastructure. First, regulatory compliance: the GDPR, the European AI Act, and industry standards all require you to demonstrate what your system does, and a demonstration without logs is worthless. Second, traceability in the strict sense—knowing who did what, when, with what result, even six months later. Debugging benefits daily from this, since understanding unexpected model behavior requires recovering the exact conditions of the call. Finally, accountability: documenting decisions made by the AI is what will allow you, when the time comes, to show that a contested recommendation rested on a controlled process.

Structure of an Audit Record

The dataclass below is as much a contract as it is a data structure. It groups identifiers, application context, the request, the response, the moderation verdict, and technical metadata. Two choices deserve your attention. The user identifier appears only in hashed form, never in plain text. And the prompt is not stored: only its hash and length are. This preserves your ability to prove that two calls concerned the same content without ever creating an archive of what your users wrote.

from dataclasses import dataclass, field, asdict
from datetime import datetime
from typing import Optional
import json
import uuid
import hashlib

@dataclass
class AuditRecord:
    """Audit record for an API call."""
    # Identifiers
    audit_id: str = field(default_factory=lambda: str(uuid.uuid4()))
    timestamp: str = field(
        default_factory=lambda: datetime.now().isoformat()
    )

    # Context
    user_id_hash: str = ""  # Never the plain ID
    session_id: str = ""
    application: str = ""

    # Request (no sensitive data)
    model: str = ""
    prompt_hash: str = ""  # Hash of the prompt, not the prompt itself
    prompt_length: int = 0

    # Response
    response_id: str = ""
    output_length: int = 0
    input_tokens: int = 0
    output_tokens: int = 0

    # Moderation
    input_flagged: bool = False
    output_flagged: bool = False

    # Metadata
    duration_ms: int = 0
    status: str = "success"
    error: Optional[str] = None

    def to_json(self) -> str:
        return json.dumps(asdict(self), ensure_ascii=False)

Complete Audit System

The client that leverages this structure chains input moderation, the API call, output moderation, and writes in every case. The important word is append-only: the JSONL file is opened in append mode, never rewritten, which is the condition for a log to have evidentiary value. Note the path of a flagged entry—the record is saved before the exception is raised, with the status blocked_input. A block therefore leaves a trace just like a success, and the final except block guarantees that no error, whatever its origin, slips under the radar.

import time
from openai import OpenAI

class AuditedClient:
    """OpenAI client with integrated audit trail."""

    def __init__(self, application: str, audit_store: str = "audit.jsonl"):
        self.client = OpenAI()
        self.application = application
        self.audit_store = audit_store

    def _hash(self, text: str) -> str:
        """Truncated SHA-256 hash for auditing."""
        return hashlib.sha256(text.encode()).hexdigest()[:16]

    def _save_audit(self, record: AuditRecord):
        """Saves an audit record (append-only)."""
        with open(self.audit_store, "a", encoding="utf-8") as f:
            f.write(record.to_json() + "\n")

    def create(self, user_id: str, session_id: str,
               model: str, input: str, **kwargs) -> object:
        """API call with automatic auditing."""

        record = AuditRecord(
            user_id_hash=self._hash(user_id),
            session_id=session_id,
            application=self.application,
            model=model,
            prompt_hash=self._hash(input),
            prompt_length=len(input),
        )

        start = time.monotonic()

        try:
            # Input moderation
            mod = self.client.moderations.create(
                model="omni-moderation-latest",
                input=input
            )
            record.input_flagged = mod.results[0].flagged

            if record.input_flagged:
                record.status = "blocked_input"
                self._save_audit(record)
                raise ValueError("Inappropriate input content")

            # API call
            response = self.client.responses.create(
                model=model,
                input=input,
                **kwargs
            )

            # Output moderation
            mod_out = self.client.moderations.create(
                model="omni-moderation-latest",
                input=response.output_text
            )
            record.output_flagged = mod_out.results[0].flagged

            # Fill metrics
            record.response_id = response.id
            record.output_length = len(response.output_text)
            record.input_tokens = response.usage.input_tokens
            record.output_tokens = response.usage.output_tokens
            record.duration_ms = round((time.monotonic() - start) * 1000)
            record.status = "success"

            self._save_audit(record)
            return response

        except Exception as e:
            record.duration_ms = round((time.monotonic() - start) * 1000)
            record.status = "error"
            record.error = type(e).__name__
            self._save_audit(record)
            raise

# Usage
audited = AuditedClient(application="support-chatbot")

response = audited.create(
    user_id="[email protected]",
    session_id="sess-abc123",
    model="gpt-5.6-terra",
    input="How do I return a product?"
)
print(response.output_text)

In this example, the email address passed as user_id never reaches the file: it is hashed as soon as the record is constructed. A JSONL file works fine to get started and for a single instance; as soon as multiple processes write in parallel, direct this stream to a database or a log collector.

Audit Queries

A log you can’t query only serves to occupy disk space. Line-by-line reading of the JSONL format makes exploitation trivial, and the report below summarizes the covered period, the distribution between successes, errors, and blocks, token consumption, and average latency. The count of unique users relies on hashes, which proves incidentally that a pseudonymized log remains perfectly analyzable.

import json

def read_audit(file: str = "audit.jsonl") -> list[dict]:
    """Reads all audit records."""
    records = []
    try:
        with open(file, "r", encoding="utf-8") as f:
            for line in f:
                if line.strip():
                    records.append(json.loads(line))
    except FileNotFoundError:
        pass
    return records

def audit_report(file: str = "audit.jsonl") -> dict:
    """Generates an audit report."""
    records = read_audit(file)

    if not records:
        return {"message": "No records"}

    total = len(records)
    errors = sum(1 for r in records if r["status"] == "error")
    blocked = sum(1 for r in records if r["status"] == "blocked_input")
    tokens_total = sum(
        r.get("input_tokens", 0) + r.get("output_tokens", 0)
        for r in records
    )
    durations = [r["duration_ms"] for r in records if r["duration_ms"] > 0]

    return {
        "period": {
            "start": records[0]["timestamp"],
            "end": records[-1]["timestamp"],
        },
        "total_calls": total,
        "successes": total - errors - blocked,
        "errors": errors,
        "moderation_blocks": blocked,
        "tokens_consumed": tokens_total,
        "avg_latency_ms": round(sum(durations) / max(len(durations), 1)),
        "unique_users": len(set(r["user_id_hash"] for r in records)),
    }

print(json.dumps(audit_report(), indent=2, ensure_ascii=False))

Retention and Archiving

Keeping data indefinitely contradicts the principle of storage limitation; deleting too soon deprives you of the proof you might need. Rotation solves this dilemma: beyond a duration you set—ninety days here—records leave the active file for a dated archive, while the current log is rewritten with only the recent ones. Schedule this function as a periodic task and align retention_days with the policy validated by your legal counsel, not a value chosen at random.

from datetime import datetime, timedelta
import shutil
import os

def archive_audit(file: str = "audit.jsonl",
                  retention_days: int = 90):
    """Archives and purges old records."""
    records = read_audit(file)
    limit = datetime.now() - timedelta(days=retention_days)

    recent = []
    archives = []

    for record in records:
        date_record = datetime.fromisoformat(record["timestamp"])
        if date_record > limit:
            recent.append(record)
        else:
            archives.append(record)

    if archives:
        # Archive old ones
        archive_name = f"audit_archive_{datetime.now().strftime('%Y%m%d')}.jsonl"
        with open(archive_name, "w", encoding="utf-8") as f:
            for record in archives:
                f.write(json.dumps(record, ensure_ascii=False) + "\n")

        # Rewrite main file with recent ones
        with open(file, "w", encoding="utf-8") as f:
            for record in recent:
                f.write(json.dumps(record, ensure_ascii=False) + "\n")

        print(f"Archived {len(archives)} records to {archive_name}")
        print(f"Kept {len(recent)} recent records")

European AI Act: What Changes

The European AI Act (AI Act) imposes obligations based on the risk level of your application. The logic is graduated: the more your system weighs on people’s lives, the stricter the requirements. An FAQ chatbot, a summary, or a translation falls under minimal risk and calls only for basic transparency. As soon as there is conversation or sentiment analysis, you move to limited risk, with the obligation to inform the user they are interacting with an AI. The jump is abrupt for high risk—credit scoring, recruitment, medical diagnosis—where conformity assessment, risk management, training data documentation, complete traceability, human supervision, and demonstration of accuracy are added. Certain uses, such as social scoring or subliminal manipulation, are purely prohibited.

# Assess the risk level of your application
RISK_LEVELS = {
    "minimal": {
        "examples": ["FAQ chatbot", "text summary", "translation"],
        "obligations": ["Basic transparency"]
    },
    "limited": {
        "examples": ["customer service chatbot", "sentiment analysis"],
        "obligations": ["Inform the user they are interacting with an AI"]
    },
    "high": {
        "examples": ["credit scoring", "recruitment", "medical diagnosis"],
        "obligations": [
            "Conformity assessment",
            "Risk management system",
            "Documented training data",
            "Logging and complete traceability",
            "Mandatory human supervision",
            "Demonstrated accuracy and robustness"
        ]
    },
    "unacceptable": {
        "examples": ["social scoring", "subliminal manipulation"],
        "obligations": ["PROHIBITED"]
    }
}

Honestly situate your project in this grid before writing the first line of code: an application classified as high risk imposes architectural constraints—exhaustive logging, human control point—that cannot be grafted on after the fact to a system designed without them.

Compliance Checklist

  • Append-only audit trail with timestamp
  • Hash of personal data in logs (never in plain text)
  • Systematic moderation of inputs and outputs
  • Up-to-date GDPR record of processing activities
  • Defined retention and archiving policy
  • AI Act risk level assessment performed
  • Inform users they are interacting with an AI
  • Human supervision for impactful decisions

Key Takeaways

  • An audit trail is mandatory for regulatory compliance
  • Log in append-only mode: never modify existing records
  • Hash all personal data in audit logs
  • The European AI Act classifies applications by risk level
  • Implement automatic moderation AND human supervision
  • Define a retention policy and archive regularly

Test Your Knowledge

Twenty-two API lessons: validate the mechanisms that hold production together.

1. Why migrate to the Responses API rather than staying on Chat Completions?

Answer: It’s OpenAI’s modern API: integrated conversation state management, native tools, and evolution focused on it—Chat Completions remains supported but legacy.

2. How do you choose between Sol, Terra, and Luna on the API side?

Answer: gpt-5.6-sol for demanding tasks, terra for cost/quality balance, luna for economic volume—and the reasoning level (none→max) is then adjusted per request.

3. What does Structured Output guarantee?

Answer: Output conforming to the provided JSON schema—fields, types, structure—exploitable by a program without defensive parsing or repair.

4. What do you do in the face of a 429 error?

Answer: Respect rate limits: retry with exponential backoff (and jitter), cap attempts, and plan for a circuit breaker—never an immediate retry loop.

5. Name two indispensable protections on the application security side.

Answer: Moderation of incoming/outgoing content via the dedicated API, and defense against prompt injection: isolate instructions from data, validate outputs, limit exposed capabilities.

Production, security, compliance: the last three blocks of the course make the difference between a prototype and a service—keep them as a checklist.