diff --git a/README.md b/README.md index 20e0e2e..f270cd6 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,15 @@ studio --path "$SITE_PATH" wp option update \ connectors_ai_anthropic_api_key "$ANTHROPIC_API_KEY" ``` +> **Cost note.** The bundled demo agents use `model: auto`, so the active +> provider picks the model — which on a cloud provider is its *default*, often +> the priciest (e.g. Opus). To control spend, set each agent's +> `default_config['model']` to a specific id the provider exposes (e.g. a dated +> Anthropic Haiku id). If a configured model id doesn't resolve, wp-ai-client +> silently falls back to the provider default; openclaWP logs that substitution +> as `[openclawp] model_substitution …` so you can catch it. Pair with the +> per-site budget caps (`budget_daily_usd`, `budget_daily_turns`). + ### Path B — an existing WordPress site If a site already exists (Pressable, a VPS, [Local](https://localwp.com/), your own Docker, …), drop the plugins in. Anthropic shown to keep the block short — the Ollama recipe in Path A applies verbatim, just call `wp` instead of `studio --path … wp`. diff --git a/includes/class-openclawp-agent-registrar.php b/includes/class-openclawp-agent-registrar.php index e085487..d77ca1c 100644 --- a/includes/class-openclawp-agent-registrar.php +++ b/includes/class-openclawp-agent-registrar.php @@ -53,7 +53,7 @@ public static function maybe_register_site_introspection_agent(): void { 'owner_resolver' => static fn(): int => get_current_user_id(), 'default_config' => array( 'provider' => 'auto', - 'model' => 'claude-haiku-4-5', + 'model' => 'auto', 'tools' => array( 'openclawp/get-recent-posts', 'openclawp/count-comments', @@ -93,7 +93,7 @@ public static function maybe_register_loop_demo_agent(): void { 'owner_resolver' => static fn(): int => get_current_user_id(), 'default_config' => array( 'provider' => 'auto', - 'model' => 'claude-haiku-4-5', + 'model' => 'auto', 'tools' => array( 'openclawp/get-time' ), 'max_turns' => 5, ), @@ -131,7 +131,7 @@ public static function maybe_register_example_agent(): void { 'owner_resolver' => static fn(): int => get_current_user_id(), 'default_config' => array( 'provider' => 'auto', - 'model' => 'claude-haiku-4-5', + 'model' => 'auto', ), 'meta' => array( 'source_plugin' => 'openclawp/openclawp.php', @@ -177,7 +177,7 @@ public static function maybe_register_workflow_drafter_agent(): void { 'owner_resolver' => static fn(): int => get_current_user_id(), 'default_config' => array( 'provider' => 'auto', - 'model' => 'claude-haiku-4-5', + 'model' => 'auto', ), 'meta' => array( 'source_plugin' => 'openclawp/openclawp.php', @@ -305,7 +305,7 @@ public static function maybe_register_coordinator_demo_agent(): void { 'owner_resolver' => static fn(): int => get_current_user_id(), 'default_config' => array( 'provider' => 'auto', - 'model' => 'claude-haiku-4-5', + 'model' => 'auto', 'max_turns' => 6, ), 'subagents' => array( diff --git a/includes/class-openclawp-runner.php b/includes/class-openclawp-runner.php index de6a3db..405e6d8 100644 --- a/includes/class-openclawp-runner.php +++ b/includes/class-openclawp-runner.php @@ -368,6 +368,17 @@ private static function build_turn_runner( WP_Agent $agent, array $session, stri if ( null !== $preference ) { $builder = $builder->using_model_preference( $preference ); } + // The model id the agent asked for (empty when 'auto'/unset). Used + // below to flag silent provider substitution — wp-ai-client falls + // back to the provider's default model (often the priciest) when the + // requested id isn't one the active provider exposes, rather than + // erroring, which has surprised installs with unexpected billing. + $requested_model_id = ''; + if ( is_array( $preference ) ) { + $requested_model_id = (string) ( $preference[1] ?? '' ); + } elseif ( is_string( $preference ) ) { + $requested_model_id = $preference; + } $start_us = microtime( true ); $generated = $builder->generate_text_result(); @@ -390,6 +401,25 @@ private static function build_turn_runner( WP_Agent $agent, array $session, stri $telemetry['model'] = self::extract_model_id( $generated ); $telemetry['token_usage'] = self::extract_token_usage( $generated ); + // Surface silent model substitution: the agent asked for one model + // but the provider answered with another (its default fallback). This + // is otherwise invisible and can mean paying for a premium model + // (e.g. an unrecognised id resolving to Opus). Record it on the turn + // telemetry and warn once per turn. + $used_model = (string) $telemetry['model']; + if ( '' !== $requested_model_id && '' !== $used_model && $requested_model_id !== $used_model ) { + $telemetry['model_requested'] = $requested_model_id; + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Matches openclaWP's telemetry channel (grep error_log for [openclawp]). + error_log( + sprintf( + '[openclawp] model_substitution agent=%s requested=%s used=%s — the configured model id did not resolve on the active provider and silently fell back. Pin a model id the provider exposes to control cost.', + (string) $agent_obj->get_slug(), + $requested_model_id, + $used_model + ) + ); + } + $tool_calls = self::extract_tool_calls( $generated ); $telemetry['tool_call_count'] = count( $tool_calls ); // `toText()` throws "No text content found in first candidate" diff --git a/tests/integration/prompt-assembly/__snapshots__/coordinator--chat.json b/tests/integration/prompt-assembly/__snapshots__/coordinator--chat.json index 3d3c7b3..d90709a 100644 --- a/tests/integration/prompt-assembly/__snapshots__/coordinator--chat.json +++ b/tests/integration/prompt-assembly/__snapshots__/coordinator--chat.json @@ -12,9 +12,7 @@ "role": "user" } ], - "model_preference": { - "model": "claude-haiku-4-5" - }, + "model_preference": null, "runtime_context": { "client_context": { "client_name": "chat-block", diff --git a/tests/integration/prompt-assembly/__snapshots__/example--whatsapp.json b/tests/integration/prompt-assembly/__snapshots__/example--whatsapp.json index 6d0b69c..92140b3 100644 --- a/tests/integration/prompt-assembly/__snapshots__/example--whatsapp.json +++ b/tests/integration/prompt-assembly/__snapshots__/example--whatsapp.json @@ -9,9 +9,7 @@ "role": "user" } ], - "model_preference": { - "model": "claude-haiku-4-5" - }, + "model_preference": null, "runtime_context": { "client_context": { "client_name": "whatsapp", diff --git a/tests/integration/prompt-assembly/__snapshots__/loop-demo--chat.json b/tests/integration/prompt-assembly/__snapshots__/loop-demo--chat.json index 5e6386e..6e4ca67 100644 --- a/tests/integration/prompt-assembly/__snapshots__/loop-demo--chat.json +++ b/tests/integration/prompt-assembly/__snapshots__/loop-demo--chat.json @@ -9,9 +9,7 @@ "role": "user" } ], - "model_preference": { - "model": "claude-haiku-4-5" - }, + "model_preference": null, "runtime_context": { "client_context": { "client_name": "chat-block", diff --git a/tests/integration/prompt-assembly/__snapshots__/site-introspection--whatsapp.json b/tests/integration/prompt-assembly/__snapshots__/site-introspection--whatsapp.json index 238598b..3b21a13 100644 --- a/tests/integration/prompt-assembly/__snapshots__/site-introspection--whatsapp.json +++ b/tests/integration/prompt-assembly/__snapshots__/site-introspection--whatsapp.json @@ -9,9 +9,7 @@ "role": "user" } ], - "model_preference": { - "model": "claude-haiku-4-5" - }, + "model_preference": null, "runtime_context": { "client_context": { "client_name": "whatsapp", diff --git a/tests/integration/prompt-assembly/__snapshots__/workflow-drafter--chat.json b/tests/integration/prompt-assembly/__snapshots__/workflow-drafter--chat.json index 1669017..b6198fb 100644 --- a/tests/integration/prompt-assembly/__snapshots__/workflow-drafter--chat.json +++ b/tests/integration/prompt-assembly/__snapshots__/workflow-drafter--chat.json @@ -9,9 +9,7 @@ "role": "user" } ], - "model_preference": { - "model": "claude-haiku-4-5" - }, + "model_preference": null, "runtime_context": { "client_context": { "client_name": "chat-block",