Skip to main content
Version: 0.13

Model Interaction Capture

Use model interaction capture when a cloud model is giving your application better answers than the small model you ultimately want to ship. It saves the prompts your application sent to the teacher model and the teacher's completed answers as JSONL training examples. You can then review, filter, and fine-tune a smaller local model on the behavior that matters to your game or application.

For example, an NPC dialogue system can initially route requests to a proxy backend such as Claude, Gemini, or OpenAI. Once it has a useful collection of quest dialogue, you can fine-tune a small local model and replace the proxy without changing the dialogue call sites.

Capture is off by default. Enable it once when you create the engine; every normal request continues to return exactly as it did before. The application does not wait for a file write: Atelico queues completed records and writes them on a separate background thread.

When to use it

Model interaction capture is useful when you want to:

  • collect real prompts and high-quality teacher answers for a fine-tuning set;
  • compare a new cloud model or prompt with the model you plan to ship;
  • retain a small sample of production behavior for evaluation and regression tests;
  • collect data first, then move an application from a cloud proxy to a local model.

It is not a replacement for application analytics or error reporting. Capture stores model inputs and outputs, which may include player text, system prompts, and other sensitive application data. Enable it only where you are allowed to retain that data, choose a directory with appropriate access controls, and delete or filter records that should not become training data.

Quick start

Add a data_logging object to the engine configuration before starting the engine. This example keeps every completed chat request sent to the teacher proxy backend in teacher-data/:

{
"backends": [
{
"name": "teacher",
"type": "proxy",
"base_url": "https://api.openai.com/v1"
}
],
"data_logging": {
"enabled": true,
"output_dir": "teacher-data",
"backends": ["teacher"],
"request_types": ["chat_completion"]
}
}

Use teacher::your-model-id in the normal chat request. No logging call is needed around that request. Atelico creates files such as teacher-data/2026-07-12.jsonl; each line is one completed interaction.

The example omits the provider credential. Supply it through the proxy backend configuration exactly as you normally do; the Rust, Python, and Godot examples below show environment-backed credentials.

tip

Start with sample_rate: 0.1 for a production trial. Increase it after you have checked the data volume and quality. Use backends to make sure you only collect answers from the teacher backend, not requests served by a local model.

Configure it from your SDK or plugin

All SDKs use the same data_logging shape. Configure it before creating the engine (or, for Godot, before initialize_engine). Your normal model calls are unchanged.

Rust SDK

use atelico_sdk::{
BackendConfig, BackendType, DataLoggingConfig, Engine, EngineConfig,
};

let engine = Engine::from_config(EngineConfig {
backends: vec![BackendConfig {
name: "teacher".into(),
backend_type: BackendType::Proxy,
base_url: Some("https://api.openai.com/v1".into()),
api_key: Some(std::env::var("OPENAI_API_KEY")?),
..Default::default()
}],
data_logging: Some(DataLoggingConfig {
enabled: true,
output_dir: "teacher-data".into(),
backends: vec!["teacher".into()],
request_types: vec!["chat_completion".into()],
..Default::default()
}),
..Default::default()
})?;

// Existing calls need no wrapper. A request using `teacher::...` is captured.
# let _ = engine;
# Ok::<(), anyhow::Error>(())

Pass a full engine configuration to Engine. The normal chat method is the same one you use without capture.

import json
import os
from atelico import Engine

engine = Engine(config_json=json.dumps({
"backends": [{
"name": "teacher",
"type": "proxy",
"base_url": "https://api.openai.com/v1",
"api_key": os.environ["OPENAI_API_KEY"],
}],
"data_logging": {
"enabled": True,
"output_dir": "teacher-data",
"backends": ["teacher"],
"request_types": ["chat_completion"],
},
}))

reply = json.loads(engine.llm_chat_completion(json.dumps({
"model": "teacher::gpt-4.1-mini",
"messages": [{"role": "user", "content": "Give my NPC a greeting."}],
})))
print(reply["choices"][0]["message"]["content"])

If your application already uses the default local engine, the short form is Engine(data_logging_json=json.dumps({"enabled": True, ...})).

C FFI and custom hosts

Pass the full JSON document to atelico_engine_create(config_json, ...). This is also the configuration path used by Unity. No per-request FFI calls are needed, and capture never blocks the caller thread:

const char* config = "{\"data_logging\":{\"enabled\":true,\"output_dir\":\"teacher-data\",\"backends\":[\"openai\"]}}";
AtelicoEngine* engine = NULL;
int32_t result = atelico_engine_create(config, &engine);

See the C FFI API reference for the surrounding entry points.

Choose what to capture

The following fields go inside data_logging. Omit a field to use its default.

FieldDefaultUse it for
enabledfalseTurning capture on.
output_dirdata_logsChoosing the JSONL destination.
sample_rate1.0Keeping only a fraction of eligible calls.
backends[] (all)Limiting collection to a teacher/proxy backend.
request_types[] (all)Limiting collection to chat_completion, text_completion, response, image_generation, embedding, or background_removal.
redact_system_promptsfalseRemoving system messages before they reach disk.
max_output_lengthunsetLimiting captured text output to a byte count.
log_errorstrueRetaining failed calls and their error text.
buffer_size1024Setting the maximum in-memory record queue.
batch_size64Choosing how many records the writer writes together.
flush_interval_ms100Setting the longest delay before a partial batch is written.

An empty backends or request_types list means "all". Backend filtering matches the backend name, such as teacher in teacher::gpt-4.1-mini.

What you get on disk

The built-in sink appends one JSON object per line and rotates daily:

{
"schema_version": 1,
"timestamp": 1783872000,
"request_type": "chat_completion",
"backend": "teacher",
"model": "gpt-4.1-mini",
"input": {
"type": "chat",
"messages": [{"role": "user", "content": "Give my NPC a greeting."}]
},
"output": {"type": "chat", "content": "Welcome, traveler!"},
"metadata": {"latency_ms": 184, "streamed": false}
}

Keep the raw JSONL files immutable. A training-preparation job should select successful rows for the intended teacher model, remove entries that are not appropriate for training, and write the transformed chat dataset elsewhere. Do not train blindly on every captured response: review quality, consent, licensing, errors, and tool-only responses first.

Server deployments

If you operate atelico-server instead of an in-process SDK, enable the same feature with environment variables:

ATELICO_DATA_LOG=1 \
ATELICO_DATA_LOG_DIR=/var/lib/my-app/teacher-data \
ATELICO_DATA_LOG_SAMPLE_RATE=0.1 \
./atelico-server

Set ATELICO_DATA_LOG_REDACT_SYSTEM=1 to omit system messages. See Server Configuration for the complete server environment reference.

Troubleshooting

  • No JSONL file appears: make one routed model request, verify the process can create output_dir, and check that the selected backend/request type matches your filters.
  • A streamed answer is not there yet: the record is written after the stream has finished, not once per token.
  • Too much data: reduce sample_rate, restrict backends and request_types, or set max_output_length.
  • Capture affects a busy game loop: it deliberately drops records when its bounded queue is full instead of blocking inference. Increase buffer_size only if retaining more records is worth the extra memory.

API reference

  • Core applications can use atelico_core::data_log::{DataLogConfig, DataLogFormat, DataLogger, DataLogSink, DataLogStats} directly.
  • Routers expose enable_data_logging, enable_data_logging_with_sink, disable_data_logging, and data_log_stats.
  • SDK-backed applications use atelico_sdk::DataLoggingConfig through EngineConfig.data_logging.