The single function all AI features call. Model selection, budget checks, error handling, and logging — written once, inherited by everything.
The difference between a maintainable AI integration and a sprawling one is whether the cross-cutting concerns live in one place. Without a central wrapper, each AI feature has its own error handling, its own model selection, its own logging (or none). Adding a cost cap means touching every feature. Changing a model means touching every feature. With a wrapper, every AI feature is one function call, and all the policy lives in one function.
The Anchor build needed three distinct AI features: description generation for the inventory catalog, natural language search interpretation, and lead summary generation for the sales team. Each feature had different latency requirements, different acceptable costs, and different failure modes. Without a central wrapper, each would have been implemented in isolation — three separate HTTP client calls, three separate error handlers, no shared logging, no consistent cost tracking.
All AI calls in the system route through [client]_ai_request(). The function handles argument defaults, budget checking, model selection, the HTTP request, response parsing, logging, and error normalization:
function [client]_ai_request( array $args ): array {
$defaults = [
'task' => 'general',
'model' => null, // null = resolve by task
'system' => '',
'messages' => [],
'max_tokens' => 512,
'temperature' => 0.7,
'cache_system' => false,
];
$args = wp_parse_args( $args, $defaults );
if ( empty( $args['messages'] ) ) {
return [ 'ok' => false, 'error' => 'no_messages', 'content' => '', 'tokens' => 0 ];
}
// Budget check before making any API call
if ( ! [client]_ai_budget_available( $args['task'] ) ) {
return [ 'ok' => false, 'error' => 'daily_cap', 'content' => '', 'tokens' => 0 ];
}
$model = $args['model'] ?? [client]_select_ai_model( $args['task'] );
$started_at = microtime( true );
$request_body = [
'model' => $model,
'messages' => $args['messages'],
'max_tokens' => $args['max_tokens'],
'temperature' => $args['temperature'],
];
// System prompt — with optional prompt caching
if ( $args['system'] ) {
if ( $args['cache_system'] ) {
$request_body['system'] = [ [
'type' => 'text',
'text' => $args['system'],
'cache_control' => [ 'type' => 'ephemeral' ],
] ];
} else {
$request_body['system'] = $args['system'];
}
}
$api_key = [client]_get_setting( 'anthropic_api_key', '' );
$response = wp_remote_post( 'https://api.anthropic.com/v1/messages', [
'timeout' => 30,
'headers' => [
'x-api-key' => $api_key,
'anthropic-version' => '2023-06-01',
'content-type' => 'application/json',
'anthropic-beta' => 'prompt-caching-2024-07-31',
],
'body' => wp_json_encode( $request_body ),
] );
$duration_ms = (int) ( ( microtime( true ) - $started_at ) * 1000 );
if ( is_wp_error( $response ) ) {
[client]_log_ai_call( $args['task'], $model, 0, 0, $duration_ms, 'error',
$response->get_error_message() );
return [ 'ok' => false, 'error' => 'request_failed', 'content' => '', 'tokens' => 0 ];
}
$code = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( $code !== 200 || empty( $body['content'][0]['text'] ) ) {
$error_msg = $body['error']['message'] ?? "HTTP {$code}";
[client]_log_ai_call( $args['task'], $model, 0, 0, $duration_ms, 'error', $error_msg );
return [ 'ok' => false, 'error' => $error_msg, 'content' => '', 'tokens' => 0 ];
}
$text = $body['content'][0]['text'];
$total_tokens = ( $body['usage']['input_tokens'] ?? 0 )
+ ( $body['usage']['output_tokens'] ?? 0 );
$cost = [client]_estimate_cost( $model, $total_tokens );
[client]_log_ai_call( $args['task'], $model, $total_tokens, $cost, $duration_ms, 'ok', '' );
return [ 'ok' => true, 'content' => $text, 'tokens' => $total_tokens, 'cost' => $cost ];
}
The model for each call is determined by the task name, not by the caller. This is a policy decision made in one place, not spread across the codebase:
function [client]_select_ai_model( string $task ): string {
// Haiku for high-volume, low-stakes work
$haiku_tasks = [ 'description', 'classification', 'summary', 'nl_query', 'title_fix' ];
return in_array( $task, $haiku_tasks, true )
? 'claude-haiku-4-5-20251001'
: 'claude-sonnet-4-6'; // Sonnet for complex reasoning
}
Every AI call is logged to a dedicated table. The log is what answers cost questions, debugging questions, and “what did the AI generate for this unit?” questions later:
function [client]_log_ai_call(
string $task,
string $model,
int $tokens,
float $cost_usd,
int $duration_ms,
string $status,
string $error_message = ''
): void {
global $wpdb;
$wpdb->insert(
'[client]_ai_call_log',
[
'task' => $task,
'model' => $model,
'tokens' => $tokens,
'cost_usd' => $cost_usd,
'duration_ms' => $duration_ms,
'status' => $status,
'error_message' => $error_message,
'called_at' => current_time( 'mysql' ),
],
[ '%s', '%s', '%d', '%f', '%d', '%s', '%s', '%s' ]
);
}
The wrapper makes AI features composable. When Anthropic releases a new model, the task→model mapping changes in [client]_select_ai_model() and the update propagates to every feature. When you add a cost cap, it’s one budget check in one function. When you need to debug why a feature produced a specific output, the call log has the task, model, tokens, cost, duration, and timestamp for every call.
The consistent return shape — ['ok' => bool, 'content' => string, 'tokens' => int] — means every caller handles success and failure identically. There’s no feature that has its own error-handling idiom. Error handling is tested once; callers just check $result['ok'].
Three AI features ship through the same wrapper: description generation (Haiku, batch), NL search parsing (Haiku, per-query), and lead summary generation (Haiku, per-lead). When Haiku 4.5 was released and the model ID changed, one string in [client]_select_ai_model() updated all three features simultaneously. The call log provided the cost breakdown that justified continued AI investment: $3.20/day average across all three features combined.
['ok' => bool, 'content' => string] is predictable. Every caller can check $result['ok'] and handle the error case the same way. Inconsistent shapes produce inconsistent caller behavior.Every lesson stays free — no account, no paywall, no email gate, ever. But if you’d rather have this system standing on your business than wire all 48 lessons yourself, leave your email. We’ll send you a direct line to a build — and you’ll be first to hear when we add new tools to the curriculum.
None of this gates a single lesson. The curriculum was free before you got here and it stays that way.
You came here to understand the system, and now you do. If you’d rather have it standing on your business than spend the next three months wiring it yourself, GAP Concierge is the same architecture from these lessons — a white-label AI agent that knows your catalog and captures your leads — set up for you, from $97/mo.
See GAP Concierge →