Fingerprinting

A fingerprint uniquely identifies a specific configuration, so you can detect whether anything significant changed between runs or to define configuration-specific parameters (e.g., in AI Control research, suspiciousness threshold values may vary depending on the configuration used to calculate them).

A fingerprint is built from a specification that mirrors the config’s key structure, with leaf values naming an encoding method for the value at that path. A fingerprint can be defined for the entire configuration, or only a subset. By convention the spec lives at config.fingerprint.

For instance, this spec:

config:
  fingerprint:
    model:
      name: value
    prompts:
      system: hash
    build_info:
      version: value|minor_version

May result in a fingerprint that looks something like:

model:
  name: openai/gpt-4o
prompts:
  system: a6d75ef920b9785a2a94469c030926e7b62cbc6e111801db909bd30a113cb972
build_info:
  version: "1.4"

Fingerprints can be generated with create_fingerprint by passing in a configuration and a spec, where the spec can be a Mapping or a string pointing to a path in the configuration to load the Mapping from. By default the spec is read from config.fingerprint.

Encoding Methods

The specification mirrors the shape of the merged configuration: nested mappings recurse while leaf values name an encoding method for the config value at that path.

Regular encoding methods include:

  • hash
    A stable hash of the value.

  • value
    For built-ins (scalars, dicts, lists, etc.), the value is used as-is; otherwise its repr representation is used.

  • set-attrs-dict
    For dataclasses and Pydantic models, a dict of only the attributes that differ from the defaults; otherwise raises a TypeError.

  • none
    Omit this part of the configuration (useful when overriding a default spec).

Collection encoding methods can be applied to sequences or mappings:

  • list[method]
    Fingerprint each list element with one of the methods listed above.

  • dict[method]
    Fingerprint the value in each mapping entry with one of the methods listed above.

Finally, a fingerprint method can be prefixed with config: to indicate that the value should be retrieved from the configuration without instantiating (e.g., use the raw configuration). This can be helpful for fingerprinting functions or classes that require creating a fingerprint to be constructed, thus avoiding circular dependencies.

Transforms

Values can be transformed before being encoded using the form method|transform (e.g., value|minor_version, dict[value|major_version]). If a value is a sequence, the transform will be applied to every element in the sequence; if the value is a mapping, the transform will be applied to the value of each entry in the mapping.

Values can be transformed using one of the following transforms:

  • release_version
    The release segments only, dropping any pre-release, dev or local metadata (0.8.2.dev63+g1a2b3c0.8.2).

  • minor_version
    The major.minor prefix (0.9.30.9 or 3.4.13.4).

  • major_version
    The major prefix (16.0.016). This raises a ValueError for 0.x versions, since every 0.x release would collapse to 0 and match unrelated releases.

Version transforms can be used to truncate a version string so that non-breaking releases do not change the fingerprint.

Named Fingerprints

Fingerprints for different configurations can be stored under a key like fingerprints that maps unique names to fingerprints.

For instance:

# fingerprints.yaml (loaded as part of your config, e.g., via `includes`)
fingerprints:
  gpt-4o_factored-task-prompts:
    model:
      name: openai/gpt-4o
    prompts:
      system: a6d75ef920b9785a2a94469c030926e7b62cbc6e111801db909bd30a113cb972
    build_info:
      version: "1.4"

  sonnet-4-5_factored-task-prompts:
    model:
      name: anthropic/claude-sonnet-4-5
    prompts:
      system: a6d75ef920b9785a2a94469c030926e7b62cbc6e111801db909bd30a113cb972
    build_info:
      version: "1.4"

These fingerprints can then be searched to identify the fingerprint that matches the current configuration using current_fingerprint:

from strataconf import current_fingerprint

# e.g., returns "sonnet-4-5_factored-task-prompts", or None if nothing matches
name = current_fingerprint(
  config=config,                    # the current configuration
  fingerprints_key="fingerprints",  # defaults to `fingerprints`
  spec="config.fingerprint"         # defaults to `config.fingerprint`; can be a string or dict
)

The name makes it easy to identify the current configuration and reference it from elsewhere. It is completely arbitrary; though best practice is to identify the parts of the fingerprint that are expected to change between different configurations and use these in the fingerprint name.

Names allow you to tie configuration-specific values to a fingerprint name and resolve them for the current configuration. For example, config.get(f"thresholds.{fingerprint_name}") would return threshold values tied to the current configuration or None if the critical values defined by the fingerprint had been overridden such that no match could be found.

The current fingerprint is the first fingerprint defined under the fingerprints key that matches the fingerprint of the current configuration, where a match occurs when all of the shared keys are the same.

More specifically, the current fingerprint is resolved via this process:

  1. Create a fingerprint for the current configuration using the fingerprint spec.
  2. Retrieve an ordered mapping of fingerprints under the fingerprints key.
  3. For each entry, compare the shared keys between the entry and the current fingerprint.
  4. Return the name of the first entry whose shared keys all match the current fingerprint.

When the logging mode is set to debug, information on why each fingerprint did not match is logged. This can be helpful for identifying configuration changes that are causing fingerprint mismatches.