Skip to content

Day 3 — Scripted First, Live Provider Second

Today Brandforge gains a replaceable producer for BusinessProfile. The required implementation reads a saved model response. The optional implementation calls a live provider. Both must return the same domain type, and neither gets permission to write campaign artifacts.

A live request mixes several variables:

  • network and authentication;
  • provider and model release;
  • prompt version;
  • sampling variation;
  • response decoding;
  • your validation logic.

Start with fixtures/brandforge/business-profile.json so the last two can be tested independently. Once those are correct, a provider failure cannot masquerade as a Rust bug.

pub trait ProfileModel: Send + Sync {
fn extract(&self, source: &str) -> Result<BusinessProfile, ModelError>;
}

The trait describes what Brandforge needs, not every feature a vendor offers. It does not expose image generation, web search, conversation IDs, or provider-specific response objects.

A fixture adapter can own saved JSON:

pub struct ScriptedProfileModel {
response: Vec<u8>,
}
impl ProfileModel for ScriptedProfileModel {
fn extract(&self, _source: &str) -> Result<BusinessProfile, ModelError> {
let profile = serde_json::from_slice(&self.response)?;
Ok(profile)
}
}

Notice the scripted adapter does not pretend to understand the source. Its job is to make the rest of the pipeline deterministic.

For a live adapter, send:

  1. an instruction that defines the extraction task;
  2. the frozen source snapshot;
  3. a strict JSON schema matching BusinessProfile;
  4. an explicit model supplied by configuration;
  5. a timeout and bounded retry policy.

Do not hard-code an API key. Read it at startup, return a configuration error if it is absent, and redact it from traces.

The current OpenAI API supports structured output through the Responses API. The provider schema is a wire contract: every field is required and objects disable additional properties. Keep the Rust domain separate so a future provider adapter can produce the same BusinessProfile.

A successful HTTP response is not necessarily a usable profile. The adapter must distinguish:

  • missing credentials;
  • transport failure;
  • timeout;
  • non-success HTTP status;
  • refusal;
  • incomplete output;
  • malformed JSON;
  • schema-valid but domain-invalid output.

These states deserve different error variants because retrying an invalid palette is not the same as retrying a connection reset.

Build a FailingProfileModel whose extract returns ModelError::Timeout. Assert that Brandforge:

  • creates no campaign artifact;
  • does not silently fall back to invented data;
  • reports the failure at the provider boundary;
  • does not retry beyond the configured budget.

Then return valid JSON with an unsupported claim. That request succeeds at the provider boundary and must fail tomorrow at the evidence boundary.

You used a trait to isolate model variance and a scripted adapter to make AI software testable. The important architecture is not “call an API.” It is probabilistic producer → typed domain → independent validator.

Deep references: Provider-neutral capability traits and Remote, embedded, or service inference.

Next: Day 4 — Ground Every Claim →.