diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8e9da871e..bebc2956f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,18 @@ All notable changes to solstone (the Python package) will be documented in this
Format adapted from [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), aligned with `cmo/brand/changelog-voice.md`.
+## [Unreleased]
+
+### Changed
+
+- sol now keeps one active brain for each lane instead of switching providers behind the scenes. old tier, context-routing, and backup keys are left in your config untouched, but they no longer steer model choice.
+- paths that used to be pinned to lite-class models now use the single model chosen for that lane. for byo-token owners this can raise per-token cost on those paths; local-lane installs are unaffected.
+- a legacy config with both `tier` and `model` now honors the model you named instead of ignoring it.
+
+### Fixed
+
+- timeline rollups now work on local-lane installs. they used to pin a cloud model internally, which made local setups fail before the rollup could run.
+
## [0.8.4] - 2026-07-12
### Added
diff --git a/solstone/apps/entities/routes.py b/solstone/apps/entities/routes.py
index 787e3abd4..a41823757 100644
--- a/solstone/apps/entities/routes.py
+++ b/solstone/apps/entities/routes.py
@@ -1301,10 +1301,8 @@ def generate_description(facet_name: str) -> Any:
def _entity_describe_generate_readiness_error() -> Any | None:
from solstone.think.models import resolve_provider
from solstone.think.providers.state import readiness_for_provider
- from solstone.think.talent import key_to_context
- context = key_to_context("entities:entity_describe")
- provider, model = resolve_provider(context, "generate")
+ provider, model = resolve_provider("generate")
readiness = readiness_for_provider(provider, "generate", model)
if readiness.status not in {"blocked", "unhealthy"}:
return None
diff --git a/solstone/apps/entities/talent/detection.md b/solstone/apps/entities/talent/detection.md
index dae33410f..26463d945 100644
--- a/solstone/apps/entities/talent/detection.md
+++ b/solstone/apps/entities/talent/detection.md
@@ -5,7 +5,6 @@
"color": "#00695c",
"schedule": "segment",
"priority": 15,
- "provider": "google",
"thinking_budget": 2048,
"max_output_tokens": 1024,
"output": "json",
diff --git a/solstone/apps/entities/talent/entities_review.md b/solstone/apps/entities/talent/entities_review.md
index e6cf13135..32fd12757 100644
--- a/solstone/apps/entities/talent/entities_review.md
+++ b/solstone/apps/entities/talent/entities_review.md
@@ -1,6 +1,5 @@
{
"type": "generate",
- "tier": 2,
"title": "Entity Reviewer",
"description": "Reviews detected entities and promotes recurring ones to attached status",
"color": "#00796b",
diff --git a/solstone/apps/entities/talent/entity_observer.md b/solstone/apps/entities/talent/entity_observer.md
index b14eaff4f..1b6da966e 100644
--- a/solstone/apps/entities/talent/entity_observer.md
+++ b/solstone/apps/entities/talent/entity_observer.md
@@ -1,6 +1,5 @@
{
"type": "generate",
- "tier": 2,
"title": "Entity Observer",
"description": "Extracts durable factoids about attached entities from journal content",
diff --git a/solstone/apps/settings/call.py b/solstone/apps/settings/call.py
index bd1d13294..c6285f448 100644
--- a/solstone/apps/settings/call.py
+++ b/solstone/apps/settings/call.py
@@ -338,8 +338,6 @@ def providers_clear_local_endpoint() -> None:
@providers_app.command("set-generate")
def providers_set_generate(
provider: str | None = typer.Option(None, "--provider", help="Primary provider."),
- tier: int | None = typer.Option(None, "--tier", help="Tier (1, 2, or 3)."),
- backup: str | None = typer.Option(None, "--backup", help="Backup provider."),
) -> None:
"""Moved to ``sol call thinking providers set-generate``."""
@@ -349,8 +347,6 @@ def providers_set_generate(
@providers_app.command("set-cogitate")
def providers_set_cogitate(
provider: str | None = typer.Option(None, "--provider", help="Primary provider."),
- tier: int | None = typer.Option(None, "--tier", help="Tier (1, 2, or 3)."),
- backup: str | None = typer.Option(None, "--backup", help="Backup provider."),
) -> None:
"""Moved to ``sol call thinking providers set-cogitate``."""
diff --git a/solstone/apps/settings/tests/conftest.py b/solstone/apps/settings/tests/conftest.py
index 46bd71b69..c8243de05 100644
--- a/solstone/apps/settings/tests/conftest.py
+++ b/solstone/apps/settings/tests/conftest.py
@@ -47,13 +47,11 @@ def settings_env(tmp_path, monkeypatch):
"providers": {
"generate": {
"provider": "google",
- "tier": 2,
- "backup": "anthropic",
+ "model": "gemini-flash-latest",
},
"cogitate": {
"provider": "openai",
- "tier": 2,
- "backup": "anthropic",
+ "model": "gpt-5.4-mini",
},
"auth": {
"google": "api_key",
diff --git a/solstone/apps/sol/maint/006_rename_unified_triage_providers.py b/solstone/apps/sol/maint/006_rename_unified_triage_providers.py
index 972101fad..8919d8270 100644
--- a/solstone/apps/sol/maint/006_rename_unified_triage_providers.py
+++ b/solstone/apps/sol/maint/006_rename_unified_triage_providers.py
@@ -1,26 +1,18 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright (c) 2026 sol pbc
-"""Rename legacy unified and triage provider contexts for the chat refactor."""
+"""Retired provider-context rename for the chat refactor."""
from __future__ import annotations
import argparse
-import json
import logging
import sys
from dataclasses import dataclass
from pathlib import Path
-from solstone.think.journal_config import write_journal_config
-from solstone.think.utils import get_journal, setup_cli
-
logger = logging.getLogger(__name__)
-_UNIFIED_CONTEXT = "talent.system.unified"
-_CHAT_CONTEXT = "talent.system.chat"
-_TRIAGE_CONTEXT = "talent.system.triage"
-
@dataclass
class MigrationSummary:
@@ -32,73 +24,8 @@ class MigrationSummary:
def run_migration(journal_path: Path, *, dry_run: bool) -> MigrationSummary:
- summary = MigrationSummary()
- config_path = journal_path / "config" / "journal.json"
-
- if not config_path.exists():
- summary.skipped_reason = "no file"
- return summary
-
- try:
- raw_bytes = config_path.read_bytes()
- except OSError:
- logger.exception("Failed to read %s", config_path)
- summary.errors += 1
- return summary
-
- if not raw_bytes.strip():
- summary.skipped_reason = "empty file"
- return summary
-
- try:
- raw = json.loads(raw_bytes)
- except json.JSONDecodeError:
- summary.skipped_reason = "unparseable"
- return summary
-
- if not isinstance(raw, dict):
- summary.skipped_reason = "unparseable"
- return summary
-
- providers = raw.get("providers")
- if not isinstance(providers, dict):
- summary.skipped_reason = "no providers"
- return summary
-
- contexts = providers.get("contexts")
- if not isinstance(contexts, dict):
- summary.skipped_reason = "no contexts"
- return summary
-
- changed = False
- if _UNIFIED_CONTEXT in contexts:
- legacy_chat = contexts[_UNIFIED_CONTEXT]
- if _CHAT_CONTEXT not in contexts:
- contexts[_CHAT_CONTEXT] = legacy_chat
- summary.renamed += 1
- else:
- summary.preserved += 1
- del contexts[_UNIFIED_CONTEXT]
- changed = True
-
- if _TRIAGE_CONTEXT in contexts:
- del contexts[_TRIAGE_CONTEXT]
- summary.removed += 1
- changed = True
-
- if not changed:
- return summary
-
- if dry_run:
- return summary
-
- try:
- write_journal_config(raw)
- except OSError:
- logger.exception("Failed to write %s", config_path)
- summary.errors += 1
-
- return summary
+ _ = (journal_path, dry_run)
+ return MigrationSummary(skipped_reason="retired")
def _print_summary(summary: MigrationSummary) -> None:
@@ -118,10 +45,10 @@ def main() -> None:
action="store_true",
help="Preview the provider-context rename without writing files.",
)
- args = setup_cli(parser)
+ args = parser.parse_args()
logging.basicConfig(level=logging.INFO, format="%(message)s")
- journal_path = Path(get_journal())
+ journal_path = Path.cwd()
summary = run_migration(journal_path, dry_run=args.dry_run)
_print_summary(summary)
diff --git a/solstone/apps/thinking/call.py b/solstone/apps/thinking/call.py
index fdacb1d90..bec9a1f6b 100644
--- a/solstone/apps/thinking/call.py
+++ b/solstone/apps/thinking/call.py
@@ -136,11 +136,6 @@ def _validate_provider_or_exit(provider: str, *, cloud_only: bool = False) -> No
_exit_with(f"Invalid provider: {provider}. Must be one of: {', '.join(valid)}")
-def _validate_tier_or_exit(tier: int | None) -> None:
- if tier is not None and tier not in {1, 2, 3}:
- _exit_with(f"Invalid tier: {tier}. Must be one of: 1, 2, 3")
-
-
def _get_providers() -> dict[str, Any]:
return _request("GET", "/app/thinking/api/providers")
@@ -420,20 +415,15 @@ def providers_show(
def _set_provider_type(
agent_type: str,
provider: str | None,
- tier: int | None,
- backup: str | None,
+ model: str | None,
) -> dict[str, Any]:
if provider is not None:
_validate_provider_or_exit(provider)
- if backup is not None:
- _validate_provider_or_exit(backup)
- _validate_tier_or_exit(tier)
payload = {
key: value
for key, value in {
"provider": provider,
- "tier": tier,
- "backup": backup,
+ "model": model,
}.items()
if value is not None
}
@@ -454,24 +444,22 @@ def _set_provider_type(
@convey_cli
def providers_set_generate(
provider: str | None = typer.Option(None, "--provider", help="Primary provider."),
- tier: int | None = typer.Option(None, "--tier", help="Tier (1, 2, or 3)."),
- backup: str | None = typer.Option(None, "--backup", help="Backup provider."),
+ model: str | None = typer.Option(None, "--model", help="Model override."),
) -> None:
"""Set generate provider defaults."""
- _echo_json(_set_provider_type("generate", provider, tier, backup))
+ _echo_json(_set_provider_type("generate", provider, model))
@providers_app.command("set-cogitate")
@convey_cli
def providers_set_cogitate(
provider: str | None = typer.Option(None, "--provider", help="Primary provider."),
- tier: int | None = typer.Option(None, "--tier", help="Tier (1, 2, or 3)."),
- backup: str | None = typer.Option(None, "--backup", help="Backup provider."),
+ model: str | None = typer.Option(None, "--model", help="Model override."),
) -> None:
"""Set cogitate provider defaults."""
- _echo_json(_set_provider_type("cogitate", provider, tier, backup))
+ _echo_json(_set_provider_type("cogitate", provider, model))
@app.command("set-local-endpoint")
diff --git a/solstone/apps/thinking/routes.py b/solstone/apps/thinking/routes.py
index f7ead3939..46f5744d9 100644
--- a/solstone/apps/thinking/routes.py
+++ b/solstone/apps/thinking/routes.py
@@ -48,7 +48,6 @@ from solstone.think.journal_config import (
from solstone.think.models import (
LOCAL_MODEL,
NO_BRAIN_PROVIDER,
- TYPE_DEFAULTS,
resolve_provider,
)
from solstone.think.providers import (
@@ -96,7 +95,6 @@ AI_ENV_TO_PROVIDER = {
"OPENAI_API_KEY": "openai",
}
AI_PROVIDERS = frozenset(AI_ENV_TO_PROVIDER.values())
-VALID_TIERS = {1, 2, 3}
LANES = {"byo", "confidential", "local"}
GENERIC_THINKING_ERROR = (
"something went wrong - try again, and if it persists, check the health dashboard"
@@ -335,15 +333,13 @@ def _validate_local_endpoint_url(endpoint_url: str) -> str | Any:
def _type_settings(providers_config: dict[str, Any]) -> dict[str, dict[str, Any]]:
settings: dict[str, dict[str, Any]] = {}
for agent_type in ("generate", "cogitate"):
- defaults = TYPE_DEFAULTS[agent_type]
type_config = providers_config.get(agent_type, {})
if not isinstance(type_config, dict):
type_config = {}
- provider, _ = resolve_provider("", agent_type)
+ provider, model = resolve_provider(agent_type)
settings[agent_type] = {
"provider": provider,
- "tier": type_config.get("tier", defaults["tier"]),
- "backup": type_config.get("backup", defaults["backup"]),
+ "model": type_config.get("model") or model,
}
return settings
@@ -461,35 +457,11 @@ def _compute_ai_key_validation(config: dict[str, Any]) -> dict[str, Any]:
def _provider_payload(config: dict[str, Any], local_model_id: str) -> dict[str, Any]:
- from solstone.think.models import get_context_registry, resolve_effective_route
- from solstone.think.talent import get_talent_configs, key_to_context
-
providers_config = config.get("providers", {})
if not isinstance(providers_config, dict):
providers_config = {}
type_settings = _type_settings(providers_config)
- contexts = providers_config.get("contexts", {})
- if not isinstance(contexts, dict):
- contexts = {}
-
- context_defaults: dict[str, dict[str, Any]] = {}
- for pattern, ctx_config in get_context_registry().items():
- context_defaults[pattern] = {
- "tier": ctx_config["tier"],
- "label": ctx_config["label"],
- "group": ctx_config["group"],
- }
- if "type" in ctx_config:
- context_defaults[pattern]["type"] = ctx_config["type"]
-
- talent_configs = get_talent_configs(include_disabled=True)
- for key, info in talent_configs.items():
- context_key = key_to_context(key)
- if context_key in context_defaults:
- if "schedule" in info:
- context_defaults[context_key]["schedule"] = info["schedule"]
- context_defaults[context_key]["disabled"] = info.get("disabled", False)
providers_list = get_provider_list()
vertex_creds_path = providers_config.get("vertex_credentials")
@@ -508,26 +480,6 @@ def _provider_payload(config: dict[str, Any], local_model_id: str) -> dict[str,
local_model_id=local_model_id,
include_local=True,
)
- effective_contexts: dict[str, dict[str, Any]] = {}
- for pattern, raw_ctx in contexts.items():
- try:
- interface, provider, model = resolve_effective_route(pattern)
- raw_provider = (
- raw_ctx.get("provider") if isinstance(raw_ctx, dict) else None
- )
- raw_model = raw_ctx.get("model") if isinstance(raw_ctx, dict) else None
- effective_contexts[pattern] = {
- "interface": interface,
- "provider": provider,
- "model": model,
- "differs_from_raw": (
- (raw_provider is not None and raw_provider != provider)
- or (raw_model is not None and raw_model != model)
- ),
- }
- except Exception:
- logger.exception("error resolving effective route for context %s", pattern)
- effective_contexts[pattern] = {"unavailable": True}
return {
"providers": providers_list,
@@ -539,9 +491,6 @@ def _provider_payload(config: dict[str, Any], local_model_id: str) -> dict[str,
"active_lane": _active_lane_payload(type_settings),
"generate": type_settings["generate"],
"cogitate": type_settings["cogitate"],
- "contexts": contexts,
- "effective_contexts": effective_contexts,
- "context_defaults": context_defaults,
"api_keys": _api_key_status(config),
"key_validation": _filtered_ai_key_validation(config),
"local": local_status,
@@ -1069,29 +1018,31 @@ def _apply_type_update(
config["providers"][agent_type]["provider"] = provider
if "tier" in type_data:
- tier = type_data["tier"]
- if tier not in VALID_TIERS:
+ return error_response(
+ INVALID_CONFIG_VALUE,
+ detail=f"{agent_type}.tier is retired; set provider/model instead.",
+ )
+
+ if "backup" in type_data:
+ return error_response(
+ INVALID_CONFIG_VALUE,
+ detail=f"{agent_type}.backup is retired; automatic provider switching is disabled.",
+ )
+
+ if "model" in type_data:
+ model = type_data["model"]
+ if not isinstance(model, str) or not model.strip():
return error_response(
INVALID_CONFIG_VALUE,
- detail=f"Invalid tier: {tier}. Must be 1, 2, or 3.",
+ detail=f"{agent_type}.model must be a non-empty string.",
)
- if old_type.get("tier") != tier:
- changed_fields[f"{agent_type}.tier"] = {
- "old": old_type.get("tier"),
- "new": tier,
+ model = model.strip()
+ if old_type.get("model") != model:
+ changed_fields[f"{agent_type}.model"] = {
+ "old": old_type.get("model"),
+ "new": model,
}
- config["providers"][agent_type]["tier"] = tier
-
- if "backup" in type_data:
- backup = _validate_provider(type_data["backup"], "backup provider")
- if not isinstance(backup, str):
- return backup
- if old_type.get("backup") != backup:
- changed_fields[f"{agent_type}.backup"] = {
- "old": old_type.get("backup"),
- "new": backup,
- }
- config["providers"][agent_type]["backup"] = backup
+ config["providers"][agent_type]["model"] = model
return None
@@ -1150,14 +1101,29 @@ def update_providers() -> Any:
config.setdefault("providers", {})
changed_fields: dict[str, Any] = {}
+ for legacy_key in ("tier", "backup"):
+ if legacy_key in request_data:
+ return error_response(
+ INVALID_CONFIG_VALUE,
+ detail=(
+ f"{legacy_key} is retired; configure generate/cogitate "
+ "provider/model instead."
+ ),
+ )
+ if "contexts" in request_data:
+ return error_response(
+ INVALID_CONFIG_VALUE,
+ detail=(
+ "providers.contexts routing is retired; use /api/generators "
+ "for disabled/extract toggles."
+ ),
+ )
+
if "lane" in request_data:
provider = _lane_provider(request_data)
if not isinstance(provider, str):
return provider
lane_update = {"provider": provider}
- for optional in ("tier", "backup"):
- if optional in request_data:
- lane_update[optional] = request_data[optional]
for agent_type in ("generate", "cogitate"):
error = _apply_type_update(
config,
@@ -1185,71 +1151,6 @@ def update_providers() -> Any:
if error is not None:
return error
- if "contexts" in request_data:
- contexts_data = request_data["contexts"]
- if not isinstance(contexts_data, dict):
- return error_response(INVALID_REQUEST_VALUE, detail="contexts")
- config["providers"].setdefault("contexts", {})
- old_contexts = old_providers.get("contexts", {})
- for pattern, ctx_config in contexts_data.items():
- old_ctx = old_contexts.get(pattern)
- if ctx_config is None:
- if pattern in config["providers"]["contexts"]:
- changed_fields[f"contexts.{pattern}"] = {
- "old": old_ctx,
- "new": None,
- }
- del config["providers"]["contexts"][pattern]
- continue
- if not isinstance(ctx_config, dict):
- return error_response(
- INVALID_CONFIG_VALUE,
- detail=f"context for {pattern} must be an object or null",
- )
- if "provider" in ctx_config:
- provider = _validate_provider(
- ctx_config["provider"],
- f"provider for {pattern}",
- )
- if not isinstance(provider, str):
- return provider
- if "tier" in ctx_config and ctx_config["tier"] not in VALID_TIERS:
- return error_response(
- INVALID_CONFIG_VALUE,
- detail=f"Invalid tier for {pattern}: {ctx_config['tier']}",
- )
- if "disabled" in ctx_config and not isinstance(
- ctx_config["disabled"],
- bool,
- ):
- return error_response(
- INVALID_CONFIG_VALUE,
- detail=f"disabled for {pattern} must be a boolean",
- )
- if "extract" in ctx_config and not isinstance(
- ctx_config["extract"],
- bool,
- ):
- return error_response(
- INVALID_CONFIG_VALUE,
- detail=f"extract for {pattern} must be a boolean",
- )
- if "model" in ctx_config:
- model_value = ctx_config["model"]
- if not isinstance(model_value, str) or not model_value.strip():
- return error_response(
- INVALID_CONFIG_VALUE,
- detail=f"model for {pattern} must be a non-empty string",
- )
- ctx_config["model"] = model_value.strip()
- if ctx_config:
- if old_ctx != ctx_config:
- changed_fields[f"contexts.{pattern}"] = {
- "old": old_ctx,
- "new": ctx_config,
- }
- config["providers"]["contexts"][pattern] = ctx_config
-
if "google_backend" in request_data:
backend = request_data["google_backend"]
if backend not in ("auto", "aistudio", "vertex"):
diff --git a/solstone/apps/thinking/static/thinking.js b/solstone/apps/thinking/static/thinking.js
index 9ecdf9128..050f14c31 100644
--- a/solstone/apps/thinking/static/thinking.js
+++ b/solstone/apps/thinking/static/thinking.js
@@ -1073,12 +1073,6 @@
function renderAdvanced() {
populateProviderSelect($('field-generate-provider'), state.providers.generate?.provider);
populateProviderSelect($('field-cogitate-provider'), state.providers.cogitate?.provider);
- if ($('field-generate-tier')) {
- $('field-generate-tier').value = String(state.providers.generate?.tier || 2);
- }
- if ($('field-cogitate-tier')) {
- $('field-cogitate-tier').value = String(state.providers.cogitate?.tier || 2);
- }
if ($('field-google-backend')) {
$('field-google-backend').value = state.providers.google_backend || 'auto';
}
@@ -1749,7 +1743,7 @@
}
async function saveAdvanced(agentType, field, value) {
- const payload = {[agentType]: {[field]: field === 'tier' ? Number(value) : value}};
+ const payload = {[agentType]: {[field]: value}};
state.providers = await api('api/providers', {
method: 'PUT',
body: JSON.stringify(payload),
@@ -1957,8 +1951,6 @@
});
$('field-generate-provider')?.addEventListener('change', (event) => saveAdvanced('generate', 'provider', event.target.value).catch((err) => setMessage('advancedStatus', err.message, 'error')));
$('field-cogitate-provider')?.addEventListener('change', (event) => saveAdvanced('cogitate', 'provider', event.target.value).catch((err) => setMessage('advancedStatus', err.message, 'error')));
- $('field-generate-tier')?.addEventListener('change', (event) => saveAdvanced('generate', 'tier', event.target.value).catch((err) => setMessage('advancedStatus', err.message, 'error')));
- $('field-cogitate-tier')?.addEventListener('change', (event) => saveAdvanced('cogitate', 'tier', event.target.value).catch((err) => setMessage('advancedStatus', err.message, 'error')));
$('field-google-backend')?.addEventListener('change', () => saveGoogleBackend().catch((err) => setMessage('vertexStatus', err.message, 'error')));
$('vertexSave')?.addEventListener('click', () => saveVertexCredentials().catch((err) => setMessage('vertexStatus', err.message, 'error')));
$('vertexClear')?.addEventListener('click', () => clearVertexCredentials().catch((err) => setMessage('vertexStatus', err.message, 'error')));
diff --git a/solstone/apps/thinking/tests/conftest.py b/solstone/apps/thinking/tests/conftest.py
index 4e95b1dfe..0eb9ce0d1 100644
--- a/solstone/apps/thinking/tests/conftest.py
+++ b/solstone/apps/thinking/tests/conftest.py
@@ -63,22 +63,11 @@ def settings_env(tmp_path, monkeypatch):
"providers": {
"generate": {
"provider": "google",
- "tier": 2,
- "backup": "anthropic",
+ "model": "gemini-flash-latest",
},
"cogitate": {
"provider": "openai",
- "tier": 2,
- "backup": "anthropic",
- },
- "contexts": {
- "work": {
- "provider": "google",
- "tier": 2,
- }
- },
- "models": {
- "generate": "gemini-2.5-pro",
+ "model": "gpt-5.4-mini",
},
"auth": {
"google": "api_key",
diff --git a/solstone/apps/thinking/tests/test_local_bootstrap_routes.py b/solstone/apps/thinking/tests/test_local_bootstrap_routes.py
index 433f2f752..c020dbaa1 100644
--- a/solstone/apps/thinking/tests/test_local_bootstrap_routes.py
+++ b/solstone/apps/thinking/tests/test_local_bootstrap_routes.py
@@ -38,8 +38,8 @@ def _settings_config() -> dict:
return {
"setup": {"completed_at": "2026-05-09T00:00:00Z"},
"providers": {
- "generate": {"provider": "google", "tier": 2, "backup": "anthropic"},
- "cogitate": {"provider": "openai", "tier": 2, "backup": "anthropic"},
+ "generate": {"provider": "google", "model": "gemini-flash-latest"},
+ "cogitate": {"provider": "openai", "model": "gpt-5.4-mini"},
"auth": {"google": "api_key", "openai": "api_key"},
},
}
diff --git a/solstone/apps/thinking/tests/test_providers_payload_extended.py b/solstone/apps/thinking/tests/test_providers_payload_extended.py
index 9a525ecac..073a5c202 100644
--- a/solstone/apps/thinking/tests/test_providers_payload_extended.py
+++ b/solstone/apps/thinking/tests/test_providers_payload_extended.py
@@ -66,7 +66,7 @@ def _assert_install_status(payload: dict) -> None:
def _patch_selected_providers(monkeypatch, *, provider: str = "google") -> None:
monkeypatch.setattr(
"solstone.think.models.resolve_provider",
- lambda _context, _interface: (provider, f"{provider}-model"),
+ lambda _interface: (provider, f"{provider}-model"),
)
@@ -104,7 +104,7 @@ def _assert_ai_readiness_shape(payload: dict) -> None:
"recovery_action",
"operator_detail",
}
- assert set(ai_readiness) >= {"summary", "interfaces", "context_routes", "groups"}
+ assert set(ai_readiness) >= {"summary", "interfaces", "groups"}
assert set(ai_readiness["summary"]) == {
"status",
"severity",
@@ -112,11 +112,8 @@ def _assert_ai_readiness_shape(payload: dict) -> None:
"blocked_count",
}
assert set(ai_readiness["interfaces"]) == {"generate", "cogitate"}
- assert isinstance(ai_readiness["context_routes"], list)
for view in ai_readiness["interfaces"].values():
assert set(view) == expected_view_keys
- for view in ai_readiness["context_routes"]:
- assert set(view) == expected_view_keys
if ai_readiness.get("local") is not None:
assert set(ai_readiness["local"]) == expected_view_keys
@@ -194,21 +191,44 @@ def test_get_providers_reports_advanced_when_generate_and_cogitate_lanes_split(
assert payload["active_lane"]["split"] is True
-@pytest.mark.parametrize("model_value", [""])
-def test_context_update_rejects_invalid_model_value(
- settings_client_with_journal, model_value
-):
+def test_provider_update_rejects_context_payload(settings_client_with_journal):
client, _journal_path = settings_client_with_journal
response = client.put(
"/app/thinking/api/providers",
- json={"contexts": {"talent.x": {"provider": "local", "model": model_value}}},
+ json={"contexts": {"talent.x": {"provider": "local", "model": ""}}},
)
assert response.status_code != 200
payload = response.get_json()
assert payload["reason_code"] == "invalid_config_value"
- assert "talent.x" in payload["detail"]
+ assert "providers.contexts routing is retired" in payload["detail"]
+
+
+@pytest.mark.parametrize(
+ "payload",
+ [
+ {"tier": 2},
+ {"backup": "anthropic"},
+ {"generate": {"tier": 2}},
+ {"cogitate": {"backup": "anthropic"}},
+ ],
+)
+def test_provider_update_rejects_retired_routing_keys(
+ settings_client_with_journal,
+ payload,
+):
+ client, journal_path = settings_client_with_journal
+ config_path = journal_path / "config" / "journal.json"
+ before = config_path.read_bytes()
+
+ response = client.put("/app/thinking/api/providers", json=payload)
+
+ assert response.status_code == 400
+ body = response.get_json()
+ assert body["reason_code"] == "invalid_config_value"
+ assert "retired" in body["detail"]
+ assert config_path.read_bytes() == before
def test_scout_enabled_google_provider_derives_byo_with_provenance(
@@ -795,91 +815,6 @@ def test_get_providers_ai_readiness_shape(settings_client):
assert payload["ai_readiness"]["local"]["provider"] == "local"
-def test_providers_payload_includes_effective_contexts(settings_client):
- response = settings_client.get("/app/thinking/api/providers")
-
- assert response.status_code == 200
- payload = response.get_json()
- effective_contexts = payload["effective_contexts"]
- assert isinstance(effective_contexts, dict)
- for pattern in payload["contexts"]:
- entry = effective_contexts[pattern]
- assert set(entry) == {
- "interface",
- "provider",
- "model",
- "differs_from_raw",
- }
-
-
-def test_effective_contexts_show_local_resolution_differences(
- settings_client_with_journal,
-):
- client, journal_path = settings_client_with_journal
- config = json.loads((journal_path / "config" / "journal.json").read_text())
- config["providers"]["generate"]["provider"] = "local"
- config["providers"]["contexts"]["talent.cloud.pin"] = {
- "provider": "google",
- "model": "gemini-flash-lite-latest",
- }
- config["providers"]["contexts"]["talent.local.blank"] = {
- "provider": "local",
- "model": "",
- }
- _write_config(journal_path, config)
-
- response = client.get("/app/thinking/api/providers")
-
- assert response.status_code == 200
- effective_contexts = response.get_json()["effective_contexts"]
- cloud_pin = effective_contexts["talent.cloud.pin"]
- assert cloud_pin["interface"] == "generate"
- assert cloud_pin["provider"] == "local"
- assert cloud_pin["model"] == LOCAL_MODEL
- assert cloud_pin["differs_from_raw"] is True
- blank_pin = effective_contexts["talent.local.blank"]
- assert blank_pin["provider"] == "local"
- assert blank_pin["model"]
- assert blank_pin["model"] != ""
- assert blank_pin["differs_from_raw"] is True
-
-
-def test_ai_readiness_context_routes_use_effective_local_route(
- settings_client_with_journal, monkeypatch
-):
- client, journal_path = settings_client_with_journal
- config = json.loads((journal_path / "config" / "journal.json").read_text())
- config["providers"]["generate"]["provider"] = "local"
- config["providers"]["contexts"]["talent.cloud.pin"] = {
- "provider": "google",
- "model": "gemini-flash-lite-latest",
- }
- config["providers"]["contexts"]["talent.local.blank"] = {
- "provider": "local",
- "model": "",
- }
- _write_config(journal_path, config)
-
- def fake_readiness(provider: str, interface: str, model: str):
- return ProviderState(
- provider=provider,
- interface=interface,
- status="ready",
- model=model,
- )
-
- monkeypatch.setattr(
- "solstone.think.providers.state.readiness_for_provider",
- fake_readiness,
- )
-
- response = client.get("/app/thinking/api/providers")
-
- assert response.status_code == 200
- context_routes = response.get_json()["ai_readiness"]["context_routes"]
- assert {route["provider"] for route in context_routes} == {"local"}
-
-
def test_get_providers_ai_readiness_surfaces_gpu_probe_failed_from_inspect(
settings_client, monkeypatch
):
diff --git a/solstone/apps/thinking/tests/test_workspace_html.py b/solstone/apps/thinking/tests/test_workspace_html.py
index 44a877870..d554fce3c 100644
--- a/solstone/apps/thinking/tests/test_workspace_html.py
+++ b/solstone/apps/thinking/tests/test_workspace_html.py
@@ -88,8 +88,6 @@ def test_workspace_renders_each_lane(settings_env):
for control_id in (
"field-generate-provider",
"field-cogitate-provider",
- "field-generate-tier",
- "field-cogitate-tier",
"field-google-backend",
"vertexSave",
"vertexClear",
diff --git a/solstone/apps/thinking/workspace.html b/solstone/apps/thinking/workspace.html
index 2303a2b07..fce0723dd 100644
--- a/solstone/apps/thinking/workspace.html
+++ b/solstone/apps/thinking/workspace.html
@@ -544,22 +544,6 @@
-
-
-
-
-
-
-
-
diff --git a/solstone/apps/timeline/maint/002_register_segment_summary_model.py b/solstone/apps/timeline/maint/002_register_segment_summary_model.py
index 386bf8a6f..b1dace2f5 100644
--- a/solstone/apps/timeline/maint/002_register_segment_summary_model.py
+++ b/solstone/apps/timeline/maint/002_register_segment_summary_model.py
@@ -1,25 +1,15 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright (c) 2026 sol pbc
-"""Register timeline segment summary provider context."""
+"""Retired provider-context registration for timeline segment summary."""
from __future__ import annotations
import argparse
-import json
-import logging
import sys
from dataclasses import dataclass
from pathlib import Path
-from solstone.think.journal_config import write_journal_config
-from solstone.think.utils import get_journal, setup_cli
-
-logger = logging.getLogger(__name__)
-
-CONTEXT_NAME = "talent.timeline.segment_summary"
-EXPECTED_CONTEXT = {"provider": "google", "model": "gemini-flash-lite-latest"}
-
@dataclass
class RegistrationSummary:
@@ -32,71 +22,8 @@ class RegistrationSummary:
def run_registration(
journal_path: Path, *, dry_run: bool = False
) -> RegistrationSummary:
- summary = RegistrationSummary()
- config_path = journal_path / "config" / "journal.json"
-
- if config_path.exists():
- try:
- raw_bytes = config_path.read_bytes()
- except OSError as exc:
- logger.warning("Failed to read %s: %s", config_path, exc)
- summary.errors += 1
- return summary
-
- if raw_bytes.strip():
- try:
- raw = json.loads(raw_bytes)
- except json.JSONDecodeError as exc:
- logger.warning("Malformed JSON in %s: %s", config_path, exc)
- summary.errors += 1
- return summary
- if not isinstance(raw, dict):
- logger.warning(
- "Malformed journal config in %s: expected object", config_path
- )
- summary.errors += 1
- return summary
- else:
- raw = {}
- else:
- raw = {}
-
- providers = raw.setdefault("providers", {})
- if not isinstance(providers, dict):
- logger.warning("Preserving divergent providers config in %s", config_path)
- summary.warnings += 1
- return summary
-
- contexts = providers.setdefault("contexts", {})
- if not isinstance(contexts, dict):
- logger.warning(
- "Preserving divergent providers.contexts config in %s", config_path
- )
- summary.warnings += 1
- return summary
-
- existing = contexts.get(CONTEXT_NAME)
- if existing == EXPECTED_CONTEXT:
- summary.preserved += 1
- return summary
- if existing is not None:
- logger.warning("Preserving divergent provider context %s", CONTEXT_NAME)
- summary.warnings += 1
- return summary
-
- contexts[CONTEXT_NAME] = dict(EXPECTED_CONTEXT)
- summary.added += 1
-
- if dry_run:
- return summary
-
- try:
- write_journal_config(raw)
- except OSError as exc:
- logger.warning("Failed to write %s: %s", config_path, exc)
- summary.errors += 1
-
- return summary
+ _ = (journal_path, dry_run)
+ return RegistrationSummary()
def _print_summary(summary: RegistrationSummary) -> None:
@@ -105,6 +32,7 @@ def _print_summary(summary: RegistrationSummary) -> None:
print(f" preserved: {summary.preserved}")
print(f" warnings: {summary.warnings}")
print(f" errors: {summary.errors}")
+ print(" retired: provider context registration is no longer needed")
def main() -> None:
@@ -114,9 +42,9 @@ def main() -> None:
action="store_true",
help="Preview provider context registration without writing files.",
)
- args = setup_cli(parser)
+ args = parser.parse_args()
- summary = run_registration(Path(get_journal()), dry_run=args.dry_run)
+ summary = run_registration(Path.cwd(), dry_run=args.dry_run)
_print_summary(summary)
if summary.errors:
sys.exit(1)
diff --git a/solstone/apps/timeline/maintenance.py b/solstone/apps/timeline/maintenance.py
index 00e3b70df..378cde0d4 100644
--- a/solstone/apps/timeline/maintenance.py
+++ b/solstone/apps/timeline/maintenance.py
@@ -15,12 +15,12 @@ from datetime import datetime, timedelta
from pathlib import Path
from solstone.apps.timeline.rollup import (
- MODEL,
pick_top_events_async,
pick_top_events_batch,
)
from solstone.think.journal_io import atomic_replace
from solstone.think.maintenance import MaintenanceRoutine
+from solstone.think.models import resolve_provider
from solstone.think.utils import (
EXIT_EMPTY,
get_journal,
@@ -34,6 +34,10 @@ SEGMENT_RE = re.compile(r"^(\d{2})(\d{2})(\d{2})_\d{1,6}$")
DAY_RE = re.compile(r"^\d{8}$")
+def _resolved_generate_model() -> str:
+ return resolve_provider("generate")[1]
+
+
def _default_day() -> str:
return (datetime.now(get_owner_timezone()) - timedelta(days=1)).strftime("%Y%m%d")
@@ -233,9 +237,10 @@ async def _rollup_day(
day_rationale = day_result["rationale"]
t_total = time.time() - t0
+ model = _resolved_generate_model()
payload = {
"day": day,
- "model": MODEL,
+ "model": model,
"generated_at": int(time.time()),
"segment_count": len(segments),
"hour_count": len(hours_out),
@@ -352,8 +357,9 @@ async def _rollup_master(
print(" [empty] no month candidates found")
return 0
+ model = _resolved_generate_model()
print(
- f"rolling up {len(jobs_in)} month(s) with model={MODEL} top={top} jobs={jobs}"
+ f"rolling up {len(jobs_in)} month(s) with model={model} top={top} jobs={jobs}"
)
t0 = time.time()
try:
@@ -408,7 +414,7 @@ async def _rollup_master(
payload = {
"generated_at": int(time.time()),
- "model": MODEL,
+ "model": model,
"top_n": top,
"year_top": year_top,
"months": months_out,
diff --git a/solstone/apps/timeline/rollup.py b/solstone/apps/timeline/rollup.py
index 793dc2ba1..c52e5d07f 100644
--- a/solstone/apps/timeline/rollup.py
+++ b/solstone/apps/timeline/rollup.py
@@ -7,11 +7,6 @@ from __future__ import annotations
import json
-from solstone.think.models import GEMINI_FLASH
-
-# Use the full flash, not lite — qualitative ranking benefits from headroom.
-MODEL = GEMINI_FLASH
-
# Schema: a list of indices into the candidate array. We don't ask the model
# to re-emit titles/descriptions because (a) we want lossless preservation of
@@ -104,7 +99,6 @@ async def pick_top_events_async(
req = batch.create(
contents=build_user_prompt(events),
context="timeline.scratch.rollup",
- model=MODEL,
system_instruction=build_system_instruction(scope_label, n),
json_output=True,
json_schema=build_rollup_schema(n),
@@ -177,7 +171,6 @@ async def pick_top_events_batch(
req = batch.create(
contents=build_user_prompt(events),
context="timeline.scratch.rollup",
- model=MODEL,
system_instruction=build_system_instruction(scope_label, n),
json_output=True,
json_schema=build_rollup_schema(n),
diff --git a/solstone/apps/timeline/talent/segment_summary.md b/solstone/apps/timeline/talent/segment_summary.md
index 5a54a2e9d..b455109b2 100644
--- a/solstone/apps/timeline/talent/segment_summary.md
+++ b/solstone/apps/timeline/talent/segment_summary.md
@@ -1,9 +1,7 @@
{
"type": "generate",
- "tier": 3,
"schedule": "segment",
"priority": 41,
- "provider": "google",
"output": "json",
"schema": "segment_summary.schema.json",
"hook": {"pre": "timeline:segment_summary", "post": "timeline:segment_summary"},
diff --git a/solstone/apps/timeline/talent/segment_summary.py b/solstone/apps/timeline/talent/segment_summary.py
index a11e2d616..be60ad6b4 100644
--- a/solstone/apps/timeline/talent/segment_summary.py
+++ b/solstone/apps/timeline/talent/segment_summary.py
@@ -11,13 +11,10 @@ import time
from pathlib import Path
from solstone.think.journal_io import atomic_replace
-from solstone.think.models import GEMINI_LITE
from solstone.think.utils import day_path, iter_segments, segment_path
logger = logging.getLogger(__name__)
-MODEL = GEMINI_LITE
-
SYSTEM_INSTRUCTION = (
"Pick the SINGLE MOST IMPORTANT EVENT from this ~5-minute slice of a "
"personal life-journal and name it. The output is one cell in a multi-scale "
@@ -201,7 +198,7 @@ def post_process(result: str, context: dict) -> str | None:
"title": parsed.get("title", ""),
"description": parsed.get("description", ""),
"origin": origin_for_segment(seg_dir),
- "model": context.get("model", MODEL),
+ "model": context["model"],
"generated_at": int(time.time()),
}
atomic_replace(
diff --git a/solstone/apps/timeline/tests/test_register_segment_summary_model.py b/solstone/apps/timeline/tests/test_register_segment_summary_model.py
index a01977626..a70bf2c46 100644
--- a/solstone/apps/timeline/tests/test_register_segment_summary_model.py
+++ b/solstone/apps/timeline/tests/test_register_segment_summary_model.py
@@ -1,19 +1,14 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright (c) 2026 sol pbc
-"""Tests for timeline segment-summary provider context registration."""
+"""Tests for retired timeline segment-summary provider context registration."""
from __future__ import annotations
import importlib
-import json
import sys
-import pytest
-
from solstone.apps.timeline.tests.conftest import write_json
-from solstone.think.models import resolve_provider
-from solstone.think.utils import CorruptConfigError
mod = importlib.import_module(
"solstone.apps.timeline.maint.002_register_segment_summary_model"
@@ -24,79 +19,40 @@ def _journal_config_path(journal):
return journal / "config" / "journal.json"
-def test_adds_providers_contexts_entry_when_missing(timeline_journal):
- write_json(_journal_config_path(timeline_journal), {"providers": {"contexts": {}}})
-
- summary = mod.run_registration(timeline_journal)
-
- data = json.loads(_journal_config_path(timeline_journal).read_text())
- assert summary.added == 1
- assert data["providers"]["contexts"][mod.CONTEXT_NAME] == mod.EXPECTED_CONTEXT
-
-
-def test_idempotent_when_present_and_matches(timeline_journal, monkeypatch):
+def test_registration_is_retired_noop(timeline_journal):
+ path = _journal_config_path(timeline_journal)
write_json(
- _journal_config_path(timeline_journal),
- {"providers": {"contexts": {mod.CONTEXT_NAME: mod.EXPECTED_CONTEXT}}},
- )
- monkeypatch.setattr(
- mod, "write_journal_config", lambda *args, **kwargs: pytest.fail("rewrite")
- )
-
- summary = mod.run_registration(timeline_journal)
-
- assert summary.preserved == 1
-
-
-def test_warns_and_preserves_divergent_model(timeline_journal, monkeypatch):
- data = {
- "providers": {
- "contexts": {
- mod.CONTEXT_NAME: {"provider": "google", "model": "different-model"}
+ path,
+ {
+ "providers": {
+ "contexts": {
+ "talent.timeline.segment_summary": {
+ "provider": "google",
+ "model": "legacy-model",
+ }
+ }
}
- }
- }
- write_json(_journal_config_path(timeline_journal), data)
- monkeypatch.setattr(
- mod, "write_journal_config", lambda *args, **kwargs: pytest.fail("rewrite")
- )
-
- summary = mod.run_registration(timeline_journal)
-
- assert summary.warnings == 1
- assert json.loads(_journal_config_path(timeline_journal).read_text()) == data
-
-
-def test_creates_providers_contexts_when_missing_section(timeline_journal):
- write_json(_journal_config_path(timeline_journal), {"identity": {"name": "Test"}})
-
- summary = mod.run_registration(timeline_journal)
-
- data = json.loads(_journal_config_path(timeline_journal).read_text())
- assert summary.added == 1
- assert data["providers"]["contexts"][mod.CONTEXT_NAME] == mod.EXPECTED_CONTEXT
-
-
-def test_local_generate_default_wins_over_registered_cloud_context(timeline_journal):
- write_json(
- _journal_config_path(timeline_journal),
- {"providers": {"generate": {"provider": "local"}}},
+ },
)
+ before = path.read_bytes()
summary = mod.run_registration(timeline_journal)
- provider, _ = resolve_provider(mod.CONTEXT_NAME, "generate")
- assert summary.added == 1
- assert provider == "local"
+ assert summary.added == 0
+ assert summary.preserved == 0
+ assert summary.warnings == 0
+ assert summary.errors == 0
+ assert path.read_bytes() == before
-def test_malformed_json_fails_loud(timeline_journal, monkeypatch):
- config_path = _journal_config_path(timeline_journal)
- config_path.write_text("{bad", encoding="utf-8")
- before = config_path.read_bytes()
+def test_registration_main_succeeds_without_reading_malformed_config(
+ timeline_journal, monkeypatch
+):
+ path = _journal_config_path(timeline_journal)
+ path.write_text("{bad", encoding="utf-8")
+ before = path.read_bytes()
monkeypatch.setattr(sys, "argv", ["register-segment-summary-model"])
- with pytest.raises(CorruptConfigError):
- mod.main()
+ mod.main()
- assert config_path.read_bytes() == before
+ assert path.read_bytes() == before
diff --git a/solstone/apps/timeline/tests/test_rollup_day.py b/solstone/apps/timeline/tests/test_rollup_day.py
index c62fc610a..d23d4b76c 100644
--- a/solstone/apps/timeline/tests/test_rollup_day.py
+++ b/solstone/apps/timeline/tests/test_rollup_day.py
@@ -10,9 +10,11 @@ import json
from solstone.apps.timeline.maintenance import _rollup_day, run_rollup_day
from solstone.apps.timeline.tests.conftest import write_json
-from solstone.think.models import GEMINI_FLASH, GEMINI_LITE, SchemaValidationError
+from solstone.think.models import SchemaValidationError
DAY = "20260512"
+SEGMENT_MODEL = "segment-test-model"
+ROLLUP_MODEL = "resolved-rollup-model"
def _write_segment(journal, day, segment, title, hour_stream="archon"):
@@ -22,7 +24,7 @@ def _write_segment(journal, day, segment, title, hour_stream="archon"):
"title": title,
"description": f"{title} description.",
"origin": f"{day}/{hour_stream}/{segment}",
- "model": GEMINI_LITE,
+ "model": SEGMENT_MODEL,
"generated_at": 1770000000,
},
)
@@ -85,8 +87,12 @@ def test_rollup_day_dry_run_no_llm_calls(timeline_journal, mock_agenerate):
assert mock.call_count == 0
-def test_rollup_day_writes_seed_shape(timeline_journal, mock_agenerate):
+def test_rollup_day_writes_seed_shape(timeline_journal, mock_agenerate, monkeypatch):
"""AC#6."""
+ monkeypatch.setattr(
+ "solstone.apps.timeline.maintenance.resolve_provider",
+ lambda _interface: ("google", ROLLUP_MODEL),
+ )
for i in range(5):
title = "Café Event" if i == 0 else f"Event {i}"
_write_segment(timeline_journal, DAY, f"12000{i}_60", title)
@@ -99,12 +105,12 @@ def test_rollup_day_writes_seed_shape(timeline_journal, mock_agenerate):
timeline_path = timeline_journal / "chronicle" / DAY / "timeline.json"
payload = json.loads(timeline_path.read_text())
assert payload["day"] == DAY
- assert payload["model"] == GEMINI_FLASH
+ assert payload["model"] == ROLLUP_MODEL
assert payload["segment_count"] == 5
assert payload["hour_count"] == 1
assert len(payload["day_top"]) == 4
assert payload["hours"]["12"]["rationale"] == "highest consequence"
- assert mock.call_args.kwargs["model"] == GEMINI_FLASH
+ assert "model" not in mock.call_args.kwargs
raw = timeline_path.read_bytes()
assert b"Caf\xc3\xa9 Event" in raw
assert b"\\u00e9" not in raw
diff --git a/solstone/apps/timeline/tests/test_rollup_master.py b/solstone/apps/timeline/tests/test_rollup_master.py
index 1eaf30852..0db974e5c 100644
--- a/solstone/apps/timeline/tests/test_rollup_master.py
+++ b/solstone/apps/timeline/tests/test_rollup_master.py
@@ -12,7 +12,9 @@ import pytest
from solstone.apps.timeline.maintenance import _rollup_master, run_rollup_master
from solstone.apps.timeline.tests.conftest import write_json
-from solstone.think.models import GEMINI_FLASH, SchemaValidationError
+from solstone.think.models import SchemaValidationError
+
+ROLLUP_MODEL = "resolved-rollup-model"
def _write_day(journal, day, titles):
@@ -20,7 +22,7 @@ def _write_day(journal, day, titles):
journal / "chronicle" / day / "timeline.json",
{
"day": day,
- "model": GEMINI_FLASH,
+ "model": "day-rollup-model",
"generated_at": 1770000000,
"segment_count": len(titles),
"hour_count": 1,
@@ -104,8 +106,12 @@ def test_rollup_master_empty_input_exits_empty_sentinel(timeline_journal):
assert not (timeline_journal / "timeline.json").exists()
-def test_rollup_master_writes_seed_shape(timeline_journal, mock_agenerate):
+def test_rollup_master_writes_seed_shape(timeline_journal, mock_agenerate, monkeypatch):
"""AC#9, AC#10."""
+ monkeypatch.setattr(
+ "solstone.apps.timeline.maintenance.resolve_provider",
+ lambda _interface: ("google", ROLLUP_MODEL),
+ )
_write_day(timeline_journal, "20260510", ["A", "B", "C"])
_write_day(timeline_journal, "20260511", ["D", "Café E"])
mock = mock_agenerate({"picks": [4, 0, 1, 2], "rationale": "monthly consequence"})
@@ -123,13 +129,13 @@ def test_rollup_master_writes_seed_shape(timeline_journal, mock_agenerate):
timeline_path = timeline_journal / "timeline.json"
payload = json.loads(timeline_path.read_text())
- assert payload["model"] == GEMINI_FLASH
+ assert payload["model"] == ROLLUP_MODEL
assert payload["top_n"] == 4
assert list(payload["months"]) == ["202605"]
assert payload["months"]["202605"]["day_count"] == 2
assert payload["months"]["202605"]["month_top"][0]["title"] == "Café E"
assert payload["year_top"][0]["month"] == "202605"
- assert mock.call_args.kwargs["model"] == GEMINI_FLASH
+ assert "model" not in mock.call_args.kwargs
raw = timeline_path.read_bytes()
assert b"Caf\xc3\xa9 E" in raw
assert b"\\u00e9" not in raw
diff --git a/solstone/apps/timeline/tests/test_segment_summary_hooks.py b/solstone/apps/timeline/tests/test_segment_summary_hooks.py
index 1e00fbd00..daa0205ac 100644
--- a/solstone/apps/timeline/tests/test_segment_summary_hooks.py
+++ b/solstone/apps/timeline/tests/test_segment_summary_hooks.py
@@ -11,10 +11,10 @@ from pathlib import Path
import pytest
from solstone.apps.timeline.talent import segment_summary
-from solstone.think.models import GEMINI_LITE
DAY = "20260512"
SEGMENT = "120000_60"
+TEST_MODEL = "test-dispatched-model"
def _make_activity(
@@ -111,7 +111,7 @@ def test_post_process_writes_augmented_timeline_atomically(
returned = segment_summary.post_process(
json.dumps({"title": "Café Reset", "description": "Restarts café display."}),
- {"day": DAY, "segment": SEGMENT, "stream": "archon"},
+ {"day": DAY, "segment": SEGMENT, "stream": "archon", "model": TEST_MODEL},
)
assert returned is None
@@ -123,7 +123,7 @@ def test_post_process_writes_augmented_timeline_atomically(
"title": "Café Reset",
"description": "Restarts café display.",
"origin": f"{DAY}/archon/{SEGMENT}",
- "model": GEMINI_LITE,
+ "model": TEST_MODEL,
"generated_at": 1770000000,
}
raw = timeline.read_bytes()
@@ -139,11 +139,11 @@ def test_post_process_records_literal_model(timeline_journal):
segment_summary.post_process(
json.dumps({"title": "Short Title", "description": "Brief description here"}),
- {"day": DAY, "segment": SEGMENT, "stream": ""},
+ {"day": DAY, "segment": SEGMENT, "stream": "", "model": TEST_MODEL},
)
timeline = timeline_journal / "chronicle" / DAY / SEGMENT / "timeline.json"
- assert json.loads(timeline.read_text(encoding="utf-8"))["model"] == GEMINI_LITE
+ assert json.loads(timeline.read_text(encoding="utf-8"))["model"] == TEST_MODEL
@pytest.mark.parametrize(
@@ -170,7 +170,7 @@ def test_post_process_origin_matches_seed_for_all_4_layouts(
segment_summary.post_process(
json.dumps({"title": "Short Title", "description": "Brief description here"}),
- {"day": DAY, "segment": SEGMENT, "stream": stream},
+ {"day": DAY, "segment": SEGMENT, "stream": stream, "model": TEST_MODEL},
)
timeline = timeline_journal / "chronicle" / expected_origin / "timeline.json"
diff --git a/solstone/convey/readiness_snapshot.py b/solstone/convey/readiness_snapshot.py
index b12869257..c298f1ae8 100644
--- a/solstone/convey/readiness_snapshot.py
+++ b/solstone/convey/readiness_snapshot.py
@@ -64,15 +64,12 @@ def _build_interface_snapshot() -> dict[str, Any]:
interface_views = {}
views = []
for interface in ("generate", "cogitate"):
- provider, model = models.resolve_provider("", interface)
+ provider, model = models.resolve_provider(interface)
readiness = provider_state.readiness_for_provider(provider, interface, model)
view = present_readiness(readiness)
interface_views[interface] = view_to_dict(view)
views.append(view)
- context_route_views = _build_context_route_views()
- views.extend(context_route_views)
-
groups_by_key = {}
for view in views:
if view.severity not in {"blocker", "attention"}:
@@ -95,27 +92,10 @@ def _build_interface_snapshot() -> dict[str, Any]:
),
},
"interfaces": interface_views,
- "context_routes": [view_to_dict(view) for view in context_route_views],
"groups": groups,
}
-def _build_context_route_views() -> list:
- config = models.get_config()
- providers = config.get("providers", {})
- contexts = providers.get("contexts", {}) if isinstance(providers, dict) else {}
- if not isinstance(contexts, dict):
- return []
- views_by_route: dict[tuple[str, str, str], Any] = {}
- for context in sorted(contexts):
- interface, provider, model = models.resolve_effective_route(context)
- key = (provider, model, interface)
- if key not in views_by_route:
- state = provider_state.readiness_for_provider(provider, interface, model)
- views_by_route[key] = present_readiness(state)
- return list(views_by_route.values())
-
-
def highest_severity_group(snapshot: dict[str, Any]) -> dict[str, Any] | None:
"""Return the snapshot group with the highest presenter severity, or None.
diff --git a/solstone/observe/categories/__init__.py b/solstone/observe/categories/__init__.py
index 4abf8f814..7e826b19d 100644
--- a/solstone/observe/categories/__init__.py
+++ b/solstone/observe/categories/__init__.py
@@ -4,7 +4,7 @@
"""Category prompts and formatters for screen description.
This package contains:
-- .md: Prompt template with JSON frontmatter (metadata: description, output format, tier)
+- .md: Prompt template with JSON frontmatter (metadata: description, output format)
- .py: Optional formatter for rich markdown output
Categories with extraction prompts in their .md files are extractable.
diff --git a/solstone/observe/describe.md b/solstone/observe/describe.md
index 933f69ddc..56a737e53 100644
--- a/solstone/observe/describe.md
+++ b/solstone/observe/describe.md
@@ -1,6 +1,5 @@
---
context: observe.describe.frame
-tier: 3
label: Screen Categorization
group: Observe
---
diff --git a/solstone/observe/describe.py b/solstone/observe/describe.py
index 9a98dc67d..7bb01cf1f 100644
--- a/solstone/observe/describe.py
+++ b/solstone/observe/describe.py
@@ -144,7 +144,6 @@ def _discover_categories() -> dict[str, dict]:
Each category is a .md file with JSON frontmatter containing:
- description (required): Single-line description for categorization prompt
- output (optional, default: "markdown"): Response format for extraction
- - tier (optional, default: 2): Model tier for this category (1=pro, 2=flash, 3=lite)
- label (optional): Human-readable name for settings UI
- group (optional, default: "Screen Analysis"): Category for grouping in settings UI
@@ -177,9 +176,6 @@ def _discover_categories() -> dict[str, dict]:
# Apply defaults for observation settings
metadata.setdefault("output", "markdown")
- # Apply defaults for tier routing
- # tier: 1=pro, 2=flash, 3=lite (default: flash)
- metadata.setdefault("tier", 2)
metadata.setdefault("max_output_tokens", 4096)
# label: Human-readable name (default: title-cased category name)
metadata.setdefault("label", category.replace("_", " ").title())
@@ -287,7 +283,7 @@ def _dedup_readiness_contexts(contexts: list[str]) -> list[str]:
selected: list[str] = []
seen: set[tuple[str, str]] = set()
for context in contexts:
- key = resolve_provider(context, "generate")
+ key = resolve_provider("generate")
if key in seen:
continue
seen.add(key)
@@ -764,8 +760,7 @@ class VideoProcessor:
promoted = True
try:
- # Resolve model for frame description (tier from describe.md frontmatter)
- frame_provider, frame_model = resolve_provider(FRAME_CONTEXT, "generate")
+ frame_provider, frame_model = resolve_provider("generate")
if frame_provider == NO_BRAIN_PROVIDER:
logger.info("No thinking engine selected; deferring frame description")
return
@@ -1092,10 +1087,7 @@ class VideoProcessor:
# Determine output format from metadata
is_json = cat_meta.get("output") == "json"
- # Resolve model for this category context
- cat_provider, cat_model = resolve_provider(
- cat_meta["context"], "generate"
- )
+ cat_provider, cat_model = resolve_provider("generate")
if cat_provider == NO_BRAIN_PROVIDER:
logger.info(
"No thinking engine selected; deferring %s extraction",
diff --git a/solstone/observe/enrich.md b/solstone/observe/enrich.md
index 76d53a2c7..af812e6a9 100644
--- a/solstone/observe/enrich.md
+++ b/solstone/observe/enrich.md
@@ -1,6 +1,5 @@
---
context: observe.enrich
-tier: 2
label: Audio Enrichment
group: Observe
---
diff --git a/solstone/observe/extract.md b/solstone/observe/extract.md
index c0bd0071e..bc6120012 100644
--- a/solstone/observe/extract.md
+++ b/solstone/observe/extract.md
@@ -1,6 +1,5 @@
---
context: observe.extract.selection
-tier: 2
label: Frame Selection
group: Observe
---
diff --git a/solstone/observe/transcribe/gemini.md b/solstone/observe/transcribe/gemini.md
index 42f3d091e..f49887abd 100644
--- a/solstone/observe/transcribe/gemini.md
+++ b/solstone/observe/transcribe/gemini.md
@@ -1,6 +1,5 @@
---
context: observe.transcribe.gemini
-tier: 2
label: Audio Transcription (Gemini)
group: Observe
---
diff --git a/solstone/talent/chat.md b/solstone/talent/chat.md
index 76768bf19..cbca0c7b9 100644
--- a/solstone/talent/chat.md
+++ b/solstone/talent/chat.md
@@ -2,7 +2,6 @@
"type": "generate",
"title": "Chat",
"description": "Structured conversational reply planner for the chat backend rewrite",
- "tier": 2,
"thinking_budget": 4096,
"max_output_tokens": 2048,
"output": "json",
diff --git a/solstone/talent/conversation.md b/solstone/talent/conversation.md
index 84a851cdd..e4bc679aa 100644
--- a/solstone/talent/conversation.md
+++ b/solstone/talent/conversation.md
@@ -6,7 +6,6 @@
"schedule": "activity",
"activities": ["meeting", "call", "messaging", "email"],
"priority": 20,
- "tier": 3,
"output": "json",
"max_output_tokens": 12288,
"schema": "story.schema.json",
diff --git a/solstone/talent/participation.md b/solstone/talent/participation.md
index aa440b5d2..f5d91ec0a 100644
--- a/solstone/talent/participation.md
+++ b/solstone/talent/participation.md
@@ -6,7 +6,6 @@
"schedule": "activity",
"activities": ["*"],
"priority": 10,
- "tier": 3,
"output": "json",
"schema": "participation.schema.json",
"max_output_tokens": 12288,
diff --git a/solstone/talent/pulse.md b/solstone/talent/pulse.md
index fda68607c..e764724f6 100644
--- a/solstone/talent/pulse.md
+++ b/solstone/talent/pulse.md
@@ -5,7 +5,6 @@
"schedule": "cadence",
"cadence_minutes": 5,
"priority": 50,
- "tier": 3,
"hook": {"pre": "pulse", "post": "pulse"},
"output": "json",
"schema": "pulse.schema.json",
diff --git a/solstone/talent/screen.md b/solstone/talent/screen.md
index d783e4f00..9b1ba82fe 100644
--- a/solstone/talent/screen.md
+++ b/solstone/talent/screen.md
@@ -6,7 +6,6 @@
"color": "#9c27b0",
"schedule": "segment",
"priority": 10,
- "tier": 3,
"output": "json",
"schema": "screen.schema.json",
"max_output_tokens": 12288,
diff --git a/solstone/talent/sense.md b/solstone/talent/sense.md
index f4f2bae56..21a1b0710 100644
--- a/solstone/talent/sense.md
+++ b/solstone/talent/sense.md
@@ -6,7 +6,6 @@
"color": "#ff6f00",
"schedule": "segment",
"priority": 5,
- "tier": 3,
"output": "json",
"schema": "sense.schema.json",
"max_output_tokens": 6144,
diff --git a/solstone/talent/steward.md b/solstone/talent/steward.md
index 39ac41cda..1e6dc2a8f 100644
--- a/solstone/talent/steward.md
+++ b/solstone/talent/steward.md
@@ -6,7 +6,6 @@
"schedule": "cadence",
"cadence_minutes": 30,
"priority": 45,
- "tier": 3,
"hook": {"pre": "steward", "post": "steward"},
"output": "json",
"schema": "steward.schema.json",
diff --git a/solstone/think/batch.py b/solstone/think/batch.py
index a63240e3a..db931f412 100644
--- a/solstone/think/batch.py
+++ b/solstone/think/batch.py
@@ -316,7 +316,7 @@ class Batch:
request.provider = getattr(e, "provider", None)
if request.provider is None:
try:
- request.provider = resolve_provider(request.context, "generate")[0]
+ request.provider = resolve_provider("generate")[0]
except (KeyError, TypeError, ValueError):
request.provider = None
diff --git a/solstone/think/detect_created.md b/solstone/think/detect_created.md
index a4a8cb6fe..7ae13df2a 100644
--- a/solstone/think/detect_created.md
+++ b/solstone/think/detect_created.md
@@ -1,6 +1,5 @@
---
context: detect.created
-tier: 3
label: Date Detection
group: Import
---
diff --git a/solstone/think/detect_transcript_json.md b/solstone/think/detect_transcript_json.md
index d2ec9197a..e41d608a4 100644
--- a/solstone/think/detect_transcript_json.md
+++ b/solstone/think/detect_transcript_json.md
@@ -1,6 +1,5 @@
---
context: observe.detect.json
-tier: 2
label: Normalization
group: Import
---
diff --git a/solstone/think/detect_transcript_segment.md b/solstone/think/detect_transcript_segment.md
index a1a22d208..db6fe21c9 100644
--- a/solstone/think/detect_transcript_segment.md
+++ b/solstone/think/detect_transcript_segment.md
@@ -1,6 +1,5 @@
---
context: observe.detect.segment
-tier: 2
label: Segmentation
group: Import
---
diff --git a/solstone/think/models.py b/solstone/think/models.py
index 6a66b9579..2395c3dfe 100644
--- a/solstone/think/models.py
+++ b/solstone/think/models.py
@@ -1,7 +1,6 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright (c) 2026 sol pbc
-import fnmatch
import functools
import inspect
import json
@@ -20,14 +19,6 @@ from solstone.think.utils import get_config, get_journal
logger = logging.getLogger(__name__)
-# ---------------------------------------------------------------------------
-# Tier constants
-# ---------------------------------------------------------------------------
-
-TIER_PRO = 1
-TIER_FLASH = 2
-TIER_LITE = 3
-
# ---------------------------------------------------------------------------
# Model constants
#
@@ -151,9 +142,6 @@ LOCAL_MODEL = "local/qwen3.5-4b"
QWEN_35_9B = "qwen3.5:9b"
GEMMA4_26B_A4B_4BIT = "gemma-4-26b-a4b-it-mlx-4bit"
-MLX_PRO = QWEN_35_9B
-MLX_FLASH = QWEN_35_9B
-MLX_LITE = QWEN_35_9B
# Per-model request parameter capability overrides.
@@ -169,40 +157,14 @@ def model_supports(model: str, param: str) -> bool:
return MODEL_CAPABILITIES.get(model, {}).get(param) is not False
-# ---------------------------------------------------------------------------
-# System defaults: provider -> tier -> model
-# ---------------------------------------------------------------------------
-
-PROVIDER_DEFAULTS: Dict[str, Dict[int, str]] = {
- "google": {
- TIER_PRO: GEMINI_PRO,
- TIER_FLASH: GEMINI_FLASH,
- TIER_LITE: GEMINI_LITE,
- },
- "openai": {
- TIER_PRO: GPT_5,
- TIER_FLASH: GPT_5_MINI,
- TIER_LITE: GPT_5_NANO,
- },
- "anthropic": {
- TIER_PRO: CLAUDE_OPUS_4,
- TIER_FLASH: CLAUDE_SONNET_4,
- TIER_LITE: CLAUDE_HAIKU_4,
- },
- "local": {
- TIER_PRO: LOCAL_MODEL,
- TIER_FLASH: LOCAL_MODEL,
- TIER_LITE: LOCAL_MODEL,
- },
-}
-
-TYPE_DEFAULTS: Dict[str, Dict[str, Any]] = {
- "generate": {"tier": TIER_FLASH, "backup": "anthropic"},
- "cogitate": {"tier": TIER_FLASH, "backup": "anthropic"},
-}
-
NO_BRAIN_PROVIDER = "none"
IMPLICIT_CLOUD_PROVIDER_ORDER = ("google", "anthropic", "openai")
+DEFAULT_MODEL_BY_PROVIDER: dict[str, str] = {
+ "google": GEMINI_FLASH,
+ "openai": GPT_5_MINI,
+ "anthropic": CLAUDE_SONNET_4,
+ "local": LOCAL_MODEL,
+}
# ---------------------------------------------------------------------------
@@ -333,7 +295,7 @@ def _confidential_attestation_verifier() -> Callable[[dict[str, Any]], None]:
# ---------------------------------------------------------------------------
# Prompt context discovery
#
-# Context metadata (tier, label, group) is defined in prompt .md files via
+# Context metadata (label, group, type) is defined in prompt .md files via
# YAML frontmatter. This eliminates duplication between code and config.
#
# NAMING CONVENTION:
@@ -348,18 +310,17 @@ def _confidential_attestation_verifier() -> Callable[[dict[str, Any]], None]:
#
# DISCOVERY SOURCES:
# 1. Prompt files listed in PROMPT_PATHS (with context in frontmatter)
-# 2. Categories from observe/categories/*.md (tier/label/group in frontmatter)
+# 2. Categories from observe/categories/*.md (label/group in frontmatter)
# 3. Talent configs from talent/*.md and apps/*/talent/*.md
#
# When adding new contexts:
# 1. Create a .md prompt file with YAML frontmatter containing:
-# context, tier, label, group
+# context, label, group
# 2. Add the path to PROMPT_PATHS
-# 3. If not listed, context falls back to the type's default tier
# ---------------------------------------------------------------------------
# Flat list of prompt files that define context metadata in frontmatter.
-# Each must have: context, tier, label, group in YAML frontmatter.
+# Each must have: context, label, group in YAML frontmatter.
PROMPT_PATHS: List[str] = [
"observe/describe.md",
"observe/enrich.md",
@@ -387,14 +348,13 @@ def _discover_prompt_contexts() -> Dict[str, Dict[str, Any]]:
Each file must have YAML frontmatter with:
- context: The context string (e.g., "observe.enrich")
- - tier: Tier number (1=pro, 2=flash, 3=lite)
- label: Human-readable name
- group: Settings UI category
Returns
-------
Dict[str, Dict[str, Any]]
- Mapping of context patterns to {tier, label, group} dicts.
+ Mapping of context patterns to {label, group} dicts.
"""
contexts = {}
base_dir = Path(__file__).parent.parent # Package root
@@ -415,7 +375,6 @@ def _discover_prompt_contexts() -> Dict[str, Dict[str, Any]]:
continue
contexts[context] = {
- "tier": meta.get("tier", TIER_FLASH),
"label": meta.get("label", context),
"group": meta.get("group", "Other"),
}
@@ -428,13 +387,14 @@ def _discover_prompt_contexts() -> Dict[str, Dict[str, Any]]:
def _discover_talent_contexts() -> Dict[str, Dict[str, Any]]:
"""Discover talent context defaults from talent/*.md config files.
- Uses get_talent_configs() from solstone.think.talent to load all talent configurations
- and converts them to context patterns with tier/label/group metadata.
+ Uses get_talent_configs() from solstone.think.talent to load all talent
+ configurations and converts them to context patterns with label/group/type
+ metadata.
Returns
-------
Dict[str, Dict[str, Any]]
- Mapping of context patterns to {tier, label, group, type} dicts.
+ Mapping of context patterns to {label, group, type} dicts.
Context patterns are: talent.system.{name} or talent.{app}.{name}
"""
from solstone.think.talent import get_talent_configs, key_to_context
@@ -447,7 +407,6 @@ def _discover_talent_contexts() -> Dict[str, Dict[str, Any]]:
for key, config in all_configs.items():
context = key_to_context(key)
contexts[context] = {
- "tier": config.get("tier", TIER_FLASH),
"label": config.get("label", config.get("title", key)),
"group": config.get("group", "Think"),
"type": config.get("type"),
@@ -467,7 +426,7 @@ def _build_context_registry() -> Dict[str, Dict[str, Any]]:
Returns
-------
Dict[str, Dict[str, Any]]
- Complete context registry mapping patterns to {tier, label, group}.
+ Complete context registry mapping patterns to {label, group, type?}.
"""
# Start with prompt contexts (from PROMPT_PATHS)
registry = _discover_prompt_contexts()
@@ -479,7 +438,6 @@ def _build_context_registry() -> Dict[str, Dict[str, Any]]:
for category, metadata in CATEGORIES.items():
context = metadata.get("context", f"observe.describe.{category}")
registry[context] = {
- "tier": metadata.get("tier", TIER_FLASH),
"label": metadata.get("label", category.replace("_", " ").title()),
"group": metadata.get("group", "Screen Analysis"),
}
@@ -499,7 +457,7 @@ def get_context_registry() -> Dict[str, Dict[str, Any]]:
Returns
-------
Dict[str, Dict[str, Any]]
- Complete context registry mapping patterns to {tier, label, group}.
+ Complete context registry mapping patterns to {label, group, type?}.
"""
global _context_registry
if _context_registry is None:
@@ -507,54 +465,6 @@ def get_context_registry() -> Dict[str, Dict[str, Any]]:
return _context_registry
-def _resolve_tier(context: str, agent_type: str) -> int:
- """Resolve context to tier number.
-
- Checks journal config contexts first, then dynamic context registry with glob matching.
-
- Parameters
- ----------
- context
- Context string (e.g., "talent.system.default", "observe.describe.frame").
- agent_type
- Agent type ("generate" or "cogitate").
-
- Returns
- -------
- int
- Tier number (1=pro, 2=flash, 3=lite).
- """
- from solstone.think.utils import get_config
-
- default_tier = TYPE_DEFAULTS[agent_type]["tier"]
-
- journal_config = get_config()
- providers_config = journal_config.get("providers", {})
- contexts = providers_config.get("contexts", {})
-
- # Get dynamic context registry (discovered prompts, categories, talent configs)
- registry = get_context_registry()
-
- # Check journal config contexts first (exact match)
- if context in contexts:
- return contexts[context].get("tier", default_tier)
-
- # Check context registry (exact match)
- if context in registry:
- return registry[context]["tier"]
-
- # Check glob patterns in both
- for pattern, ctx_config in contexts.items():
- if fnmatch.fnmatch(context, pattern):
- return ctx_config.get("tier", default_tier)
-
- for pattern, ctx_default in registry.items():
- if fnmatch.fnmatch(context, pattern):
- return ctx_default["tier"]
-
- return default_tier
-
-
def _resolve_default_provider(
providers: dict[str, Any],
agent_type: str,
@@ -587,253 +497,41 @@ def _resolve_default_provider(
return NO_BRAIN_PROVIDER
-def _resolve_model(provider: str, tier: int, config_models: Dict[str, Any]) -> str:
- """Resolve tier to model string for a given provider.
-
- Checks config overrides first, then falls back to system defaults.
- If requested tier is unavailable, falls back to more capable tiers
- (3→2→1, i.e., lite→flash→pro).
-
- Parameters
- ----------
- provider
- Provider name ("google", "openai", "anthropic").
- tier
- Tier number (1=pro, 2=flash, 3=lite).
- config_models
- The "models" section from providers config, mapping provider to tier overrides.
-
- Returns
- -------
- str
- Model identifier string.
- """
- # Check config overrides first
- provider_overrides = config_models.get(provider, {})
-
- # Try requested tier, then fall back to more capable tiers (lower numbers)
- for t in [tier, tier - 1, tier - 2] if tier > 1 else [tier]:
- if t < 1:
- continue
-
- # Check config override (tier as string key in JSON)
- tier_key = str(t)
- if tier_key in provider_overrides:
- return provider_overrides[tier_key]
-
- # Check system defaults
- provider_defaults = PROVIDER_DEFAULTS.get(provider, {})
- if t in provider_defaults:
- return provider_defaults[t]
-
+def default_model_for_provider(provider: str) -> str:
+ """Return the single default model for a provider."""
if provider == NO_BRAIN_PROVIDER:
- raise NoBrainConfiguredError()
- provider_defaults = PROVIDER_DEFAULTS.get(provider)
- if provider_defaults is None:
- raise ValueError(f"Unknown provider: {provider!r}")
- if TIER_FLASH in provider_defaults:
- return provider_defaults[TIER_FLASH]
- raise ValueError(f"Unknown provider: {provider!r}")
-
-
-def resolve_model_for_provider(
- context: str, provider: str, agent_type: str = "generate"
-) -> str:
- """Resolve model for a specific provider based on context tier.
-
- Use this when provider is overridden from the default - resolves the
- appropriate model for the given provider at the context's tier.
-
- Parameters
- ----------
- context
- Context string (e.g., "talent.system.default").
- provider
- Provider name ("google", "openai", "anthropic").
- agent_type
- Agent type ("generate" or "cogitate").
-
- Returns
- -------
- str
- Model identifier string for the provider at the context's tier.
- """
- from solstone.think.utils import get_config
-
- tier = _resolve_tier(context, agent_type)
- journal_config = get_config()
- providers_config = journal_config.get("providers", {})
- config_models = providers_config.get("models", {})
-
- return _resolve_model(provider, tier, config_models)
-
-
-def resolve_provider(context: str, agent_type: str) -> tuple[str, str]:
- """Resolve context to provider and model based on configuration.
-
- Matches context against configured contexts using exact match first,
- then glob patterns (via fnmatch), falling back to type-specific defaults.
-
- Provider precedence:
-
- 0. If ``type_default_is_local(agent_type)``: resolve to local. A cloud
- context / frontmatter / request pin may NOT override an explicit local
- type default — its cloud model string is neutralized. An explicit local
- context pin (provider: local + model) is honored verbatim. (D8 hard
- promise; predates this lode and is load-bearing for privacy.)
- 1. Explicit ``providers.contexts..provider``.
- 2. Explicit ``providers..provider``.
- 3. Key-presence fallback: first of google -> anthropic -> openai with
- cloud_key_configured(...). This order equals today's TYPE_DEFAULTS
- provider -> backup -> remainder, so every existing keyed install resolves
- to exactly the provider it does today. That IS the grandfather guarantee.
- 4. ``local``, if bundled artifacts are present.
- 5. NO_BRAIN_PROVIDER.
-
- The key-presence order is ``google -> anthropic -> openai`` because that is
- exactly today's provider -> backup -> remainder order. This is the
- grandfather guarantee: every existing keyed install with no explicit
- provider resolves to the same provider it does today.
-
- Supports both explicit model strings and tier-based routing:
- - {"provider": "google", "model": "gemini-flash-latest"} - explicit model
- - {"provider": "google", "tier": 2} - tier-based (2=flash)
- - {"tier": 1} - tier only, inherits the resolved default provider
+ return ""
+ try:
+ return DEFAULT_MODEL_BY_PROVIDER[provider]
+ except KeyError as exc:
+ raise ValueError(f"Unknown provider: {provider!r}") from exc
- The "models" section in providers config allows overriding which model
- is used for each tier per provider.
- Parameters
- ----------
- context
- Context string (e.g., "observe.describe.frame", "talent.system.meetings").
- agent_type
- Agent type ("generate" or "cogitate").
+def resolve_provider(agent_type: str) -> tuple[str, str]:
+ """Resolve the active provider and model for an interface.
- Returns
- -------
- tuple[str, str]
- (provider_name, model) tuple. Provider is one of "google", "openai",
- "anthropic", "local", or NO_BRAIN_PROVIDER. Model is the full model
- identifier string, or "" for NO_BRAIN_PROVIDER.
+ Precedence is explicit per-interface provider, configured cloud-key
+ presence in the grandfathered google -> anthropic -> openai order, local
+ runtime readiness, then NO_BRAIN_PROVIDER. Contexts, tiers, backups, and
+ providers.models overrides are intentionally ignored.
"""
config = get_config()
providers = config.get("providers", {})
if not isinstance(providers, dict):
providers = {}
- config_models = providers.get("models", {})
- if not isinstance(config_models, dict):
- config_models = {}
- # Get type-specific defaults from config, falling back to system constants
- type_defaults = TYPE_DEFAULTS[agent_type]
type_config = providers.get(agent_type, {})
if not isinstance(type_config, dict):
type_config = {}
- default_provider = _resolve_default_provider(providers, agent_type)
- default_tier = type_config.get("tier", type_defaults["tier"])
-
- # Handle explicit "model" key in type config (overrides tier-based resolution)
- if default_provider == NO_BRAIN_PROVIDER:
- default_model = ""
- elif "model" in type_config and "tier" not in type_config:
- default_model = type_config["model"]
- else:
- default_model = _resolve_model(default_provider, default_tier, config_models)
-
- contexts = providers.get("contexts", {})
- if not isinstance(contexts, dict):
- contexts = {}
-
- # Find matching context config
- match_config: Optional[Dict[str, Any]] = None
-
- if context and contexts:
- # Check for exact match first
- if context in contexts:
- match_config = contexts[context]
- else:
- # Check glob patterns - most specific (longest non-wildcard prefix) wins
- matches = []
- for pattern, ctx_config in contexts.items():
- if fnmatch.fnmatch(context, pattern):
- specificity = len(pattern.split("*")[0])
- matches.append((specificity, pattern, ctx_config))
-
- if matches:
- matches.sort(key=lambda x: x[0], reverse=True)
- _, _, match_config = matches[0]
-
- # No context match - check dynamic context registry for this context
- if match_config is None:
- # Get dynamic context registry (discovered prompts, categories, talent configs)
- registry = get_context_registry()
-
- # Check for matching context default (exact match first, then glob)
- context_tier = None
- if context:
- if context in registry:
- context_tier = registry[context]["tier"]
- else:
- # Check glob patterns
- matches = []
- for pattern, ctx_default in registry.items():
- if fnmatch.fnmatch(context, pattern):
- specificity = len(pattern.split("*")[0])
- matches.append((specificity, ctx_default["tier"]))
- if matches:
- matches.sort(key=lambda x: x[0], reverse=True)
- context_tier = matches[0][1]
-
- if context_tier is not None:
- if default_provider == NO_BRAIN_PROVIDER:
- return (NO_BRAIN_PROVIDER, "")
- model = _resolve_model(default_provider, context_tier, config_models)
- return (default_provider, model)
-
- return (default_provider, default_model)
-
- # Resolve provider (from match or default)
- provider = match_config.get("provider", default_provider)
-
- # Local type-default is a hard promise: a cloud context provider pin cannot
- # override it -- its cloud model string is neutralized. An explicit local
- # context pin (provider: local + model) is honored verbatim; otherwise only
- # the context's tier feeds local model selection.
- if type_default_is_local(agent_type, config):
- pinned = match_config.get("model")
- if (
- match_config.get("provider") == "local"
- and isinstance(pinned, str)
- and pinned.strip()
- ):
- return ("local", pinned)
- tier = match_config.get("tier", default_tier)
- if not isinstance(tier, int) or tier < 1 or tier > 3:
- tier = default_tier
- return ("local", _resolve_model("local", tier, config_models))
+ provider = _resolve_default_provider(providers, agent_type)
if provider == NO_BRAIN_PROVIDER:
return (NO_BRAIN_PROVIDER, "")
- # Resolve model: explicit non-empty model takes precedence over tier
- explicit_model = match_config.get("model")
+ explicit_model = type_config.get("model")
if isinstance(explicit_model, str) and explicit_model.strip():
- model = explicit_model
- elif "tier" in match_config:
- tier = match_config["tier"]
- # Validate tier
- if not isinstance(tier, int) or tier < 1 or tier > 3:
- logging.getLogger(__name__).warning(
- "Invalid tier %r in context %r, using default", tier, context
- )
- tier = default_tier
- model = _resolve_model(provider, tier, config_models)
- else:
- # No model or tier specified - use default tier
- model = _resolve_model(provider, default_tier, config_models)
-
- return (provider, model)
+ return (provider, explicit_model.strip())
+ return (provider, default_model_for_provider(provider))
def resolve_effective_route(context: str) -> tuple[str, str, str]:
@@ -841,7 +539,7 @@ def resolve_effective_route(context: str) -> tuple[str, str, str]:
Interface is the talent context's registry ``type`` when it is one of
generate/cogitate, else "generate" — never pass any other value to
- resolve_provider (TYPE_DEFAULTS is keyed only on generate/cogitate).
+ resolve_provider.
"""
registry_entry = get_context_registry().get(context)
interface = (
@@ -849,7 +547,7 @@ def resolve_effective_route(context: str) -> tuple[str, str, str]:
if registry_entry and registry_entry.get("type") in ("generate", "cogitate")
else "generate"
)
- provider, model = resolve_provider(context, interface)
+ provider, model = resolve_provider(interface)
return (interface, provider, model)
@@ -865,15 +563,6 @@ def is_local_provider_needed(config: dict[str, Any] | None = None) -> bool:
if isinstance(type_config, dict) and type_config.get("provider") == "local":
return True
- contexts = providers.get("contexts", {})
- if not isinstance(contexts, dict):
- return False
- if any(
- isinstance(context_config, dict) and context_config.get("provider") == "local"
- for context_config in contexts.values()
- ):
- return True
-
from solstone.think.providers.state import local_runtime_ready
local_ready = local_runtime_ready()
@@ -1452,18 +1141,14 @@ def generate(
timeout_s: Optional[float] = None,
**kwargs: Any,
) -> str:
- """Generate text using the configured provider for the given context.
-
- Routes the request to the appropriate backend (Google, OpenAI, or Anthropic)
- based on the providers configuration in journal.json.
+ """Generate text using the configured generate provider.
Parameters
----------
contents : str or List
The content to send to the model.
context : str
- Context string for routing and token logging (e.g., "talent.system.meetings").
- This is required and determines which provider/model to use.
+ Context string for token logging and telemetry.
temperature : float
Temperature for generation (default: 0.3).
max_output_tokens : int
@@ -1503,7 +1188,7 @@ def generate(
# Allow model override via kwargs (used by callers with explicit model selection)
model_override = kwargs.pop("model", None)
- provider, model = resolve_provider(context, "generate")
+ provider, model = resolve_provider("generate")
if model_override:
model = model_override
@@ -1553,114 +1238,6 @@ def generate(
return result["text"]
-# ---------------------------------------------------------------------------
-# Provider Health & Fallback Helpers
-# ---------------------------------------------------------------------------
-
-
-def get_backup_provider(agent_type: str) -> Optional[str]:
- """Get the backup provider for the given agent type.
-
- Reads from the type-specific section in journal config, falling back
- to TYPE_DEFAULTS.
-
- Returns None if backup would be the same as the primary provider.
- """
- type_defaults = TYPE_DEFAULTS[agent_type]
- config = get_config()
- providers_config = config.get("providers", {})
- if not isinstance(providers_config, dict):
- providers_config = {}
- type_config = providers_config.get(agent_type, {})
- if not isinstance(type_config, dict):
- type_config = {}
- primary_provider = _resolve_default_provider(providers_config, agent_type)
- backup = type_config.get("backup", type_defaults["backup"])
- if primary_provider in {"local", NO_BRAIN_PROVIDER}:
- return None
- if backup == primary_provider:
- return None
- return backup
-
-
-def load_health_status() -> Optional[dict]:
- """Load health status from journal/health/talents.json.
-
- Returns parsed dict or None if file is missing/unreadable.
- """
- # Delegating wrapper -> providers.state (see lode contract).
- from solstone.think.providers import state
-
- return state.read_health_status()
-
-
-def is_provider_healthy(provider: str, health_data: Optional[dict]) -> bool:
- """Check if a provider is healthy based on health data.
-
- Returns True (assume healthy) when:
- - health_data is None (no data available)
- - No results exist for the provider
- - Any result for the provider has ok=True
-
- Returns False only when all results for the provider have ok=False.
- """
- # Delegating wrapper -> providers.state (see lode contract).
- from solstone.think.providers import state
-
- return state.is_provider_healthy(provider, health_data)
-
-
-def is_provider_model_interface_healthy(
- provider: str,
- model: str,
- interface: str,
- health_data: Optional[dict],
-) -> bool:
- """Check health for a specific provider/model/interface row."""
- # Delegating wrapper -> providers.state (see lode contract).
- from solstone.think.providers import state
-
- return state.is_provider_model_interface_healthy(
- provider,
- model,
- interface,
- health_data,
- )
-
-
-def record_provider_failure(
- provider: str,
- tier: str,
- model: str,
- interface: str,
- reset_at_ms: int,
-) -> None:
- """Record a provider/model/interface quota failure in health status."""
- # Delegating wrapper -> providers.state (see lode contract).
- from solstone.think.providers import state
-
- state.record_quota_failure(provider, tier, model, interface, reset_at_ms)
-
-
-def should_recheck_health(health_data: Optional[dict]) -> bool:
- """Check if health data should be rechecked.
-
- Returns False when health_data is None or on parse errors.
- """
- # Delegating wrapper -> providers.state (see lode contract).
- from solstone.think.providers import state
-
- return state.should_recheck_health(health_data)
-
-
-def request_health_recheck() -> None:
- """Request a health re-check through the supervisor."""
- # Delegating wrapper -> providers.state (see lode contract).
- from solstone.think.providers import state
-
- state.request_recheck()
-
-
def generate_with_result(
contents: Union[str, List[Any]],
context: str,
@@ -1720,11 +1297,11 @@ def generate_with_result(
model_override = kwargs.pop("model", None)
provider_override = kwargs.pop("provider", None)
- provider, model = resolve_provider(context, "generate")
+ provider, model = resolve_provider("generate")
if provider_override:
provider = provider_override
if not model_override:
- model = resolve_model_for_provider(context, provider, "generate")
+ model = default_model_for_provider(provider)
if model_override:
model = model_override
@@ -1792,11 +1369,11 @@ async def agenerate_with_result(
model_override = kwargs.pop("model", None)
provider_override = kwargs.pop("provider", None)
- provider, model = resolve_provider(context, "generate")
+ provider, model = resolve_provider("generate")
if provider_override:
provider = provider_override
if not model_override:
- model = resolve_model_for_provider(context, provider, "generate")
+ model = default_model_for_provider(provider)
if model_override:
model = model_override
@@ -1852,18 +1429,14 @@ async def agenerate(
timeout_s: Optional[float] = None,
**kwargs: Any,
) -> str:
- """Async generate text using the configured provider for the given context.
-
- Routes the request to the appropriate backend (Google, OpenAI, or Anthropic)
- based on the providers configuration in journal.json.
+ """Async generate text using the configured generate provider.
Parameters
----------
contents : str or List
The content to send to the model.
context : str
- Context string for routing and token logging (e.g., "talent.system.meetings").
- This is required and determines which provider/model to use.
+ Context string for token logging and telemetry.
temperature : float
Temperature for generation (default: 0.3).
max_output_tokens : int
@@ -1903,7 +1476,7 @@ async def agenerate(
# Allow model override via kwargs (used by Batch for explicit model selection)
model_override = kwargs.pop("model", None)
- provider, model = resolve_provider(context, "generate")
+ provider, model = resolve_provider("generate")
if model_override:
model = model_override
@@ -1955,7 +1528,7 @@ async def agenerate(
__all__ = [
# Provider configuration
- "TYPE_DEFAULTS",
+ "DEFAULT_MODEL_BY_PROVIDER",
"NO_BRAIN_PROVIDER",
"NoBrainConfiguredError",
"AttestationFailedError",
@@ -1965,13 +1538,12 @@ __all__ = [
"get_context_registry",
# Model constants (used by provider backends for defaults)
"GEMINI_FLASH",
- "GPT_5",
+ "GPT_5_MINI",
"CLAUDE_SONNET_4",
"DEFAULT_PROVIDER_TIMEOUT_S",
"QWEN_35_9B",
"GEMMA4_26B_A4B_4BIT",
"LOCAL_MODEL",
- "MLX_FLASH",
# Model capability helpers
"model_supports",
# Unified API
@@ -1982,6 +1554,7 @@ __all__ = [
"finish_reason_error",
"IncompleteTextError",
"ProviderResponseInvalidError",
+ "default_model_for_provider",
"resolve_provider",
"resolve_effective_route",
"is_local_provider_needed",
diff --git a/solstone/think/planner.md b/solstone/think/planner.md
index 29bf3c190..57cf90919 100644
--- a/solstone/think/planner.md
+++ b/solstone/think/planner.md
@@ -1,6 +1,5 @@
---
context: planner.generate
-tier: 2
label: Agent Prompt Generation
group: Think
---
diff --git a/solstone/think/providers/shared.py b/solstone/think/providers/shared.py
index 355fe2512..44891e9ab 100644
--- a/solstone/think/providers/shared.py
+++ b/solstone/think/providers/shared.py
@@ -126,17 +126,6 @@ class TextDeltaEvent(TypedDict, total=False):
raw: Optional[list[dict[str, Any]]] # Original provider JSON event(s)
-class FallbackEvent(TypedDict, total=False):
- """Event emitted when provider fallback occurs."""
-
- event: Required[Literal["fallback"]]
- ts: Required[int]
- original_provider: Required[str]
- backup_provider: Required[str]
- reason: Required[str] # "preflight" or "on_failure"
- error: Optional[str] # Error message for on_failure case
-
-
Event = Union[
ToolStartEvent,
ToolEndEvent,
@@ -146,7 +135,6 @@ Event = Union[
ThinkingEvent,
TextDeltaEvent,
TalentUpdatedEvent,
- FallbackEvent,
]
diff --git a/solstone/think/providers/state.py b/solstone/think/providers/state.py
index d5b713112..b47caa918 100644
--- a/solstone/think/providers/state.py
+++ b/solstone/think/providers/state.py
@@ -159,7 +159,6 @@ def _summarize_health_results(results: list[dict[str, Any]]) -> dict[str, int]:
def record_quota_failure(
provider: str,
- tier: str,
model: str,
interface: str,
reset_at_ms: int,
@@ -187,7 +186,6 @@ def record_quota_failure(
results = []
failure_row = {
"provider": provider,
- "tier": tier,
"model": model,
"interface": interface,
"ok": False,
@@ -749,7 +747,7 @@ def readiness_for_context(context: str, interface: str) -> ProviderState:
"""Resolve a context then return passive readiness for its provider."""
from solstone.think.models import resolve_provider
- provider, model = resolve_provider(context, interface)
+ provider, model = resolve_provider(interface)
provider_state = readiness_for_provider(provider, interface, model)
return replace(provider_state, context=context)
diff --git a/solstone/think/providers_cli.py b/solstone/think/providers_cli.py
index 73895b3e6..2df5f6a56 100644
--- a/solstone/think/providers_cli.py
+++ b/solstone/think/providers_cli.py
@@ -41,11 +41,10 @@ def _provider_status(provider_name: str) -> dict[str, object]:
def _check_generate(
provider_name: str,
- tier: int,
+ model: str,
timeout: int,
) -> tuple[str, str, str | None]:
"""Check generate interface for a provider."""
- from solstone.think.models import PROVIDER_DEFAULTS
from solstone.think.providers import PROVIDER_METADATA, get_provider_module
env_key = PROVIDER_METADATA[provider_name]["env_key"]
@@ -76,7 +75,6 @@ def _check_generate(
try:
module = get_provider_module(provider_name)
- model = PROVIDER_DEFAULTS[provider_name][tier]
# Connectivity probe with canned content; deliberately outside the
# confidential attestation gate so diagnostics can always run.
result = module.run_generate(
@@ -97,7 +95,7 @@ def _check_generate(
from solstone.think.models import log_token_usage
log_token_usage(
- model=PROVIDER_DEFAULTS[provider_name][tier],
+ model=model,
usage=usage,
context="health.check.generate",
type="generate",
@@ -111,10 +109,9 @@ def _check_generate(
async def _check_cogitate(
- provider_name: str, tier: int, timeout: int
+ provider_name: str, model: str, timeout: int
) -> tuple[str, str, str | None]:
"""Check cogitate interface for a provider by running a real prompt."""
- from solstone.think.models import PROVIDER_DEFAULTS
from solstone.think.providers import PROVIDER_METADATA, get_provider_module
env_key = PROVIDER_METADATA[provider_name]["env_key"]
@@ -176,7 +173,6 @@ async def _check_cogitate(
try:
module = get_provider_module(provider_name)
- model = PROVIDER_DEFAULTS[provider_name][tier]
config = {"prompt": "Say OK", "model": model, "provider": provider_name}
# Connectivity probe with canned content; deliberately outside the
# confidential attestation gate so diagnostics can always run.
@@ -197,47 +193,30 @@ async def _check_cogitate(
async def _run_check(args: argparse.Namespace) -> None:
"""Run connectivity checks against AI providers."""
- from solstone.think.models import PROVIDER_DEFAULTS, TIER_FLASH, TIER_LITE, TIER_PRO
+ from solstone.think.models import (
+ NO_BRAIN_PROVIDER,
+ default_model_for_provider,
+ resolve_provider,
+ )
from solstone.think.providers import PROVIDER_REGISTRY
- targeted_pairs = None
- if args.targeted and not args.provider and not args.tier:
+ lock_fd = None
+ if args.targeted and not args.provider:
import fcntl
- from solstone.think.models import (
- NO_BRAIN_PROVIDER,
- TYPE_DEFAULTS,
- get_backup_provider,
- resolve_provider,
- )
- from solstone.think.utils import get_config
-
- targeted_pairs = set()
- config = get_config()
- providers_config = config.get("providers", {})
- if not isinstance(providers_config, dict):
- providers_config = {}
- for talent_type, defaults in TYPE_DEFAULTS.items():
- type_config = providers_config.get(talent_type, {})
- if not isinstance(type_config, dict):
- type_config = {}
- provider, _ = resolve_provider("", talent_type)
- tier = type_config.get("tier", defaults["tier"])
- if provider != NO_BRAIN_PROVIDER:
- targeted_pairs.add((provider, tier))
- backup = get_backup_provider(talent_type)
- if backup:
- targeted_pairs.add((backup, tier))
-
lock_dir = Path(get_journal()) / "health"
lock_dir.mkdir(parents=True, exist_ok=True)
- lock_fd = open(lock_dir / "recheck.lock", "w")
+ lock_fd = open(lock_dir / "recheck.lock", "w", encoding="utf-8")
try:
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError:
lock_fd.close()
return
+ if args.model and not args.provider:
+ print("--model requires --provider", file=sys.stderr)
+ sys.exit(1)
+
if args.provider:
providers = args.provider
for name in providers:
@@ -252,13 +231,21 @@ async def _run_check(args: argparse.Namespace) -> None:
providers = list(PROVIDER_REGISTRY.keys())
interfaces = [args.interface] if args.interface else ["generate", "cogitate"]
- tier_names = {1: "pro", 2: "flash", 3: "lite"}
- tiers = [args.tier] if args.tier else [TIER_PRO, TIER_FLASH, TIER_LITE]
- provider_width = max(len(n) for n in providers) if providers else 0
- tier_width = max(len(tier_names[t]) for t in tiers)
- model_names = {PROVIDER_DEFAULTS[p][t] for p in providers for t in tiers}
- model_width = max(len(m) for m in model_names) if model_names else 0
+ probes: list[tuple[str, str, str]] = []
+ if args.targeted and not args.provider:
+ for interface_name in interfaces:
+ provider, model = resolve_provider(interface_name)
+ if provider != NO_BRAIN_PROVIDER:
+ probes.append((provider, model, interface_name))
+ else:
+ for provider_name in providers:
+ model = args.model or default_model_for_provider(provider_name)
+ for interface_name in interfaces:
+ probes.append((provider_name, model, interface_name))
+
+ provider_width = max((len(provider) for provider, _, _ in probes), default=0)
+ model_width = max((len(model) for _, model, _ in probes), default=0)
interface_width = max(len(n) for n in interfaces) if interfaces else 0
total = 0
@@ -266,86 +253,54 @@ async def _run_check(args: argparse.Namespace) -> None:
failed = 0
skipped = 0
results: list[dict[str, object]] = []
- cache: dict[tuple[str, str, str], tuple[str, str, str | None, str]] = {}
-
- for provider_name in providers:
- for tier in tiers:
- if (
- targeted_pairs is not None
- and (provider_name, tier) not in targeted_pairs
- ):
- continue
- model = PROVIDER_DEFAULTS[provider_name][tier]
- for interface_name in interfaces:
- cache_key = (provider_name, model, interface_name)
- if cache_key in cache:
- status, message, reason_code, source_tier = cache[cache_key]
- elapsed_s = 0.0
- elapsed_s_rounded = 0.0
- reused_from = source_tier
- else:
- start = time.perf_counter()
- if interface_name == "generate":
- status, message, reason_code = _check_generate(
- provider_name, tier, args.timeout
- )
- else:
- status, message, reason_code = await _check_cogitate(
- provider_name, tier, args.timeout
- )
- elapsed_s = time.perf_counter() - start
- elapsed_s_rounded = round(elapsed_s, 1)
- cache[cache_key] = (
- status,
- message,
- reason_code,
- tier_names[tier],
- )
- reused_from = None
-
- result: dict[str, object] = {
- "provider": provider_name,
- "tier": tier_names[tier],
- "model": model,
- "interface": interface_name,
- "ok": status != "fail",
- "status": status,
- "reason_code": reason_code,
- "message": str(message),
- "elapsed_s": elapsed_s_rounded,
- }
- if reused_from:
- result["reused_from"] = reused_from
- results.append(result)
-
- if not args.json:
- if reused_from:
- mark = "="
- display_message = f"{message} (={reused_from})"
- else:
- if status == "ok":
- mark = "✓"
- elif status == "skip":
- mark = "-"
- else:
- mark = "✗"
- display_message = str(message)
- print(
- f"{mark} "
- f"{provider_name:<{provider_width}} "
- f"{tier_names[tier]:<{tier_width}} "
- f"{model:<{model_width}} "
- f"{interface_name:<{interface_width}} "
- f"{display_message} ({elapsed_s:.1f}s)"
- )
-
- total += 1
- if status == "ok":
- passed += 1
- elif status == "skip":
- skipped += 1
- else:
- failed += 1
+
+ for provider_name, model, interface_name in probes:
+ start = time.perf_counter()
+ if interface_name == "generate":
+ status, message, reason_code = _check_generate(
+ provider_name, model, args.timeout
+ )
+ else:
+ status, message, reason_code = await _check_cogitate(
+ provider_name, model, args.timeout
+ )
+ elapsed_s = time.perf_counter() - start
+ elapsed_s_rounded = round(elapsed_s, 1)
+
+ result: dict[str, object] = {
+ "provider": provider_name,
+ "model": model,
+ "interface": interface_name,
+ "ok": status != "fail",
+ "status": status,
+ "reason_code": reason_code,
+ "message": str(message),
+ "elapsed_s": elapsed_s_rounded,
+ }
+ results.append(result)
+
+ if not args.json:
+ if status == "ok":
+ mark = "✓"
+ elif status == "skip":
+ mark = "-"
+ else:
+ mark = "✗"
+ print(
+ f"{mark} "
+ f"{provider_name:<{provider_width}} "
+ f"{model:<{model_width}} "
+ f"{interface_name:<{interface_width}} "
+ f"{message} ({elapsed_s:.1f}s)"
+ )
+
+ total += 1
+ if status == "ok":
+ passed += 1
+ elif status == "skip":
+ skipped += 1
+ else:
+ failed += 1
any_failed = any(r["status"] == "fail" for r in results)
@@ -373,6 +328,8 @@ async def _run_check(args: argparse.Namespace) -> None:
)
else:
print(f"{total} checks: {passed} passed, {skipped} skipped, {failed} failed")
+ if lock_fd is not None:
+ lock_fd.close()
sys.exit(1 if any_failed else 0)
@@ -401,11 +358,9 @@ async def main_async() -> None:
help="Timeout in seconds for generate checks (default: 30)",
)
check_parser.add_argument(
- "--tier",
- type=int,
- choices=[1, 2, 3],
+ "--model",
default=None,
- help="Tier to check (1=pro, 2=flash, 3=lite; default: all)",
+ help="Model to check with --provider (default: provider default model)",
)
check_parser.add_argument(
"--json", action="store_true", help="Output results as JSON"
@@ -413,7 +368,7 @@ async def main_async() -> None:
check_parser.add_argument(
"--targeted",
action="store_true",
- help="Only check configured provider+tier pairs (used by automated rechecks)",
+ help="Only check configured active routes (used by automated rechecks)",
)
args = setup_cli(parser)
diff --git a/solstone/think/talent.py b/solstone/think/talent.py
index 97a0d3ccb..e64487567 100644
--- a/solstone/think/talent.py
+++ b/solstone/think/talent.py
@@ -334,10 +334,6 @@ def get_talent_configs(
info["disabled"] = override["disabled"]
if "extract" in override:
info["extract"] = override["extract"]
- if "tier" in override:
- info["tier"] = override["tier"]
- if "provider" in override:
- info["provider"] = override["provider"]
# Validate: scheduled prompts must have explicit priority
for key, info in configs.items():
diff --git a/solstone/think/talent_provenance.py b/solstone/think/talent_provenance.py
index 329146cae..24bbd10a5 100644
--- a/solstone/think/talent_provenance.py
+++ b/solstone/think/talent_provenance.py
@@ -109,7 +109,6 @@ def write_provenance(
output_size: int,
provider: str | None,
model: str | None,
- fallback_from: str | None,
generation_params: dict[str, Any],
completed_at_ms: int,
use_id: str | None,
@@ -124,7 +123,6 @@ def write_provenance(
"output_size": output_size,
"provider": provider,
"model": model,
- "fallback_from": fallback_from,
"generation_params": generation_params,
"completed_at_ms": completed_at_ms,
"use_id": use_id,
diff --git a/solstone/think/talents.py b/solstone/think/talents.py
index 0bbc74732..41873f0f8 100644
--- a/solstone/think/talents.py
+++ b/solstone/think/talents.py
@@ -553,12 +553,8 @@ def prepare_config(request: dict) -> dict:
"""
from solstone.think.models import (
NO_BRAIN_PROVIDER,
- TIER_FLASH,
- TIER_LITE,
- TIER_PRO,
NoBrainConfiguredError,
- _resolve_tier,
- resolve_model_for_provider,
+ default_model_for_provider,
resolve_provider,
type_default_is_local,
)
@@ -655,15 +651,13 @@ def prepare_config(request: dict) -> dict:
# Resolve provider and model from context
context = key_to_context(name)
talent_type = config["type"]
- default_provider, default_model = resolve_provider(context, talent_type)
+ default_provider, default_model = resolve_provider(talent_type)
if type_default_is_local(talent_type):
# Local type-default is a hard runtime promise: a frontmatter/request
- # cloud provider pin may not force a local-lane talent onto cloud. An
- # explicit local pin (provider: local + model) at the talent/request
- # level is honored verbatim; otherwise the local model comes from
- # resolve_provider (which already carries any context-level local pin).
- provider = default_provider # "local" (resolve_provider already forced it)
+ # cloud provider pin may not force a local-lane talent onto cloud.
+ # An explicit local request pin with a model is honored verbatim.
+ provider = default_provider
if config.get("provider") == "local" and config.get("model"):
model = config["model"]
else:
@@ -673,7 +667,7 @@ def prepare_config(request: dict) -> dict:
model = config.get("model")
if not model:
if provider != default_provider:
- model = resolve_model_for_provider(context, provider, talent_type)
+ model = default_model_for_provider(provider)
else:
model = default_model
@@ -682,37 +676,6 @@ def prepare_config(request: dict) -> dict:
config["context"] = context
if provider == NO_BRAIN_PROVIDER:
raise NoBrainConfiguredError()
- tier = _resolve_tier(context, talent_type)
- config["tier"] = {
- TIER_PRO: "pro",
- TIER_FLASH: "flash",
- TIER_LITE: "lite",
- }.get(tier, str(tier))
-
- # --- Provider fallback: preflight swap if primary is unhealthy ---
- from solstone.think.models import (
- get_backup_provider,
- is_provider_model_interface_healthy,
- load_health_status,
- should_recheck_health,
- )
- from solstone.think.providers import PROVIDER_METADATA
-
- health_data = load_health_status()
- config["health_stale"] = should_recheck_health(health_data)
-
- if provider != "local" and not is_provider_model_interface_healthy(
- provider, model, talent_type, health_data
- ):
- backup = get_backup_provider(talent_type)
- if backup and backup != provider:
- env_key = PROVIDER_METADATA.get(backup, {}).get("env_key")
- if not env_key or os.getenv(env_key):
- config["fallback_from"] = provider
- config["provider"] = backup
- config["model"] = resolve_model_for_provider(
- context, backup, talent_type
- )
# Check if disabled
if config.get("disabled"):
@@ -1031,7 +994,6 @@ def _runtime_identity(config: dict, runtime_json_schema: Any) -> dict[str, Any]:
"sources": _normalized_sources(config),
"provider": config.get("provider"),
"model": config.get("model"),
- "fallback_from": config.get("fallback_from"),
"generation_params": _generation_params(config),
"runtime": {
"day": config.get("day"),
@@ -1221,7 +1183,6 @@ def _write_clean_provenance(
output_size=output_size,
provider=config.get("provider"),
model=config.get("model"),
- fallback_from=config.get("fallback_from"),
generation_params=_generation_params(config),
completed_at_ms=completed_at_ms,
use_id=config.get("use_id"),
@@ -1310,41 +1271,9 @@ def _emit_terminal_hook_error(
from solstone.think.models import (
NO_BRAIN_PROVIDER,
- AttestationNotVerifiedError,
- NoBrainConfiguredError,
_raise_if_confidential_unverified,
)
-_NON_RETRYABLE_ERRORS = (
- TalentHookError,
- # No implicit cloud fallback: a journal with no thinking engine selected
- # must stop here rather than retrying on any cloud provider.
- NoBrainConfiguredError,
- AttestationNotVerifiedError,
- ValueError,
- json.JSONDecodeError,
- KeyError,
- TypeError,
- AttributeError,
- FileNotFoundError,
- PermissionError,
- NotImplementedError,
- QuotaExhaustedError,
-)
-
-
-def _is_retryable_error(exc: Exception) -> bool:
- """Check if an exception is likely a provider error worth retrying.
-
- Returns False for local/code errors (ValueError, KeyError, etc.).
- Returns True for everything else (SDK connection, timeout, server errors).
- """
- return not isinstance(exc, _NON_RETRYABLE_ERRORS)
-
-
-def _should_fallback(exc: Exception) -> bool:
- return _is_retryable_error(exc) or isinstance(exc, QuotaExhaustedError)
-
def _classify_degraded(usage: dict | None, config: dict) -> dict | None:
"""Flag an opted-in talent run whose model produced near-zero output.
@@ -1365,6 +1294,20 @@ def _classify_degraded(usage: dict | None, config: dict) -> dict | None:
return None
+def _record_quota_failure(config: dict, exc: QuotaExhaustedError) -> int:
+ """Persist a quota failure row and return its reset time."""
+ reset_at_ms = now_ms() + (exc.retry_delay_ms or 0)
+ from solstone.think.providers import state
+
+ state.record_quota_failure(
+ str(config.get("provider") or ""),
+ str(config.get("model") or ""),
+ str(config.get("type") or ""),
+ reset_at_ms,
+ )
+ return reset_at_ms
+
+
async def _execute_with_tools(
config: dict,
emit_event: Callable[[dict], None],
@@ -1454,12 +1397,8 @@ async def _execute_with_tools(
_emit_terminal_hook_error(config, emit_event, exc)
return
except Exception as exc:
- if provider in {"local", NO_BRAIN_PROVIDER}:
- raise
- if config.get("fallback_from") or not _should_fallback(exc):
- raise
if isinstance(exc, QuotaExhaustedError):
- reset_at_ms = now_ms() + (exc.retry_delay_ms or 0)
+ reset_at_ms = _record_quota_failure(config, exc)
emit_event(
{
"event": "error",
@@ -1472,73 +1411,7 @@ async def _execute_with_tools(
"terminal": False,
}
)
- from solstone.think.models import record_provider_failure
-
- record_provider_failure(
- provider,
- config["tier"],
- config["model"],
- config["type"],
- reset_at_ms,
- )
- from solstone.think.models import (
- get_backup_provider,
- resolve_model_for_provider,
- )
- from solstone.think.providers import PROVIDER_METADATA
-
- backup = get_backup_provider("cogitate")
- if not backup or backup == provider:
- raise
- env_key = PROVIDER_METADATA.get(backup, {}).get("env_key")
- if env_key and not os.getenv(env_key):
- raise
-
- context = config.get("context")
- if not context:
- from solstone.think.talent import key_to_context
-
- context = key_to_context(config["name"])
- backup_model = resolve_model_for_provider(context, backup, "cogitate")
-
- emit_event(
- {
- "event": "fallback",
- "ts": now_ms(),
- "original_provider": provider,
- "backup_provider": backup,
- "reason": "on_failure",
- "error": str(exc),
- }
- )
-
- config["fallback_from"] = provider
- config["provider"] = backup
- config["model"] = backup_model
-
- backup_mod = get_provider_module(backup)
-
- # Suppress error events from backup provider — if backup also fails
- # we report the original error, not the backup's error.
- def backup_emit(data: Event) -> None:
- if data.get("event") == "error":
- return
- talent_emit_event(data)
-
- try:
- await backup_mod.run_cogitate(config=config, on_event=backup_emit)
- except Exception:
- # Ensure the original error is reported by the caller even if the
- # primary provider already emitted its own error event (_evented).
- if hasattr(exc, "_evented"):
- delattr(exc, "_evented")
- raise exc
- finally:
- if config.get("health_stale"):
- from solstone.think.models import request_health_recheck
-
- request_health_recheck()
- config["health_stale"] = False
+ raise
async def _execute_generate(
@@ -1592,6 +1465,8 @@ async def _execute_generate(
)
except Exception as exc:
provider = config.get("provider", "google")
+ if isinstance(exc, QuotaExhaustedError):
+ _record_quota_failure(config, exc)
if provider == NO_BRAIN_PROVIDER:
raise
if provider == "local":
@@ -1641,60 +1516,7 @@ async def _execute_generate(
retry_exc.retries = retries
raise
else:
- if config.get("fallback_from") or not _should_fallback(exc):
- raise
- from solstone.think.models import (
- get_backup_provider,
- resolve_model_for_provider,
- )
- from solstone.think.providers import PROVIDER_METADATA
-
- backup = get_backup_provider("generate")
- if not backup or backup == provider:
- raise
- env_key = PROVIDER_METADATA.get(backup, {}).get("env_key")
- if env_key and not os.getenv(env_key):
- raise
-
- backup_model = resolve_model_for_provider(context, backup, "generate")
-
- emit_event(
- {
- "event": "fallback",
- "ts": now_ms(),
- "original_provider": provider,
- "backup_provider": backup,
- "reason": "on_failure",
- "error": str(exc),
- }
- )
-
- config["fallback_from"] = provider
- config["provider"] = backup
- config["model"] = backup_model
-
- try:
- gen_result = generate_with_result(
- contents=contents,
- context=context,
- temperature=temperature,
- max_output_tokens=max_output_tokens,
- thinking_budget=thinking_budget,
- system_instruction=system_instruction,
- json_output=is_json_output,
- json_schema=runtime_json_schema,
- timeout_s=timeout_s,
- provider=backup,
- model=backup_model,
- )
- except Exception:
- raise exc
- finally:
- if config.get("health_stale"):
- from solstone.think.models import request_health_recheck
-
- request_health_recheck()
- config["health_stale"] = False
+ raise
raw_result = gen_result["text"]
if output_format == "md":
@@ -1844,18 +1666,6 @@ async def _run_talent(
start_event["chat_id"] = config["chat_id"]
emit_event(start_event)
- # Emit preflight fallback event if provider was swapped
- if config.get("fallback_from"):
- emit_event(
- {
- "event": "fallback",
- "ts": now_ms(),
- "original_provider": config["fallback_from"],
- "backup_provider": config["provider"],
- "reason": "preflight",
- }
- )
-
# Handle skip conditions
skip_reason = config.get("skip_reason")
if skip_reason:
diff --git a/solstone/think/thinking.py b/solstone/think/thinking.py
index 663fcb488..71bd53514 100644
--- a/solstone/think/thinking.py
+++ b/solstone/think/thinking.py
@@ -24,7 +24,6 @@ from datetime import date, datetime, timedelta, timezone
from pathlib import Path
from typing import Any
-from solstone.observe.describe import FRAME_CONTEXT
from solstone.think import admission
from solstone.think.activities import (
append_activity_record,
@@ -510,7 +509,7 @@ def _segment_work_uses_local() -> bool:
def _describe_uses_local() -> bool:
"""Return True when screen-describe resolves to the local provider."""
- provider, _ = resolve_provider(FRAME_CONTEXT, "generate")
+ provider, _ = resolve_provider("generate")
return provider == "local"
diff --git a/tests/baselines/api/stats/stats.json b/tests/baselines/api/stats/stats.json
index e5ce75eeb..de5499f37 100644
--- a/tests/baselines/api/stats/stats.json
+++ b/tests/baselines/api/stats/stats.json
@@ -13,7 +13,6 @@
"schema": "chat.schema.json",
"source": "system",
"thinking_budget": 4096,
- "tier": 2,
"title": "Chat",
"type": "generate"
},
@@ -43,7 +42,6 @@
"schedule": "activity",
"schema": "story.schema.json",
"source": "system",
- "tier": 3,
"title": "Conversation Story",
"type": "generate"
},
@@ -113,7 +111,6 @@
"output": "json",
"path": "/solstone/apps/entities/talent/detection.md",
"priority": 15,
- "provider": "google",
"schedule": "segment",
"schema": "detection.schema.json",
"source": "app",
@@ -144,7 +141,6 @@
"schema": "entities_review.schema.json",
"source": "app",
"thinking_budget": 2048,
- "tier": 2,
"title": "Entity Reviewer",
"type": "generate"
},
@@ -186,7 +182,6 @@
"schema": "entity_observer.schema.json",
"source": "app",
"thinking_budget": 2048,
- "tier": 2,
"title": "Entity Observer",
"type": "generate"
},
@@ -282,7 +277,6 @@
"schedule": "activity",
"schema": "participation.schema.json",
"source": "system",
- "tier": 3,
"timeout_s": 480,
"title": "Participation",
"type": "generate"
@@ -310,7 +304,6 @@
"schema": "pulse.schema.json",
"source": "system",
"thinking_budget": 1024,
- "tier": 3,
"title": "Pulse",
"type": "generate"
},
@@ -353,7 +346,6 @@
"schedule": "segment",
"schema": "screen.schema.json",
"source": "system",
- "tier": 3,
"title": "Screen Record",
"type": "generate"
},
@@ -373,7 +365,6 @@
"schedule": "segment",
"schema": "sense.schema.json",
"source": "system",
- "tier": 3,
"timeout_s": 480,
"title": "Segment Sense",
"type": "generate"
@@ -424,7 +415,6 @@
"schema": "steward.schema.json",
"source": "system",
"thinking_budget": 1024,
- "tier": 3,
"title": "Steward",
"type": "generate"
},
@@ -444,11 +434,9 @@
"output": "json",
"path": "/solstone/apps/timeline/talent/segment_summary.md",
"priority": 41,
- "provider": "google",
"schedule": "segment",
"schema": "segment_summary.schema.json",
"source": "app",
- "tier": 3,
"type": "generate"
},
"work": {
diff --git a/tests/baselines/api/thinking/providers.json b/tests/baselines/api/thinking/providers.json
index fb8a51305..fd4ccab71 100644
--- a/tests/baselines/api/thinking/providers.json
+++ b/tests/baselines/api/thinking/providers.json
@@ -17,153 +17,7 @@
"split": false
},
"ai_readiness": {
- "context_routes": [
- {
- "context": null,
- "detail": "Finish local model setup, then try the request again.",
- "interface": "generate",
- "model": "local/qwen3.5-4b",
- "operator_detail": "reason_code=local_model_missing; provider=local; status=blocked; model=local/qwen3.5-4b; interface=generate",
- "provider": "local",
- "reason_code": "local_model_missing",
- "recovery_action": {
- "href": "/app/thinking/#local-setup",
- "label": "Open Local Model Setup"
- },
- "semantic_key": "local_model_missing:local:local/qwen3.5-4b",
- "severity": "blocker",
- "status": "blocked",
- "summary": "local model setup is not finished",
- "work_key": null
- },
- {
- "context": null,
- "detail": "Open provider setup and add credentials, then try again.",
- "interface": "generate",
- "model": "claude-sonnet-4-5",
- "operator_detail": "reason_code=provider_key_missing; provider=anthropic; status=blocked; model=claude-sonnet-4-5; interface=generate; message=ANTHROPIC_API_KEY not set",
- "provider": "anthropic",
- "reason_code": "provider_key_missing",
- "recovery_action": {
- "href": "/app/thinking/#main",
- "label": "Open Settings"
- },
- "semantic_key": "provider_key_missing:anthropic:",
- "severity": "blocker",
- "status": "blocked",
- "summary": "Anthropic needs credentials before it can read your screen descriptions",
- "work_key": null
- },
- {
- "context": null,
- "detail": "Open provider setup and add credentials, then try again.",
- "interface": "generate",
- "model": "gemini-custom-flash-test",
- "operator_detail": "reason_code=provider_key_missing; provider=google; status=blocked; model=gemini-custom-flash-test; interface=generate; message=GOOGLE_API_KEY not set",
- "provider": "google",
- "reason_code": "provider_key_missing",
- "recovery_action": {
- "href": "/app/thinking/#main",
- "label": "Open Settings"
- },
- "semantic_key": "provider_key_missing:google:",
- "severity": "blocker",
- "status": "blocked",
- "summary": "Gemini needs credentials before it can read your screen descriptions",
- "work_key": null
- },
- {
- "context": null,
- "detail": "Open provider setup and add credentials, then try again.",
- "interface": "generate",
- "model": "gemini-flash-lite-latest",
- "operator_detail": "reason_code=provider_key_missing; provider=google; status=blocked; model=gemini-flash-lite-latest; interface=generate; message=GOOGLE_API_KEY not set",
- "provider": "google",
- "reason_code": "provider_key_missing",
- "recovery_action": {
- "href": "/app/thinking/#main",
- "label": "Open Settings"
- },
- "semantic_key": "provider_key_missing:google:",
- "severity": "blocker",
- "status": "blocked",
- "summary": "Gemini needs credentials before it can read your screen descriptions",
- "work_key": null
- },
- {
- "context": null,
- "detail": "Open provider setup and add credentials, then try again.",
- "interface": "generate",
- "model": "gemini-pro-latest",
- "operator_detail": "reason_code=provider_key_missing; provider=google; status=blocked; model=gemini-pro-latest; interface=generate; message=GOOGLE_API_KEY not set",
- "provider": "google",
- "reason_code": "provider_key_missing",
- "recovery_action": {
- "href": "/app/thinking/#main",
- "label": "Open Settings"
- },
- "semantic_key": "provider_key_missing:google:",
- "severity": "blocker",
- "status": "blocked",
- "summary": "Gemini needs credentials before it can read your screen descriptions",
- "work_key": null
- },
- {
- "context": null,
- "detail": "Open provider setup and add credentials, then try again.",
- "interface": "generate",
- "model": "gpt-5-mini",
- "operator_detail": "reason_code=provider_key_missing; provider=openai; status=blocked; model=gpt-5-mini; interface=generate; message=OPENAI_API_KEY not set",
- "provider": "openai",
- "reason_code": "provider_key_missing",
- "recovery_action": {
- "href": "/app/thinking/#main",
- "label": "Open Settings"
- },
- "semantic_key": "provider_key_missing:openai:",
- "severity": "blocker",
- "status": "blocked",
- "summary": "OpenAI needs credentials before it can read your screen descriptions",
- "work_key": null
- },
- {
- "context": null,
- "detail": "Open provider setup and add credentials, then try again.",
- "interface": "generate",
- "model": "gpt-5.4-mini",
- "operator_detail": "reason_code=provider_key_missing; provider=openai; status=blocked; model=gpt-5.4-mini; interface=generate; message=OPENAI_API_KEY not set",
- "provider": "openai",
- "reason_code": "provider_key_missing",
- "recovery_action": {
- "href": "/app/thinking/#main",
- "label": "Open Settings"
- },
- "semantic_key": "provider_key_missing:openai:",
- "severity": "blocker",
- "status": "blocked",
- "summary": "OpenAI needs credentials before it can read your screen descriptions",
- "work_key": null
- }
- ],
"groups": [
- {
- "context": null,
- "detail": "Finish local model setup, then try the request again.",
- "interface": "generate",
- "model": "local/qwen3.5-4b",
- "operator_detail": "reason_code=local_model_missing; provider=local; status=blocked; model=local/qwen3.5-4b; interface=generate",
- "provider": "local",
- "reason_code": "local_model_missing",
- "recovery_action": {
- "href": "/app/thinking/#local-setup",
- "label": "Open Local Model Setup"
- },
- "semantic_key": "local_model_missing:local:local/qwen3.5-4b",
- "severity": "blocker",
- "status": "blocked",
- "summary": "local model setup is not finished",
- "work_key": null
- },
{
"context": null,
"detail": "Open provider setup and add credentials, then try again.",
@@ -186,26 +40,8 @@
"context": null,
"detail": "Open provider setup and add credentials, then try again.",
"interface": "generate",
- "model": "claude-sonnet-4-5",
- "operator_detail": "reason_code=provider_key_missing; provider=anthropic; status=blocked; model=claude-sonnet-4-5; interface=generate; message=ANTHROPIC_API_KEY not set",
- "provider": "anthropic",
- "reason_code": "provider_key_missing",
- "recovery_action": {
- "href": "/app/thinking/#main",
- "label": "Open Settings"
- },
- "semantic_key": "provider_key_missing:anthropic:",
- "severity": "blocker",
- "status": "blocked",
- "summary": "Anthropic needs credentials before it can read your screen descriptions",
- "work_key": null
- },
- {
- "context": null,
- "detail": "Open provider setup and add credentials, then try again.",
- "interface": "generate",
- "model": "gemini-custom-flash-test",
- "operator_detail": "reason_code=provider_key_missing; provider=google; status=blocked; model=gemini-custom-flash-test; interface=generate; message=GOOGLE_API_KEY not set",
+ "model": "gemini-flash-latest",
+ "operator_detail": "reason_code=provider_key_missing; provider=google; status=blocked; model=gemini-flash-latest; interface=generate; message=GOOGLE_API_KEY not set",
"provider": "google",
"reason_code": "provider_key_missing",
"recovery_action": {
@@ -242,8 +78,8 @@
"context": null,
"detail": "Open provider setup and add credentials, then try again.",
"interface": "generate",
- "model": "gemini-custom-flash-test",
- "operator_detail": "reason_code=provider_key_missing; provider=google; status=blocked; model=gemini-custom-flash-test; interface=generate; message=GOOGLE_API_KEY not set",
+ "model": "gemini-flash-latest",
+ "operator_detail": "reason_code=provider_key_missing; provider=google; status=blocked; model=gemini-flash-latest; interface=generate; message=GOOGLE_API_KEY not set",
"provider": "google",
"reason_code": "provider_key_missing",
"recovery_action": {
@@ -276,8 +112,8 @@
"work_key": null
},
"summary": {
- "active_groups": 4,
- "blocked_count": 4,
+ "active_groups": 2,
+ "blocked_count": 2,
"severity": "blocker",
"status": "blocked"
}
@@ -288,396 +124,12 @@
"openai": false
},
"cogitate": {
- "backup": "anthropic",
- "provider": "openai",
- "tier": 2
- },
- "context_defaults": {
- "detect.created": {
- "group": "Import",
- "label": "Date Detection",
- "tier": 3
- },
- "observe.describe.browsing": {
- "group": "Screen Analysis",
- "label": "Browsing",
- "tier": 2
- },
- "observe.describe.calendar": {
- "group": "Screen Analysis",
- "label": "Calendar",
- "tier": 2
- },
- "observe.describe.code": {
- "group": "Screen Analysis",
- "label": "Code",
- "tier": 2
- },
- "observe.describe.frame": {
- "group": "Observe",
- "label": "Screen Categorization",
- "tier": 3
- },
- "observe.describe.gaming": {
- "group": "Screen Analysis",
- "label": "Gaming",
- "tier": 2
- },
- "observe.describe.media": {
- "group": "Screen Analysis",
- "label": "Media",
- "tier": 2
- },
- "observe.describe.meeting": {
- "group": "Screen Analysis",
- "label": "Meeting",
- "tier": 2
- },
- "observe.describe.messaging": {
- "group": "Screen Analysis",
- "label": "Messaging",
- "tier": 2
- },
- "observe.describe.productivity": {
- "group": "Screen Analysis",
- "label": "Productivity",
- "tier": 2
- },
- "observe.describe.reading": {
- "group": "Screen Analysis",
- "label": "Reading",
- "tier": 2
- },
- "observe.describe.social": {
- "group": "Screen Analysis",
- "label": "Social",
- "tier": 2
- },
- "observe.describe.terminal": {
- "group": "Screen Analysis",
- "label": "Terminal",
- "tier": 2
- },
- "observe.detect.json": {
- "group": "Import",
- "label": "Normalization",
- "tier": 2
- },
- "observe.detect.segment": {
- "group": "Import",
- "label": "Segmentation",
- "tier": 2
- },
- "observe.enrich": {
- "group": "Observe",
- "label": "Audio Enrichment",
- "tier": 2
- },
- "observe.extract.selection": {
- "group": "Observe",
- "label": "Frame Selection",
- "tier": 2
- },
- "observe.transcribe.gemini": {
- "group": "Observe",
- "label": "Audio Transcription (Gemini)",
- "tier": 2
- },
- "planner.generate": {
- "group": "Think",
- "label": "Agent Prompt Generation",
- "tier": 2
- },
- "talent.entities.detection": {
- "disabled": false,
- "group": "Think",
- "label": "Entity Detection",
- "schedule": "segment",
- "tier": 2,
- "type": "generate"
- },
- "talent.entities.entities_review": {
- "disabled": false,
- "group": "Entities",
- "label": "Entity Reviewer",
- "schedule": "daily",
- "tier": 2,
- "type": "generate"
- },
- "talent.entities.entity_assist": {
- "disabled": false,
- "group": "Entities",
- "label": "Entity Assistant",
- "tier": 2,
- "type": "cogitate"
- },
- "talent.entities.entity_describe": {
- "disabled": false,
- "group": "Entities",
- "label": "Entity Description",
- "tier": 2,
- "type": "generate"
- },
- "talent.entities.entity_observer": {
- "disabled": false,
- "group": "Entities",
- "label": "Entity Observer",
- "schedule": "daily",
- "tier": 2,
- "type": "generate"
- },
- "talent.support.support": {
- "disabled": false,
- "group": "Think",
- "label": "Support",
- "tier": 2,
- "type": "cogitate"
- },
- "talent.system.chat": {
- "disabled": false,
- "group": "Think",
- "label": "Chat",
- "tier": 2,
- "type": "generate"
- },
- "talent.system.conversation": {
- "disabled": false,
- "group": "Think",
- "label": "Conversation Story",
- "schedule": "activity",
- "tier": 3,
- "type": "generate"
- },
- "talent.system.daily_schedule": {
- "disabled": false,
- "group": "Think",
- "label": "Maintenance Window",
- "schedule": "daily",
- "tier": 2,
- "type": "generate"
- },
- "talent.system.documents": {
- "disabled": false,
- "group": "Think",
- "label": "Document Analysis",
- "schedule": "segment",
- "tier": 2,
- "type": "generate"
- },
- "talent.system.event": {
- "disabled": false,
- "group": "Think",
- "label": "Event Story",
- "schedule": "activity",
- "tier": 2,
- "type": "generate"
- },
- "talent.system.exec": {
- "disabled": false,
- "group": "Think",
- "label": "Exec",
- "tier": 2,
- "type": "cogitate"
- },
- "talent.system.facet_newsletter": {
- "disabled": false,
- "group": "Think",
- "label": "Facet Newsletter Generator",
- "schedule": "daily",
- "tier": 2,
- "type": "generate"
- },
- "talent.system.morning_briefing": {
- "disabled": false,
- "group": "Think",
- "label": "Morning Briefing",
- "schedule": "daily",
- "tier": 2,
- "type": "generate"
- },
- "talent.system.participation": {
- "disabled": false,
- "group": "Think",
- "label": "Participation",
- "schedule": "activity",
- "tier": 3,
- "type": "generate"
- },
- "talent.system.partner": {
- "disabled": false,
- "group": "Think",
- "label": "Partner Profile",
- "schedule": "weekly",
- "tier": 2,
- "type": "cogitate"
- },
- "talent.system.pulse": {
- "disabled": false,
- "group": "Think",
- "label": "Pulse",
- "schedule": "cadence",
- "tier": 3,
- "type": "generate"
- },
- "talent.system.read": {
- "disabled": false,
- "group": "Think",
- "label": "Read",
- "tier": 2,
- "type": "cogitate"
- },
- "talent.system.schedule": {
- "disabled": false,
- "group": "Think",
- "label": "Upcoming Schedule",
- "schedule": "daily",
- "tier": 2,
- "type": "generate"
- },
- "talent.system.screen": {
- "disabled": false,
- "group": "Think",
- "label": "Screen Record",
- "schedule": "segment",
- "tier": 3,
- "type": "generate"
- },
- "talent.system.sense": {
- "disabled": false,
- "group": "Think",
- "label": "Segment Sense",
- "schedule": "segment",
- "tier": 3,
- "type": "generate"
- },
- "talent.system.speaker_attribution": {
- "disabled": false,
- "group": "Think",
- "label": "Speaker Attribution",
- "schedule": "segment",
- "tier": 2,
- "type": "generate"
- },
- "talent.system.steward": {
- "disabled": false,
- "group": "Think",
- "label": "Steward",
- "schedule": "cadence",
- "tier": 3,
- "type": "generate"
- },
- "talent.system.weekly_reflection": {
- "disabled": false,
- "group": "Think",
- "label": "Weekly Reflection",
- "schedule": "weekly",
- "tier": 2,
- "type": "cogitate"
- },
- "talent.system.work": {
- "disabled": false,
- "group": "Think",
- "label": "Work Story",
- "schedule": "activity",
- "tier": 2,
- "type": "generate"
- },
- "talent.timeline.segment_summary": {
- "disabled": false,
- "group": "Think",
- "label": "timeline:segment_summary",
- "schedule": "segment",
- "tier": 3,
- "type": "generate"
- }
- },
- "contexts": {
- "observe.*": {
- "provider": "google",
- "tier": 3
- },
- "test.anthropic": {
- "model": "claude-sonnet-4-5",
- "provider": "anthropic"
- },
- "test.config.override": {
- "provider": "google",
- "tier": 2
- },
- "test.local": {
- "model": "local/qwen3.5-4b",
- "provider": "local"
- },
- "test.openai": {
- "model": "gpt-5-mini",
- "provider": "openai"
- },
- "test.tier": {
- "provider": "google",
- "tier": 1
- },
- "test.tier.inherit": {
- "tier": 3
- },
- "test.tier.override": {
- "provider": "openai",
- "tier": 2
- }
- },
- "effective_contexts": {
- "observe.*": {
- "differs_from_raw": false,
- "interface": "generate",
- "model": "gemini-flash-lite-latest",
- "provider": "google"
- },
- "test.anthropic": {
- "differs_from_raw": false,
- "interface": "generate",
- "model": "claude-sonnet-4-5",
- "provider": "anthropic"
- },
- "test.config.override": {
- "differs_from_raw": false,
- "interface": "generate",
- "model": "gemini-custom-flash-test",
- "provider": "google"
- },
- "test.local": {
- "differs_from_raw": false,
- "interface": "generate",
- "model": "local/qwen3.5-4b",
- "provider": "local"
- },
- "test.openai": {
- "differs_from_raw": false,
- "interface": "generate",
- "model": "gpt-5-mini",
- "provider": "openai"
- },
- "test.tier": {
- "differs_from_raw": false,
- "interface": "generate",
- "model": "gemini-pro-latest",
- "provider": "google"
- },
- "test.tier.inherit": {
- "differs_from_raw": false,
- "interface": "generate",
- "model": "gemini-flash-lite-latest",
- "provider": "google"
- },
- "test.tier.override": {
- "differs_from_raw": false,
- "interface": "generate",
- "model": "gpt-5.4-mini",
- "provider": "openai"
- }
+ "model": "gpt-5.4-mini",
+ "provider": "openai"
},
"generate": {
- "backup": "anthropic",
- "provider": "google",
- "tier": 2
+ "model": "gemini-flash-latest",
+ "provider": "google"
},
"google_backend": "auto",
"key_validation": {},
@@ -727,7 +179,7 @@
"model_missing",
"run `journal install-provider local`"
],
- "selected": true
+ "selected": false
},
"openai": {
"cogitate_ready": false,
diff --git a/tests/fixtures/journal/config/journal.json b/tests/fixtures/journal/config/journal.json
index b8385ac2e..0d05bd368 100644
--- a/tests/fixtures/journal/config/journal.json
+++ b/tests/fixtures/journal/config/journal.json
@@ -43,51 +43,11 @@
"providers": {
"generate": {
"provider": "google",
- "tier": 2,
- "backup": "anthropic"
+ "model": "gemini-custom-flash-test"
},
"cogitate": {
"provider": "openai",
- "tier": 2,
- "backup": "anthropic"
- },
- "contexts": {
- "test.openai": {
- "provider": "openai",
- "model": "gpt-5-mini"
- },
- "test.anthropic": {
- "provider": "anthropic",
- "model": "claude-sonnet-4-5"
- },
- "test.tier": {
- "provider": "google",
- "tier": 1
- },
- "test.tier.inherit": {
- "tier": 3
- },
- "test.tier.override": {
- "provider": "openai",
- "tier": 2
- },
- "test.config.override": {
- "provider": "google",
- "tier": 2
- },
- "observe.*": {
- "provider": "google",
- "tier": 3
- },
- "test.local": {
- "provider": "local",
- "model": "local/qwen3.5-4b"
- }
- },
- "models": {
- "google": {
- "2": "gemini-custom-flash-test"
- }
+ "model": "gpt-5.4-mini"
},
"bundled": {
"parakeet": {
diff --git a/tests/test_bad_media_corpus.py b/tests/test_bad_media_corpus.py
index 760fe4c35..a3a945dac 100644
--- a/tests/test_bad_media_corpus.py
+++ b/tests/test_bad_media_corpus.py
@@ -258,7 +258,7 @@ def _drive_describe(
)
monkeypatch.setattr(
"solstone.think.models.resolve_provider",
- lambda _context, _interface: ("google", "gemini-test"),
+ lambda _interface: ("google", "gemini-test"),
)
monkeypatch.setattr(describe, "callosum_send", lambda *args, **kwargs: None)
monkeypatch.setattr(describe, "select_frames_for_extraction", lambda *a, **k: [])
diff --git a/tests/test_batch.py b/tests/test_batch.py
index 9ab5f4022..dd8552ea4 100644
--- a/tests/test_batch.py
+++ b/tests/test_batch.py
@@ -225,6 +225,7 @@ async def test_batch_classifies_exception_metadata_when_attrs_missing(
assert results[0].reason_code == "network_unreachable"
assert results[0].reset_at_ms is None
assert results[0].provider == "google"
+ mock_resolve_provider.assert_called_once_with("generate")
def test_batch_update_clears_error_metadata():
diff --git a/tests/test_cluster.py b/tests/test_cluster.py
index 629f15555..43baeea00 100644
--- a/tests/test_cluster.py
+++ b/tests/test_cluster.py
@@ -18,27 +18,13 @@ SEGMENT = "120000_300"
def _patch_prepare_config_model_deps(monkeypatch) -> None:
- from solstone.think.models import TIER_LITE
-
monkeypatch.setattr(
"solstone.think.models.resolve_provider",
- lambda _context, _type: ("google", "gemini-test"),
+ lambda _type: ("google", "gemini-test"),
)
monkeypatch.setattr(
"solstone.think.models.type_default_is_local", lambda _type: False
)
- monkeypatch.setattr(
- "solstone.think.models._resolve_tier",
- lambda _context, _type: TIER_LITE,
- )
- monkeypatch.setattr("solstone.think.models.load_health_status", lambda: {})
- monkeypatch.setattr(
- "solstone.think.models.should_recheck_health", lambda _health: False
- )
- monkeypatch.setattr(
- "solstone.think.models.is_provider_model_interface_healthy",
- lambda _provider, _model, _type, _health: True,
- )
def _write_probe_talent(talent_dir: Path, load: dict | str) -> str:
diff --git a/tests/test_describe_preflight.py b/tests/test_describe_preflight.py
index cc5a1e44b..250b82a38 100644
--- a/tests/test_describe_preflight.py
+++ b/tests/test_describe_preflight.py
@@ -137,17 +137,13 @@ def test_blocked_then_ready_leaves_media_reeligible(tmp_path, monkeypatch):
]
-def test_preflight_dedups_contexts_by_resolved_provider_model(monkeypatch):
+def test_preflight_dedups_contexts_by_active_generate_route(monkeypatch):
from solstone.think import models
monkeypatch.setattr(
models,
"resolve_provider",
- lambda context, _interface: {
- "observe.describe.frame": ("google", "gemini-test"),
- "observe.describe.meeting": ("google", "gemini-test"),
- "observe.describe.terminal": ("anthropic", "claude-test"),
- }[context],
+ lambda _interface: ("google", "gemini-test"),
)
assert describe_module._dedup_readiness_contexts(
@@ -156,7 +152,7 @@ def test_preflight_dedups_contexts_by_resolved_provider_model(monkeypatch):
"observe.describe.meeting",
"observe.describe.terminal",
]
- ) == ["observe.describe.frame", "observe.describe.terminal"]
+ ) == ["observe.describe.frame"]
@pytest.mark.asyncio
@@ -234,7 +230,7 @@ async def test_mid_run_blocker_unlinks_partial_output_and_does_not_retry(
monkeypatch.setattr(
models,
"resolve_provider",
- lambda _context, _interface: ("google", "gemini-test"),
+ lambda _interface: ("google", "gemini-test"),
)
monkeypatch.setattr(
describe_module,
diff --git a/tests/test_describe_promote.py b/tests/test_describe_promote.py
index a0ddd1c35..a5128166a 100644
--- a/tests/test_describe_promote.py
+++ b/tests/test_describe_promote.py
@@ -96,7 +96,7 @@ def _install_fakes(monkeypatch, outcomes: dict[int, dict]) -> list[tuple]:
monkeypatch.setattr(
models,
"resolve_provider",
- lambda _context, _interface: ("google", "gemini-test"),
+ lambda _interface: ("google", "gemini-test"),
)
monkeypatch.setattr(
processing_record_module, "now_iso_utc", lambda: "2026-06-30T12:00:00Z"
@@ -339,12 +339,12 @@ async def test_browsing_truncation_does_not_promote_category_content(
monkeypatch.setattr(
batch_module,
"resolve_provider",
- lambda _context, _interface: ("google", "gemini-test"),
+ lambda _interface: ("google", "gemini-test"),
)
monkeypatch.setattr(
models,
"resolve_provider",
- lambda _context, _interface: ("google", "gemini-test"),
+ lambda _interface: ("google", "gemini-test"),
)
monkeypatch.setattr(
processing_record_module, "now_iso_utc", lambda: "2026-06-30T12:00:00Z"
diff --git a/tests/test_entity_observer_context.py b/tests/test_entity_observer_context.py
index 70f669d44..a4ea087c1 100644
--- a/tests/test_entity_observer_context.py
+++ b/tests/test_entity_observer_context.py
@@ -1048,7 +1048,7 @@ def test_entity_observer_agent_config(monkeypatch):
assert config["type"] == "generate"
assert config.get("output") == "json"
- assert config.get("tier") == 2
+ assert "tier" not in config
assert config.get("thinking_budget") == 2048
assert config.get("hook", {}).get("pre") == "entities:entity_observer"
assert config.get("hook", {}).get("post") == "entities:entity_observer"
diff --git a/tests/test_local.py b/tests/test_local.py
index 5452e6904..87e734623 100644
--- a/tests/test_local.py
+++ b/tests/test_local.py
@@ -17,11 +17,8 @@ from types import SimpleNamespace
import pytest
from solstone.think.models import (
+ DEFAULT_MODEL_BY_PROVIDER,
LOCAL_MODEL,
- PROVIDER_DEFAULTS,
- TIER_FLASH,
- TIER_LITE,
- TIER_PRO,
get_model_provider,
)
from solstone.think.talents import TalentHookError
@@ -122,9 +119,7 @@ def test_local_model_specs():
def test_local_provider_defaults_and_registry():
from solstone.think.providers import PROVIDER_METADATA, PROVIDER_REGISTRY
- assert PROVIDER_DEFAULTS["local"][TIER_PRO] == LOCAL_MODEL
- assert PROVIDER_DEFAULTS["local"][TIER_FLASH] == LOCAL_MODEL
- assert PROVIDER_DEFAULTS["local"][TIER_LITE] == LOCAL_MODEL
+ assert DEFAULT_MODEL_BY_PROVIDER["local"] == LOCAL_MODEL
assert PROVIDER_REGISTRY["local"] == "solstone.think.providers.local"
assert PROVIDER_METADATA["local"] == {
"label": "Local (on-device)",
diff --git a/tests/test_maint_006_rename_unified_triage_providers.py b/tests/test_maint_006_rename_unified_triage_providers.py
index cd93e14e4..b1c8773c4 100644
--- a/tests/test_maint_006_rename_unified_triage_providers.py
+++ b/tests/test_maint_006_rename_unified_triage_providers.py
@@ -18,7 +18,9 @@ def _write_journal_config(journal: Path, data: object) -> Path:
return config_path
-def test_rename_unified_and_remove_triage_idempotent(tmp_path, monkeypatch):
+def test_rename_unified_and_triage_provider_contexts_is_retired_noop(
+ tmp_path, monkeypatch
+):
monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
config_path = _write_journal_config(
tmp_path,
@@ -32,82 +34,29 @@ def test_rename_unified_and_remove_triage_idempotent(tmp_path, monkeypatch):
}
},
)
-
- summary = mod.run_migration(tmp_path, dry_run=False)
-
- assert summary.renamed == 1
- assert summary.removed == 1
- assert summary.preserved == 0
- assert summary.errors == 0
- data = json.loads(config_path.read_text(encoding="utf-8"))
- assert "talent.system.unified" not in data["providers"]["contexts"]
- assert "talent.system.triage" not in data["providers"]["contexts"]
- assert data["providers"]["contexts"]["talent.system.chat"] == {"provider": "openai"}
- assert data["providers"]["contexts"]["talent.system.morning_briefing"] == {
- "provider": "google"
- }
-
before_bytes = config_path.read_bytes()
before_mtime_ns = config_path.stat().st_mtime_ns
- rerun = mod.run_migration(tmp_path, dry_run=False)
-
- assert rerun.renamed == 0
- assert rerun.removed == 0
- assert rerun.preserved == 0
- assert rerun.errors == 0
- assert rerun.skipped_reason is None
- assert config_path.read_bytes() == before_bytes
- assert config_path.stat().st_mtime_ns == before_mtime_ns
-
-
-def test_preserves_existing_chat_context_when_unified_exists(tmp_path, monkeypatch):
- monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
- config_path = _write_journal_config(
- tmp_path,
- {
- "providers": {
- "contexts": {
- "talent.system.unified": {"provider": "openai"},
- "talent.system.chat": {"provider": "google"},
- }
- }
- },
- )
-
summary = mod.run_migration(tmp_path, dry_run=False)
assert summary.renamed == 0
assert summary.removed == 0
- assert summary.preserved == 1
+ assert summary.preserved == 0
assert summary.errors == 0
- data = json.loads(config_path.read_text(encoding="utf-8"))
- assert "talent.system.unified" not in data["providers"]["contexts"]
- assert data["providers"]["contexts"]["talent.system.chat"] == {"provider": "google"}
+ assert summary.skipped_reason == "retired"
+ assert config_path.read_bytes() == before_bytes
+ assert config_path.stat().st_mtime_ns == before_mtime_ns
-def test_noop_when_no_legacy_provider_contexts_present(tmp_path, monkeypatch):
+def test_retired_migration_dry_run_is_also_noop(tmp_path, monkeypatch):
monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
config_path = _write_journal_config(
tmp_path,
- {
- "providers": {
- "contexts": {
- "talent.system.chat": {"provider": "openai"},
- "talent.system.morning_briefing": {"provider": "google"},
- }
- }
- },
+ {"providers": {"contexts": {"talent.system.unified": {"provider": "openai"}}}},
)
- before_bytes = config_path.read_bytes()
- before_mtime_ns = config_path.stat().st_mtime_ns
+ before = config_path.read_bytes()
- summary = mod.run_migration(tmp_path, dry_run=False)
+ summary = mod.run_migration(tmp_path, dry_run=True)
- assert summary.renamed == 0
- assert summary.removed == 0
- assert summary.preserved == 0
- assert summary.errors == 0
- assert summary.skipped_reason is None
- assert config_path.read_bytes() == before_bytes
- assert config_path.stat().st_mtime_ns == before_mtime_ns
+ assert summary.skipped_reason == "retired"
+ assert config_path.read_bytes() == before
diff --git a/tests/test_models.py b/tests/test_models.py
index d8da8160b..1f54e7f3c 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -14,26 +14,17 @@ import pytest
import solstone.think.models as models_module
from solstone.think.models import (
- CLAUDE_HAIKU_4,
CLAUDE_OPUS_4,
CLAUDE_SONNET_4,
+ DEFAULT_MODEL_BY_PROVIDER,
DEFAULT_PROVIDER_TIMEOUT_S,
GEMINI_FLASH,
- GEMINI_LITE,
- GEMINI_PRO,
GEMMA4_26B_A4B_4BIT,
- GPT_5,
GPT_5_MINI,
- GPT_5_NANO,
LOCAL_MODEL,
NO_BRAIN_PROVIDER,
PROMPT_PATHS,
- PROVIDER_DEFAULTS,
QWEN_35_9B,
- TIER_FLASH,
- TIER_LITE,
- TIER_PRO,
- TYPE_DEFAULTS,
IncompleteJSONError,
IncompleteTextError,
NoBrainConfiguredError,
@@ -49,6 +40,7 @@ from solstone.think.models import (
agenerate_with_result,
calc_agent_cost,
calc_token_cost,
+ default_model_for_provider,
finish_reason_error,
generate,
generate_with_result,
@@ -58,7 +50,6 @@ from solstone.think.models import (
is_local_provider_needed,
iter_token_log,
model_supports,
- request_health_recheck,
resolve_provider,
)
@@ -246,54 +237,71 @@ def _write_tmp_journal_config(
def test_resolve_provider_default_generate(use_fixtures_journal):
- """Test that generate default provider is returned for unknown context."""
- provider, model = resolve_provider("unknown.context", "generate")
+ """Generate resolves the configured active provider/model."""
+ provider, model = resolve_provider("generate")
assert provider == "google"
- # Default tier is 2, which is overridden in fixture config to custom model
assert model == "gemini-custom-flash-test"
def test_resolve_provider_default_cogitate(use_fixtures_journal):
- """Test that cogitate default provider is returned for unknown context."""
- provider, model = resolve_provider("unknown.context", "cogitate")
+ """Cogitate resolves the configured active provider/model."""
+ provider, model = resolve_provider("cogitate")
assert provider == "openai"
assert model == GPT_5_MINI
-def test_resolve_provider_exact_match(use_fixtures_journal):
- """Test that exact context match works."""
- provider, model = resolve_provider("test.openai", "generate")
- assert provider == "openai"
- assert model == "gpt-5-mini"
-
+def test_resolve_provider_contexts_are_inert(monkeypatch, tmp_path):
+ """Legacy exact/glob contexts cannot influence active route resolution."""
+ _write_tmp_journal_config(
+ tmp_path,
+ monkeypatch,
+ {
+ "providers": {
+ "generate": {
+ "provider": "google",
+ "model": "gemini-flash-latest",
+ },
+ "contexts": {
+ "test.openai": {
+ "provider": "openai",
+ "model": "gpt-5-mini",
+ },
+ "observe.*": {
+ "provider": "anthropic",
+ "model": "claude-haiku-4-5",
+ },
+ },
+ }
+ },
+ )
-def test_resolve_provider_glob_match(use_fixtures_journal):
- """Test that glob pattern matching works."""
- # observe.* pattern should match
- provider, model = resolve_provider("observe.describe.frame", "generate")
+ provider, model = resolve_provider("generate")
assert provider == "google"
- assert model == GEMINI_LITE
+ assert model == GEMINI_FLASH
- # Also matches with other suffixes
- provider, model = resolve_provider("observe.enrich", "generate")
- assert provider == "google"
- assert model == GEMINI_LITE
+def test_resolve_provider_ordering_witness(monkeypatch, tmp_path):
+ """Explicit provider/model wins over key-presence fallback."""
+ _write_tmp_journal_config(
+ tmp_path,
+ monkeypatch,
+ {
+ "providers": {
+ "generate": {
+ "provider": "anthropic",
+ "model": "claude-haiku-4-5",
+ }
+ }
+ },
+ )
+ monkeypatch.setenv("OPENAI_API_KEY", "test-openai-key")
-def test_resolve_provider_anthropic(use_fixtures_journal):
- """Test anthropic provider routing."""
- provider, model = resolve_provider("test.anthropic", "generate")
- assert provider == "anthropic"
- # Explicit per-context model override in the fixture config wins over
- # the PROVIDER_DEFAULTS tier constant — this asserts override behavior,
- # not the default Sonnet pin, so it stays literal by design.
- assert model == "claude-sonnet-4-5"
+ assert resolve_provider("generate") == ("anthropic", "claude-haiku-4-5")
def test_resolve_provider_empty_context(use_fixtures_journal):
- """Test that empty context returns default."""
- provider, model = resolve_provider("", "generate")
- assert provider == "google"
+ """The resolver takes only an interface, not a context."""
+ assert resolve_provider("generate") == ("google", "gemini-custom-flash-test")
def test_resolve_provider_no_config(monkeypatch, tmp_path):
@@ -309,44 +317,17 @@ def test_resolve_provider_no_config(monkeypatch, tmp_path):
"solstone.think.providers.state.local_runtime_ready", lambda: False
)
- provider, model = resolve_provider("anything", "generate")
+ provider, model = resolve_provider("generate")
assert provider == NO_BRAIN_PROVIDER
assert provider != "google"
assert model == ""
- provider, model = resolve_provider("anything", "cogitate")
+ provider, model = resolve_provider("cogitate")
assert provider == NO_BRAIN_PROVIDER
assert provider != "google"
assert model == ""
-# ---------------------------------------------------------------------------
-# Tier system tests
-# ---------------------------------------------------------------------------
-
-
-def test_tier_constants():
- """Test tier constant values."""
- assert TIER_PRO == 1
- assert TIER_FLASH == 2
- assert TIER_LITE == 3
-
-
-def test_type_defaults():
- """Test TYPE_DEFAULTS structure for generate and cogitate."""
- assert "generate" in TYPE_DEFAULTS
- assert "cogitate" in TYPE_DEFAULTS
-
- for agent_type in ("generate", "cogitate"):
- defaults = TYPE_DEFAULTS[agent_type]
- assert "provider" not in defaults
- assert "tier" in defaults
- assert "backup" in defaults
-
- assert TYPE_DEFAULTS["generate"]["backup"] == "anthropic"
- assert TYPE_DEFAULTS["cogitate"]["backup"] == "anthropic"
-
-
def test_prompt_paths_exist():
"""Test all PROMPT_PATHS files exist and have valid frontmatter."""
from pathlib import Path
@@ -354,7 +335,7 @@ def test_prompt_paths_exist():
import frontmatter
base_dir = Path(__file__).parent.parent / "solstone" # Package root
- required_keys = {"context", "tier", "label", "group"}
+ required_keys = {"context", "label", "group"}
for rel_path in PROMPT_PATHS:
path = base_dir / rel_path
@@ -366,11 +347,6 @@ def test_prompt_paths_exist():
assert required_keys <= set(meta.keys()), (
f"{rel_path} missing keys: {required_keys - set(meta.keys())}"
)
- assert meta["tier"] in (
- TIER_PRO,
- TIER_FLASH,
- TIER_LITE,
- ), f"{rel_path} has invalid tier: {meta['tier']}"
assert isinstance(meta["label"], str) and meta["label"], (
f"{rel_path} has invalid label: {meta['label']}"
)
@@ -385,45 +361,22 @@ def test_prompt_contexts_in_registry():
# Verify known prompt contexts exist with correct values
assert "observe.describe.frame" in registry
- assert registry["observe.describe.frame"]["tier"] == TIER_LITE
assert registry["observe.describe.frame"]["group"] == "Observe"
assert "observe.enrich" in registry
- assert registry["observe.enrich"]["tier"] == TIER_FLASH
assert "detect.created" in registry
- assert registry["detect.created"]["tier"] == TIER_LITE
-
-
-def test_provider_defaults_structure():
- """Test PROVIDER_DEFAULTS contains all providers and tiers."""
- assert "google" in PROVIDER_DEFAULTS
- assert "openai" in PROVIDER_DEFAULTS
- assert "anthropic" in PROVIDER_DEFAULTS
-
- for provider in PROVIDER_DEFAULTS:
- assert TIER_PRO in PROVIDER_DEFAULTS[provider]
- assert TIER_FLASH in PROVIDER_DEFAULTS[provider]
- assert TIER_LITE in PROVIDER_DEFAULTS[provider]
-
-def test_provider_defaults_models():
- """Test PROVIDER_DEFAULTS maps to correct model constants."""
- assert PROVIDER_DEFAULTS["google"][TIER_PRO] == GEMINI_PRO
- assert PROVIDER_DEFAULTS["google"][TIER_FLASH] == GEMINI_FLASH
- assert PROVIDER_DEFAULTS["google"][TIER_LITE] == GEMINI_LITE
- assert PROVIDER_DEFAULTS["openai"][TIER_PRO] == GPT_5
- assert PROVIDER_DEFAULTS["openai"][TIER_FLASH] == GPT_5_MINI
- assert PROVIDER_DEFAULTS["openai"][TIER_LITE] == GPT_5_NANO
-
- assert PROVIDER_DEFAULTS["anthropic"][TIER_PRO] == CLAUDE_OPUS_4
- assert PROVIDER_DEFAULTS["anthropic"][TIER_FLASH] == CLAUDE_SONNET_4
- assert PROVIDER_DEFAULTS["anthropic"][TIER_LITE] == CLAUDE_HAIKU_4
-
- assert PROVIDER_DEFAULTS["local"][TIER_PRO] == LOCAL_MODEL
- assert PROVIDER_DEFAULTS["local"][TIER_FLASH] == LOCAL_MODEL
- assert PROVIDER_DEFAULTS["local"][TIER_LITE] == LOCAL_MODEL
+def test_default_model_by_provider():
+ assert DEFAULT_MODEL_BY_PROVIDER == {
+ "google": GEMINI_FLASH,
+ "openai": GPT_5_MINI,
+ "anthropic": CLAUDE_SONNET_4,
+ "local": LOCAL_MODEL,
+ }
+ for provider, model in DEFAULT_MODEL_BY_PROVIDER.items():
+ assert default_model_for_provider(provider) == model
@pytest.mark.parametrize(
@@ -431,7 +384,6 @@ def test_provider_defaults_models():
[
{"providers": {"generate": {"provider": "local"}}},
{"providers": {"cogitate": {"provider": "local"}}},
- {"providers": {"contexts": {"talent.*": {"provider": "local"}}}},
],
)
def test_is_local_provider_needed_true_for_selected_surfaces(config):
@@ -444,6 +396,7 @@ def test_is_local_provider_needed_true_for_selected_surfaces(config):
{},
{"providers": {"generate": {"provider": "google"}}},
{"providers": {"contexts": {"talent.*": {"provider": "anthropic"}}}},
+ {"providers": {"contexts": {"talent.*": {"provider": "local"}}}},
{"providers": []},
],
)
@@ -465,238 +418,93 @@ def test_is_local_provider_needed_true_for_implicit_local(monkeypatch):
assert is_local_provider_needed({}) is True
-def test_resolve_provider_tier_based(use_fixtures_journal):
- """Test tier-based resolution."""
- # test.tier has tier: 1 (pro)
- provider, model = resolve_provider("test.tier", "generate")
- assert provider == "google"
- assert model == GEMINI_PRO
-
-
-def test_resolve_provider_tier_inherit_provider(use_fixtures_journal):
- """Test tier with inherited provider from type default."""
- # test.tier.inherit has tier: 3 only, should inherit google from generate default
- provider, model = resolve_provider("test.tier.inherit", "generate")
- assert provider == "google"
- assert model == GEMINI_LITE
-
- # Same context with cogitate should inherit openai
- provider, model = resolve_provider("test.tier.inherit", "cogitate")
- assert provider == "openai"
- assert model == GPT_5_NANO
-
-
-def test_resolve_provider_tier_with_provider(use_fixtures_journal):
- """Test tier with explicit provider."""
- # test.tier.override has provider: openai, tier: 2
- provider, model = resolve_provider("test.tier.override", "generate")
- assert provider == "openai"
- assert model == GPT_5_MINI
-
-
-def test_resolve_provider_tier_glob(use_fixtures_journal):
- """Test tier-based glob pattern matching."""
- # observe.* now uses tier: 3 instead of explicit model
- provider, model = resolve_provider("observe.describe.frame", "generate")
- assert provider == "google"
- assert model == GEMINI_LITE
-
-
-def test_resolve_provider_model_overrides_tier(use_fixtures_journal):
- """Test that explicit model takes precedence over tier."""
- # test.openai has explicit model, not tier
- provider, model = resolve_provider("test.openai", "generate")
- assert provider == "openai"
- assert model == "gpt-5-mini"
-
-
-def test_resolve_provider_default_tier(use_fixtures_journal):
- """Test default uses tier-based resolution with config override."""
- # Generate default is tier: 2, which is overridden in config to custom model
- provider, model = resolve_provider("unknown.context", "generate")
- assert provider == "google"
- assert model == "gemini-custom-flash-test"
-
-
-def test_resolve_provider_config_model_override(use_fixtures_journal):
- """Test that config models section overrides system defaults."""
- # test.config.override uses tier: 2, which is overridden in config
- provider, model = resolve_provider("test.config.override", "generate")
- assert provider == "google"
- # Should use the custom model from config, not system default GEMINI_FLASH
- assert model == "gemini-custom-flash-test"
- assert model != GEMINI_FLASH
-
-
-def test_resolve_provider_tier_fallback_to_system_default(use_fixtures_journal):
- """Test that tiers not in config fall back to system defaults."""
- # test.tier uses tier: 1 (pro), which is NOT overridden in config
- # Should fall back to system default GEMINI_PRO
- provider, model = resolve_provider("test.tier", "generate")
- assert provider == "google"
- assert model == GEMINI_PRO
-
-
-def test_resolve_provider_invalid_tier(use_fixtures_journal, monkeypatch, tmp_path):
- """Test that invalid tier values fall back to default tier."""
- # Create a config with an invalid tier
- config_dir = tmp_path / "config"
- config_dir.mkdir()
- config = {
- "providers": {
- "generate": {"provider": "google", "tier": 2},
- "contexts": {
- "test.invalid": {"provider": "google", "tier": 99},
- "test.string": {"provider": "google", "tier": "flash"},
- },
- }
- }
- (config_dir / "journal.json").write_text(json.dumps(config))
- monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
-
- # Invalid tier 99 should fall back to generate default tier (2)
- provider, model = resolve_provider("test.invalid", "generate")
- assert provider == "google"
- assert model == GEMINI_FLASH # tier 2 system default
-
- # String tier should also fall back
- provider, model = resolve_provider("test.string", "generate")
- assert provider == "google"
- assert model == GEMINI_FLASH
-
-
-def test_resolve_provider_local_type_default_neutralizes_cloud_context_pin(
+def test_resolve_provider_legacy_keys_are_inert(
use_fixtures_journal, monkeypatch, tmp_path
):
- """A cloud context provider/model pin cannot override an explicit local lane."""
+ """Tier/backup/contexts/models legacy keys do not affect active routing."""
_write_tmp_journal_config(
tmp_path,
monkeypatch,
{
"providers": {
- "generate": {"provider": "local"},
- "contexts": {
- "talent.timeline.segment_summary": {
- "provider": "google",
- "model": "gemini-flash-lite-latest",
- }
+ "generate": {
+ "provider": "google",
+ "model": "gemini-flash-latest",
+ "tier": 1,
+ "backup": "anthropic",
},
- }
- },
- )
-
- provider, model = resolve_provider("talent.timeline.segment_summary", "generate")
-
- assert provider == "local"
- assert model == LOCAL_MODEL
- assert model != "gemini-flash-lite-latest"
-
-
-def test_resolve_provider_local_context_model_pin_is_honored(
- use_fixtures_journal, monkeypatch, tmp_path
-):
- """An explicit local context pin (provider: local + model) keeps its model."""
- _write_tmp_journal_config(
- tmp_path,
- monkeypatch,
- {
- "providers": {
- "generate": {"provider": "local"},
"contexts": {
- "talent.timeline.segment_summary": {
- "provider": "local",
- "model": "local/custom-7b",
- }
+ "talent.timeline.segment_summary": {"provider": "local"},
+ "observe.*": {"provider": "anthropic", "tier": 3},
+ },
+ "models": {
+ "google": {"1": "gemini-pro-latest"},
+ "anthropic": {"3": "claude-haiku-4-5"},
},
}
},
)
- provider, model = resolve_provider("talent.timeline.segment_summary", "generate")
+ assert resolve_provider("generate") == ("google", GEMINI_FLASH)
- assert provider == "local"
- assert model == "local/custom-7b"
- assert model != LOCAL_MODEL
-
-def test_resolve_provider_local_context_blank_pin_uses_context_tier(
+def test_resolve_provider_model_key_wins_even_when_tier_present(
use_fixtures_journal, monkeypatch, tmp_path
):
- """Blank local context model pins fall through to tier-based local routing."""
+ """The retired AC3a quirk is gone: model is honored even with tier present."""
_write_tmp_journal_config(
tmp_path,
monkeypatch,
{
"providers": {
- "generate": {"provider": "local"},
- "models": {"local": {"3": "local/lite-test"}},
- "contexts": {
- "talent.blank": {
- "provider": "local",
- "model": "",
- "tier": 3,
- }
- },
+ "generate": {
+ "provider": "google",
+ "tier": 1,
+ "model": "gemini-custom-flash-test",
+ }
}
},
)
- provider, model = resolve_provider("talent.blank", "generate")
-
- assert provider == "local"
- assert model == "local/lite-test"
- assert model != ""
+ assert resolve_provider("generate") == ("google", "gemini-custom-flash-test")
-def test_resolve_provider_local_context_malformed_pin_uses_type_default_tier(
+def test_resolve_provider_local_type_default_ignores_context_pins(
use_fixtures_journal, monkeypatch, tmp_path
):
- """Malformed local model pins fall through without crashing."""
+ """Context pins cannot push a local active interface onto cloud."""
_write_tmp_journal_config(
tmp_path,
monkeypatch,
{
"providers": {
- "generate": {"provider": "local", "tier": 2},
- "models": {"local": {"2": "local/default-test"}},
+ "generate": {"provider": "local"},
"contexts": {
- "talent.blank.no_tier": {
- "provider": "local",
- "model": "",
- },
- "talent.numeric.invalid_tier": {
- "provider": "local",
- "model": 123,
- "tier": 99,
+ "talent.timeline.segment_summary": {
+ "provider": "google",
+ "model": "gemini-flash-lite-latest",
},
},
}
},
)
- assert resolve_provider("talent.blank.no_tier", "generate") == (
- "local",
- "local/default-test",
- )
- assert resolve_provider("talent.numeric.invalid_tier", "generate") == (
- "local",
- "local/default-test",
- )
+ assert resolve_provider("generate") == ("local", LOCAL_MODEL)
-def test_resolve_provider_local_honors_context_tier_and_models_override(
+def test_legacy_context_toggles_remain_on_disk_but_not_routing(
use_fixtures_journal, monkeypatch, tmp_path
):
- """Tier-only context config inherits the local type default."""
+ """Grandfathered context keys stay inert next to disabled/extract toggles."""
_write_tmp_journal_config(
tmp_path,
monkeypatch,
{
"providers": {
- "generate": {"provider": "local"},
- "models": {"local": {"3": "local/lite-test"}},
+ "generate": {"provider": "google", "model": GEMINI_FLASH},
"contexts": {
"talent.x": {
+ "provider": "local",
"tier": 3,
"disabled": True,
"extract": "foo",
@@ -706,15 +514,66 @@ def test_resolve_provider_local_honors_context_tier_and_models_override(
},
)
- assert resolve_provider("talent.x", "generate") == ("local", "local/lite-test")
+ assert resolve_provider("generate") == ("google", GEMINI_FLASH)
stored = json.loads((tmp_path / "config" / "journal.json").read_text())
context = stored["providers"]["contexts"]["talent.x"]
+ assert context["provider"] == "local"
assert context["disabled"] is True
assert context["extract"] == "foo"
assert context["tier"] == 3
+def test_prepare_config_legacy_context_routing_keys_are_inert(
+ journal_copy: Path,
+) -> None:
+ """Legacy contexts survive on disk but cannot change prepared identity."""
+ from solstone.think.talents import prepare_config
+
+ config_path = journal_copy / "config" / "journal.json"
+ config = json.loads(config_path.read_text(encoding="utf-8"))
+ config["providers"] = {
+ "generate": {"provider": "anthropic", "model": CLAUDE_SONNET_4},
+ "contexts": {
+ "talent.timeline.segment_summary": {
+ "provider": "google",
+ "model": "gemini-flash-lite-latest",
+ "tier": 3,
+ "backup": "openai",
+ }
+ },
+ }
+ config_path.write_text(json.dumps(config), encoding="utf-8")
+
+ prepared = prepare_config({"name": "timeline:segment_summary"})
+
+ assert prepared["provider"] == "anthropic"
+ assert prepared["model"] == CLAUDE_SONNET_4
+
+
+def test_prepare_config_frontmatter_provider_pin_is_dead_through_dispatch_identity(
+ journal_copy: Path,
+) -> None:
+ """Removed google frontmatter pins do not override the active brain."""
+ from solstone.think.talents import prepare_config
+
+ config_path = journal_copy / "config" / "journal.json"
+ config = json.loads(config_path.read_text(encoding="utf-8"))
+ config["env"] = {"GOOGLE_API_KEY": "test-google-key"}
+ config["providers"] = {
+ "generate": {"provider": "anthropic", "model": CLAUDE_SONNET_4}
+ }
+ config_path.write_text(json.dumps(config), encoding="utf-8")
+
+ segment = prepare_config({"name": "timeline:segment_summary"})
+ detection = prepare_config({"name": "entities:detection"})
+
+ assert segment["provider"] == "anthropic"
+ assert segment["model"] == CLAUDE_SONNET_4
+ assert detection["provider"] == "anthropic"
+ assert detection["model"] == CLAUDE_SONNET_4
+
+
def test_resolve_provider_cogitate_system_talents_stay_local(
use_fixtures_journal, monkeypatch, tmp_path
):
@@ -725,11 +584,7 @@ def test_resolve_provider_cogitate_system_talents_stay_local(
{"providers": {"cogitate": {"provider": "local"}}},
)
- weekly_provider, _ = resolve_provider("talent.system.weekly_reflection", "cogitate")
- partner_provider, _ = resolve_provider("talent.system.partner", "cogitate")
-
- assert weekly_provider == "local"
- assert partner_provider == "local"
+ assert resolve_provider("cogitate") == ("local", LOCAL_MODEL)
def test_resolve_provider_split_lane_other_type_stays_cloud(
@@ -742,24 +597,13 @@ def test_resolve_provider_split_lane_other_type_stays_cloud(
{
"providers": {
"generate": {"provider": "local"},
- "cogitate": {"provider": "openai", "tier": 2},
- "contexts": {
- "talent.timeline.segment_summary": {
- "provider": "google",
- "model": "gemini-flash-lite-latest",
- }
- },
+ "cogitate": {"provider": "openai"},
}
},
)
- generate_provider, _ = resolve_provider(
- "talent.timeline.segment_summary", "generate"
- )
- cogitate_provider, _ = resolve_provider("talent.system.partner", "cogitate")
-
- assert generate_provider == "local"
- assert cogitate_provider == "openai"
+ assert resolve_provider("generate") == ("local", LOCAL_MODEL)
+ assert resolve_provider("cogitate") == ("openai", GPT_5_MINI)
def test_generate_rejects_cloud_model_override_for_local_provider():
@@ -900,7 +744,7 @@ def test_context_registry_includes_prompt_contexts():
registry = get_context_registry()
base_dir = Path(__file__).parent.parent / "solstone"
- # All prompt contexts should be in registry with correct tier
+ # All prompt contexts should be in registry with matching metadata
for rel_path in PROMPT_PATHS:
path = base_dir / rel_path
post = frontmatter.load(path)
@@ -908,7 +752,8 @@ def test_context_registry_includes_prompt_contexts():
context = meta.get("context")
assert context in registry, f"Prompt context {context} not in registry"
- assert registry[context]["tier"] == meta["tier"]
+ assert registry[context]["label"] == meta["label"]
+ assert registry[context]["group"] == meta["group"]
def test_context_registry_includes_categories():
@@ -923,10 +768,8 @@ def test_context_registry_includes_categories():
# Each category context should have required fields
for context in category_contexts:
- assert "tier" in registry[context]
assert "label" in registry[context]
assert "group" in registry[context]
- assert registry[context]["tier"] in (TIER_PRO, TIER_FLASH, TIER_LITE)
def test_context_registry_includes_talent_configs():
@@ -959,18 +802,13 @@ def test_context_registry_includes_talent_configs():
def test_context_registry_structure():
"""Test that all registry entries have required fields."""
registry = get_context_registry()
- required_keys = {"tier", "label", "group"}
+ required_keys = {"label", "group"}
for context, config in registry.items():
assert isinstance(config, dict), f"{context} should be a dict"
assert required_keys <= set(config.keys()), (
f"{context} missing keys: {required_keys - set(config.keys())}"
)
- assert config["tier"] in (
- TIER_PRO,
- TIER_FLASH,
- TIER_LITE,
- ), f"{context} has invalid tier: {config['tier']}"
def test_context_registry_is_cached():
@@ -988,7 +826,7 @@ def test_context_registry_is_cached():
def test_all_default_models_have_pricing():
- """Verify all models in PROVIDER_DEFAULTS have genai-prices support.
+ """Verify all default provider models have genai-prices support.
This test ensures that when default models are updated, we catch any
missing pricing data early. If this test fails:
@@ -999,25 +837,8 @@ def test_all_default_models_have_pricing():
See think/models.py model constants section for more details.
"""
- # Collect all unique models from PROVIDER_DEFAULTS
- all_models = set()
- for provider_models in PROVIDER_DEFAULTS.values():
- all_models.update(provider_models.values())
-
- # Also include the named constants directly (in case they differ)
- all_models.update(
- [
- GEMINI_PRO,
- GEMINI_FLASH,
- GEMINI_LITE,
- GPT_5,
- GPT_5_MINI,
- GPT_5_NANO,
- CLAUDE_OPUS_4,
- CLAUDE_SONNET_4,
- CLAUDE_HAIKU_4,
- ]
- )
+ all_models = set(DEFAULT_MODEL_BY_PROVIDER.values())
+ all_models.add(CLAUDE_OPUS_4)
missing_pricing = []
for model in sorted(all_models):
@@ -1978,10 +1799,12 @@ class TestDefaultProviderTimeout:
def test_request_health_recheck_emits_callosum_request():
+ from solstone.think.providers.state import request_recheck
+
with patch(
"solstone.think.providers.state.callosum_send", return_value=True
) as send:
- request_health_recheck()
+ request_recheck()
send.assert_called_once_with(
"supervisor",
@@ -1991,13 +1814,15 @@ def test_request_health_recheck_emits_callosum_request():
def test_request_health_recheck_does_not_raise_on_send_failure(caplog):
+ from solstone.think.providers.state import request_recheck
+
with (
patch(
"solstone.think.providers.state.callosum_send", return_value=False
) as send,
caplog.at_level(logging.WARNING),
):
- request_health_recheck()
+ request_recheck()
send.assert_called_once()
assert "request_health_recheck: callosum_send returned false" in caplog.text
diff --git a/tests/test_models_health.py b/tests/test_models_health.py
index 3de1ebffc..3c476fda6 100644
--- a/tests/test_models_health.py
+++ b/tests/test_models_health.py
@@ -5,7 +5,8 @@ import json
import pytest
-from solstone.think.models import GEMINI_FLASH, record_provider_failure
+from solstone.think.models import GEMINI_FLASH
+from solstone.think.providers.state import record_quota_failure
def _read_health(tmp_path):
@@ -15,9 +16,8 @@ def _read_health(tmp_path):
def test_record_provider_failure_appends_new_row(monkeypatch, tmp_path):
monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
- record_provider_failure(
+ record_quota_failure(
"google",
- "flash",
GEMINI_FLASH,
"cogitate",
12345,
@@ -27,7 +27,7 @@ def test_record_provider_failure_appends_new_row(monkeypatch, tmp_path):
assert payload["summary"] == {"total": 1, "passed": 0, "skipped": 0, "failed": 1}
row = payload["results"][0]
assert row["provider"] == "google"
- assert row["tier"] == "flash"
+ assert "tier" not in row
assert row["model"] == GEMINI_FLASH
assert row["interface"] == "cogitate"
assert row["ok"] is False
@@ -42,8 +42,8 @@ def test_record_provider_failure_appends_new_row(monkeypatch, tmp_path):
def test_record_provider_failure_updates_duplicate_key(monkeypatch, tmp_path):
monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
- record_provider_failure("google", "flash", "gemini", "cogitate", 100)
- record_provider_failure("google", "flash", "gemini", "cogitate", 200)
+ record_quota_failure("google", "gemini", "cogitate", 100)
+ record_quota_failure("google", "gemini", "cogitate", 200)
payload = _read_health(tmp_path)
assert len(payload["results"]) == 1
@@ -73,7 +73,7 @@ def test_record_provider_failure_recomputes_summary(monkeypatch, tmp_path):
)
)
- record_provider_failure("google", "flash", "gemini", "cogitate", 300)
+ record_quota_failure("google", "gemini", "cogitate", 300)
payload = _read_health(tmp_path)
assert payload["summary"] == {"total": 3, "passed": 1, "skipped": 1, "failed": 1}
@@ -100,6 +100,6 @@ def test_record_provider_failure_atomic_replace_failure_preserves_file(
monkeypatch.setattr("solstone.think.providers.state.os.replace", fail_replace)
with pytest.raises(OSError, match="replace failed"):
- record_provider_failure("google", "flash", "gemini", "cogitate", 400)
+ record_quota_failure("google", "gemini", "cogitate", 400)
assert json.loads(health_path.read_text()) == original
diff --git a/tests/test_no_implicit_cloud.py b/tests/test_no_implicit_cloud.py
index 93cc0fcca..adaaf23e7 100644
--- a/tests/test_no_implicit_cloud.py
+++ b/tests/test_no_implicit_cloud.py
@@ -19,10 +19,7 @@ from solstone.think.models import (
LOCAL_MODEL,
NO_BRAIN_PROVIDER,
AttestationFailedError,
- AttestationNotVerifiedError,
- AttestationStaleError,
NoBrainConfiguredError,
- get_backup_provider,
is_local_provider_needed,
resolve_provider,
)
@@ -131,7 +128,7 @@ def test_unconfigured_journal_resolves_to_no_brain(tmp_path, monkeypatch):
_empty_journal(tmp_path, monkeypatch)
for agent_type in ("generate", "cogitate"):
- provider, model = resolve_provider("any.context", agent_type)
+ provider, model = resolve_provider(agent_type)
assert provider == NO_BRAIN_PROVIDER
assert provider != "google"
@@ -152,6 +149,17 @@ def test_unconfigured_execution_stops_before_cloud(tmp_path, monkeypatch):
assert not (tmp_path / "config" / "journal.json").exists()
+def test_no_brain_configured_error_is_not_retried(tmp_path, monkeypatch):
+ _empty_journal(tmp_path, monkeypatch)
+ mocks = _cloud_call_mocks(monkeypatch)
+
+ with pytest.raises(NoBrainConfiguredError):
+ talents.prepare_config({"name": "chat"})
+
+ for mock in mocks:
+ mock.assert_not_called()
+
+
def test_confidential_generate_stops_before_any_provider_dispatch(
tmp_path,
monkeypatch,
@@ -260,10 +268,6 @@ def test_confidential_attestation_error_is_non_retryable(tmp_path, monkeypatch):
establish = _install_failing_confidential_transport(monkeypatch)
mocks = _cloud_call_mocks(monkeypatch)
- assert talents._is_retryable_error(AttestationNotVerifiedError()) is False
- for exc in (AttestationFailedError("x"), AttestationStaleError("x")):
- assert talents._is_retryable_error(exc) is False
- assert talents._should_fallback(exc) is False
from solstone.think.services.spp_transport import verify_confidential_attestation
assert (
@@ -273,7 +277,7 @@ def test_confidential_attestation_error_is_non_retryable(tmp_path, monkeypatch):
with pytest.raises(AttestationFailedError) as exc_info:
asyncio.run(
talents._execute_with_tools(
- {"provider": "google", "backup": "anthropic"},
+ {"provider": "google", "type": "cogitate"},
lambda _event: None,
)
)
@@ -346,27 +350,30 @@ def test_confidential_stt_chokepoint_blocks_cloud_audio_egress(
parakeet_transcribe.assert_called_once()
-def test_none_provider_module_and_backup_fail_closed(tmp_path, monkeypatch):
+def test_none_provider_module_fails_closed(tmp_path, monkeypatch):
_empty_journal(tmp_path, monkeypatch)
with pytest.raises(NoBrainConfiguredError):
get_provider_module(NO_BRAIN_PROVIDER)
- assert get_backup_provider("generate") is None
assert not (tmp_path / "config" / "journal.json").exists()
@pytest.mark.parametrize(
- ("env_key", "expected_provider", "expected_model"),
+ ("agent_type", "env_key", "expected_provider", "expected_model"),
[
- ("GOOGLE_API_KEY", "google", GEMINI_FLASH),
- ("ANTHROPIC_API_KEY", "anthropic", CLAUDE_SONNET_4),
- ("OPENAI_API_KEY", "openai", GPT_5_MINI),
+ ("generate", "GOOGLE_API_KEY", "google", GEMINI_FLASH),
+ ("generate", "ANTHROPIC_API_KEY", "anthropic", CLAUDE_SONNET_4),
+ ("generate", "OPENAI_API_KEY", "openai", GPT_5_MINI),
+ ("cogitate", "GOOGLE_API_KEY", "google", GEMINI_FLASH),
+ ("cogitate", "ANTHROPIC_API_KEY", "anthropic", CLAUDE_SONNET_4),
+ ("cogitate", "OPENAI_API_KEY", "openai", GPT_5_MINI),
],
)
def test_key_presence_grandfathers_existing_installs(
tmp_path,
monkeypatch,
+ agent_type: str,
env_key: str,
expected_provider: str,
expected_model: str,
@@ -374,7 +381,7 @@ def test_key_presence_grandfathers_existing_installs(
_empty_journal(tmp_path, monkeypatch)
original = _write_journal_config(tmp_path, {"env": {env_key: "test-key"}})
- provider, model = resolve_provider("any.context", "generate")
+ provider, model = resolve_provider(agent_type)
assert provider == expected_provider
assert model == expected_model
@@ -383,13 +390,55 @@ def test_key_presence_grandfathers_existing_installs(
) == original
+def test_model_only_config_uses_key_selected_provider(tmp_path, monkeypatch):
+ _empty_journal(tmp_path, monkeypatch)
+ _write_journal_config(
+ tmp_path,
+ {
+ "env": {"GOOGLE_API_KEY": "test-key"},
+ "providers": {"generate": {"model": "gemini-custom"}},
+ },
+ )
+
+ assert resolve_provider("generate") == ("google", "gemini-custom")
+
+
+def test_explicit_provider_does_not_fall_through_to_keyed_provider(
+ tmp_path, monkeypatch
+):
+ _empty_journal(tmp_path, monkeypatch)
+ _write_journal_config(
+ tmp_path,
+ {
+ "env": {"GOOGLE_API_KEY": "test-key"},
+ "providers": {"generate": {"provider": "anthropic"}},
+ },
+ )
+ google = Mock(side_effect=AssertionError("google dispatched"))
+ monkeypatch.setattr("solstone.think.providers.google.run_generate", google)
+
+ assert resolve_provider("generate") == ("anthropic", CLAUDE_SONNET_4)
+ with pytest.raises(ValueError, match="ANTHROPIC_API_KEY not found"):
+ models.generate("hello", "any.context")
+ google.assert_not_called()
+
+
+def test_accepted_grandfather_divergence_lite_context_now_uses_brain_model(
+ tmp_path, monkeypatch
+):
+ _empty_journal(tmp_path, monkeypatch)
+ _write_journal_config(tmp_path, {"env": {"GOOGLE_API_KEY": "test-key"}})
+
+ assert resolve_provider("generate") == ("google", GEMINI_FLASH)
+
+
def test_implicit_local_when_runtime_ready(tmp_path, monkeypatch):
_empty_journal(tmp_path, monkeypatch)
monkeypatch.setattr(
"solstone.think.providers.state.local_runtime_ready", lambda: True
)
- provider, model = resolve_provider("any.context", "generate")
+ provider, model = resolve_provider("generate")
assert provider == "local"
assert model == LOCAL_MODEL
@@ -417,7 +466,7 @@ def test_explicit_local_type_default_neutralizes_cloud_context_pin(
},
)
- provider, model = resolve_provider("talent.timeline.segment_summary", "generate")
+ provider, model = resolve_provider("generate")
assert provider == "local"
assert provider != "google"
diff --git a/tests/test_participation_talent.py b/tests/test_participation_talent.py
index 87a2bff3e..b1603100f 100644
--- a/tests/test_participation_talent.py
+++ b/tests/test_participation_talent.py
@@ -15,7 +15,7 @@ def test_participation_talent_frontmatter_and_placeholders():
assert post.metadata["schedule"] == "activity"
assert post.metadata["activities"] == ["*"]
- assert post.metadata["tier"] == 3
+ assert "tier" not in post.metadata
assert post.metadata["output"] == "json"
assert post.metadata["schema"] == "participation.schema.json"
assert post.metadata["priority"] == 10
diff --git a/tests/test_provider_state.py b/tests/test_provider_state.py
index 5020dbdc4..c5074f2a5 100644
--- a/tests/test_provider_state.py
+++ b/tests/test_provider_state.py
@@ -897,7 +897,7 @@ def test_local_status_dict_darwin(monkeypatch):
def test_readiness_for_context_routes_to_resolved_local_provider(monkeypatch):
monkeypatch.setattr(
"solstone.think.models.resolve_provider",
- lambda _context, _interface: ("local", LOCAL_MODEL),
+ lambda _interface: ("local", LOCAL_MODEL),
)
monkeypatch.setattr(
local_install,
@@ -917,7 +917,7 @@ def test_readiness_for_context_routes_to_resolved_local_provider(monkeypatch):
def test_readiness_for_context_routes_to_resolved_cloud_provider(monkeypatch):
monkeypatch.setattr(
"solstone.think.models.resolve_provider",
- lambda _context, _interface: ("google", "gemini"),
+ lambda _interface: ("google", "gemini"),
)
monkeypatch.setattr(state, "cloud_key_configured", lambda _env_key: True)
monkeypatch.setattr(
@@ -947,7 +947,12 @@ def test_readiness_for_context_routes_to_resolved_cloud_provider(monkeypatch):
def test_record_quota_failure_writes_reason_code(monkeypatch, tmp_path):
monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
- state.record_quota_failure("google", "flash", "gemini", "cogitate", 12345)
+ state.record_quota_failure("google", "gemini", "cogitate", 12345)
payload = json.loads((tmp_path / "health" / "talents.json").read_text())
- assert payload["results"][0]["reason_code"] == "provider_quota_exceeded"
+ row = payload["results"][0]
+ assert row["provider"] == "google"
+ assert row["model"] == "gemini"
+ assert row["interface"] == "cogitate"
+ assert "tier" not in row
+ assert row["reason_code"] == "provider_quota_exceeded"
diff --git a/tests/test_providers_check.py b/tests/test_providers_check.py
index 287c228b2..8f2306206 100644
--- a/tests/test_providers_check.py
+++ b/tests/test_providers_check.py
@@ -3,6 +3,7 @@
import argparse
import asyncio
+import fcntl
import json
from datetime import datetime
from types import SimpleNamespace
@@ -19,21 +20,29 @@ def _patch_health_journal(monkeypatch, providers_cli, tmp_path):
)
+def _args(**overrides):
+ values = {
+ "provider": None,
+ "interface": None,
+ "model": None,
+ "json": False,
+ "timeout": 1,
+ "targeted": False,
+ }
+ values.update(overrides)
+ return argparse.Namespace(**values)
+
+
def test_run_check_writes_health_file(tmp_path, monkeypatch):
- """_run_check writes provider health results to SOLSTONE_JOURNAL/health/talents.json."""
import solstone.think.providers_cli as providers_cli
- fake_registry = {"fake": object()}
- fake_defaults = {
- "fake": {
- 1: "fake-pro-model",
- 2: "fake-flash-model",
- 3: "fake-lite-model",
- }
- }
-
- monkeypatch.setattr("solstone.think.providers.PROVIDER_REGISTRY", fake_registry)
- monkeypatch.setattr("solstone.think.models.PROVIDER_DEFAULTS", fake_defaults)
+ monkeypatch.setattr(
+ "solstone.think.providers.PROVIDER_REGISTRY", {"fake": object()}
+ )
+ monkeypatch.setattr(
+ "solstone.think.models.default_model_for_provider",
+ lambda provider: f"{provider}-model",
+ )
_patch_health_journal(monkeypatch, providers_cli, tmp_path)
monkeypatch.setattr(
providers_cli,
@@ -46,47 +55,27 @@ def test_run_check_writes_health_file(tmp_path, monkeypatch):
monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate)
- args = argparse.Namespace(
- provider=None,
- interface=None,
- tier=None,
- json=False,
- timeout=1,
- targeted=False,
- )
-
with pytest.raises(SystemExit) as exc_info:
- asyncio.run(providers_cli._run_check(args))
+ asyncio.run(providers_cli._run_check(_args()))
assert exc_info.value.code == 0
-
- health_file = tmp_path / "health" / "talents.json"
- assert health_file.exists()
-
- payload = json.loads(health_file.read_text())
- assert "results" in payload
- assert "summary" in payload
- assert "checked_at" in payload
+ payload = json.loads((tmp_path / "health" / "talents.json").read_text())
assert datetime.fromisoformat(payload["checked_at"]).tzinfo is not None
- assert payload["summary"]["passed"] > 0
- assert payload["summary"]["skipped"] == 0
+ assert payload["summary"] == {"total": 2, "passed": 2, "skipped": 0, "failed": 0}
+ assert all("tier" not in row for row in payload["results"])
+ assert all("reused_from" not in row for row in payload["results"])
def test_run_check_partial_failure_exits_one(tmp_path, monkeypatch):
- """_run_check exits 1 when any check fails."""
import solstone.think.providers_cli as providers_cli
- fake_registry = {"fake": object()}
- fake_defaults = {
- "fake": {
- 1: "fake-pro-model",
- 2: "fake-flash-model",
- 3: "fake-lite-model",
- }
- }
-
- monkeypatch.setattr("solstone.think.providers.PROVIDER_REGISTRY", fake_registry)
- monkeypatch.setattr("solstone.think.models.PROVIDER_DEFAULTS", fake_defaults)
+ monkeypatch.setattr(
+ "solstone.think.providers.PROVIDER_REGISTRY", {"fake": object()}
+ )
+ monkeypatch.setattr(
+ "solstone.think.models.default_model_for_provider",
+ lambda _provider: "fake-model",
+ )
_patch_health_journal(monkeypatch, providers_cli, tmp_path)
monkeypatch.setattr(
providers_cli,
@@ -99,97 +88,26 @@ def test_run_check_partial_failure_exits_one(tmp_path, monkeypatch):
monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate)
- args = argparse.Namespace(
- provider=None,
- interface=None,
- tier=None,
- json=False,
- timeout=1,
- targeted=False,
- )
-
with pytest.raises(SystemExit) as exc_info:
- asyncio.run(providers_cli._run_check(args))
+ asyncio.run(providers_cli._run_check(_args()))
assert exc_info.value.code == 1
-
- health_file = tmp_path / "health" / "talents.json"
- payload = json.loads(health_file.read_text())
- assert payload["summary"]["passed"] == 3
- assert payload["summary"]["skipped"] == 0
- assert payload["summary"]["failed"] == 3
- assert all(
- row["reason_code"] == "unknown"
- for row in payload["results"]
- if row["status"] == "fail"
- )
+ payload = json.loads((tmp_path / "health" / "talents.json").read_text())
+ assert payload["summary"] == {"total": 2, "passed": 1, "skipped": 0, "failed": 1}
+ assert payload["results"][1]["reason_code"] == "unknown"
-def test_run_check_full_provider_failure_exits_one(tmp_path, monkeypatch):
- """_run_check exits 1 when all checks for a provider fail."""
+def test_run_check_targeted_uses_active_routes(tmp_path, monkeypatch):
import solstone.think.providers_cli as providers_cli
- fake_registry = {"fake": object()}
- fake_defaults = {
- "fake": {
- 1: "fake-pro-model",
- 2: "fake-flash-model",
- 3: "fake-lite-model",
- }
- }
-
- monkeypatch.setattr("solstone.think.providers.PROVIDER_REGISTRY", fake_registry)
- monkeypatch.setattr("solstone.think.models.PROVIDER_DEFAULTS", fake_defaults)
_patch_health_journal(monkeypatch, providers_cli, tmp_path)
monkeypatch.setattr(
- providers_cli,
- "_check_generate",
- lambda *_args: ("fail", "FAIL: key not set", "unknown"),
- )
-
- async def mock_check_cogitate(*_args):
- return "fail", "FAIL: key not set", "unknown"
-
- monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate)
-
- args = argparse.Namespace(
- provider=None,
- interface=None,
- tier=None,
- json=False,
- timeout=1,
- targeted=False,
+ "solstone.think.models.resolve_provider",
+ lambda interface: {
+ "generate": ("google", "gemini-flash-latest"),
+ "cogitate": ("openai", "gpt-5.4-mini"),
+ }[interface],
)
-
- with pytest.raises(SystemExit) as exc_info:
- asyncio.run(providers_cli._run_check(args))
-
- assert exc_info.value.code == 1
-
- health_file = tmp_path / "health" / "talents.json"
- payload = json.loads(health_file.read_text())
- assert payload["summary"]["passed"] == 0
- assert payload["summary"]["skipped"] == 0
- assert payload["summary"]["failed"] == 6
-
-
-def test_run_check_dedup_same_model(tmp_path, monkeypatch):
- """_run_check deduplicates checks when tiers resolve to the same model."""
- import solstone.think.providers_cli as providers_cli
-
- fake_registry = {"fake": object()}
- fake_defaults = {
- "fake": {
- 1: "fake-same-model",
- 2: "fake-same-model",
- 3: "fake-same-model",
- }
- }
-
- monkeypatch.setattr("solstone.think.providers.PROVIDER_REGISTRY", fake_registry)
- monkeypatch.setattr("solstone.think.models.PROVIDER_DEFAULTS", fake_defaults)
- _patch_health_journal(monkeypatch, providers_cli, tmp_path)
-
gen_mock = MagicMock(return_value=("ok", "ok", None))
monkeypatch.setattr(providers_cli, "_check_generate", gen_mock)
@@ -200,115 +118,24 @@ def test_run_check_dedup_same_model(tmp_path, monkeypatch):
monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate)
- args = argparse.Namespace(
- provider=None,
- interface=None,
- tier=None,
- json=False,
- timeout=1,
- targeted=False,
- )
-
with pytest.raises(SystemExit) as exc_info:
- asyncio.run(providers_cli._run_check(args))
+ asyncio.run(providers_cli._run_check(_args(targeted=True, json=True)))
assert exc_info.value.code == 0
- assert gen_mock.call_count == 1
- assert cog_inner.call_count == 1
-
- health_file = tmp_path / "health" / "talents.json"
- assert health_file.exists()
-
- payload = json.loads(health_file.read_text())
- results = payload["results"]
- assert len(results) == 6
- assert payload["summary"]["total"] == 6
- assert payload["summary"]["passed"] == 6
- assert payload["summary"]["skipped"] == 0
-
- non_reused = [result for result in results if "reused_from" not in result]
- reused = [result for result in results if "reused_from" in result]
- assert len(non_reused) == 2
- assert len(reused) == 4
- assert all(result["reused_from"] == "pro" for result in reused)
- assert all(result["elapsed_s"] == 0.0 for result in reused)
-
-
-def test_run_check_targeted_filters_to_configured_pairs(tmp_path, monkeypatch):
- """--targeted filters checks to only configured provider+tier pairs."""
- import solstone.think.providers_cli as providers_cli
-
- fake_registry = {"provA": object(), "provB": object(), "provC": object()}
- fake_defaults = {
- "provA": {1: "a-pro", 2: "a-flash", 3: "a-lite"},
- "provB": {1: "b-pro", 2: "b-flash", 3: "b-lite"},
- "provC": {1: "c-pro", 2: "c-flash", 3: "c-lite"},
- }
- fake_type_defaults = {
- "generate": {"provider": "provA", "tier": 2, "backup": "provB"},
- "cogitate": {"provider": "provC", "tier": 2, "backup": "provB"},
+ gen_mock.assert_called_once_with("google", "gemini-flash-latest", 1)
+ cog_inner.assert_called_once_with("openai", "gpt-5.4-mini", 1)
+ payload = json.loads((tmp_path / "health" / "talents.json").read_text())
+ assert {
+ (row["provider"], row["model"], row["interface"]) for row in payload["results"]
+ } == {
+ ("google", "gemini-flash-latest", "generate"),
+ ("openai", "gpt-5.4-mini", "cogitate"),
}
- monkeypatch.setattr("solstone.think.providers.PROVIDER_REGISTRY", fake_registry)
- monkeypatch.setattr("solstone.think.models.PROVIDER_DEFAULTS", fake_defaults)
- monkeypatch.setattr("solstone.think.models.TYPE_DEFAULTS", fake_type_defaults)
- monkeypatch.setattr(
- "solstone.think.models.resolve_provider",
- lambda _context, agent_type: (
- fake_type_defaults[agent_type]["provider"],
- fake_defaults[fake_type_defaults[agent_type]["provider"]][2],
- ),
- )
- _patch_health_journal(monkeypatch, providers_cli, tmp_path)
- monkeypatch.setattr(
- providers_cli,
- "_check_generate",
- lambda *_args: ("ok", "ok", None),
- )
-
- async def mock_check_cogitate(*_args):
- return "ok", "ok", None
-
- monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate)
-
- # Mock get_config to return no overrides (use TYPE_DEFAULTS)
- monkeypatch.setattr("solstone.think.utils.get_config", lambda: {})
-
- # Mock get_backup_provider to return the backup from fake_type_defaults
- def fake_get_backup(agent_type):
- d = fake_type_defaults[agent_type]
- if d["backup"] == d["provider"]:
- return None
- return d["backup"]
-
- monkeypatch.setattr("solstone.think.models.get_backup_provider", fake_get_backup)
-
- args = argparse.Namespace(
- provider=None,
- interface=None,
- tier=None,
- json=True,
- timeout=1,
- targeted=True,
- )
-
- with pytest.raises(SystemExit) as exc_info:
- asyncio.run(providers_cli._run_check(args))
-
- assert exc_info.value.code == 0
-
- health_file = tmp_path / "health" / "talents.json"
- payload = json.loads(health_file.read_text())
- # Expected targeted pairs: (provA, 2), (provB, 2), (provC, 2) = 3 pairs × 2 interfaces = 6 checks
- assert payload["summary"]["total"] == 6
- checked_pairs = {(r["provider"], r["tier"]) for r in payload["results"]}
- assert checked_pairs == {("provA", "flash"), ("provB", "flash"), ("provC", "flash")}
-
def test_run_check_targeted_empty_journal_uses_real_resolution(
tmp_path, monkeypatch, capsys
):
- """--targeted uses resolve_provider and selects no cloud pair for no-brain."""
import solstone.think.providers_cli as providers_cli
from solstone.think.models import NO_BRAIN_PROVIDER, resolve_provider
@@ -319,9 +146,8 @@ def test_run_check_targeted_empty_journal_uses_real_resolution(
"solstone.think.providers.state.local_runtime_ready", lambda: False
)
_patch_health_journal(monkeypatch, providers_cli, tmp_path)
-
- gen_mock = MagicMock(side_effect=AssertionError("cloud provider selected"))
- cog_mock = MagicMock(side_effect=AssertionError("cloud provider selected"))
+ gen_mock = MagicMock(side_effect=AssertionError("provider selected"))
+ cog_mock = MagicMock(side_effect=AssertionError("provider selected"))
monkeypatch.setattr(providers_cli, "_check_generate", gen_mock)
async def mock_check_cogitate(*_args):
@@ -329,89 +155,93 @@ def test_run_check_targeted_empty_journal_uses_real_resolution(
monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate)
- args = argparse.Namespace(
- provider=None,
- interface=None,
- tier=None,
- json=True,
- timeout=1,
- targeted=True,
- )
-
with pytest.raises(SystemExit) as exc_info:
- asyncio.run(providers_cli._run_check(args))
+ asyncio.run(providers_cli._run_check(_args(targeted=True, json=True)))
assert exc_info.value.code == 0
- assert resolve_provider("", "generate") == (NO_BRAIN_PROVIDER, "")
- assert resolve_provider("", "cogitate") == (NO_BRAIN_PROVIDER, "")
+ assert resolve_provider("generate") == (NO_BRAIN_PROVIDER, "")
+ assert resolve_provider("cogitate") == (NO_BRAIN_PROVIDER, "")
gen_mock.assert_not_called()
cog_mock.assert_not_called()
-
payload = json.loads((tmp_path / "health" / "talents.json").read_text())
assert payload["results"] == []
assert payload["summary"] == {"total": 0, "passed": 0, "skipped": 0, "failed": 0}
- printed = capsys.readouterr().out
- assert '"results": []' in printed
- assert "google" not in printed
+ assert '"results": []' in capsys.readouterr().out
def test_run_check_targeted_flock_dedup(tmp_path, monkeypatch):
- """--targeted exits silently when another targeted check holds the lock."""
- import fcntl
-
import solstone.think.providers_cli as providers_cli
- fake_registry = {"fake": object()}
- fake_defaults = {"fake": {1: "m", 2: "m", 3: "m"}}
- fake_type_defaults = {
- "generate": {"provider": "fake", "tier": 2, "backup": "fake"},
- "cogitate": {"provider": "fake", "tier": 2, "backup": "fake"},
- }
-
- monkeypatch.setattr("solstone.think.providers.PROVIDER_REGISTRY", fake_registry)
- monkeypatch.setattr("solstone.think.models.PROVIDER_DEFAULTS", fake_defaults)
- monkeypatch.setattr("solstone.think.models.TYPE_DEFAULTS", fake_type_defaults)
+ _patch_health_journal(monkeypatch, providers_cli, tmp_path)
monkeypatch.setattr(
"solstone.think.models.resolve_provider",
- lambda _context, agent_type: (
- fake_type_defaults[agent_type]["provider"],
- fake_defaults[fake_type_defaults[agent_type]["provider"]][2],
- ),
+ lambda _interface: ("google", "gemini-flash-latest"),
)
- _patch_health_journal(monkeypatch, providers_cli, tmp_path)
- monkeypatch.setattr("solstone.think.utils.get_config", lambda: {})
- monkeypatch.setattr("solstone.think.models.get_backup_provider", lambda _: None)
-
- # Pre-acquire the lock to simulate a concurrent check
lock_dir = tmp_path / "health"
lock_dir.mkdir(parents=True, exist_ok=True)
- lock_file = open(lock_dir / "recheck.lock", "w")
+ lock_file = open(lock_dir / "recheck.lock", "w", encoding="utf-8")
fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
-
gen_mock = MagicMock(return_value=("ok", "ok", None))
monkeypatch.setattr(providers_cli, "_check_generate", gen_mock)
- args = argparse.Namespace(
- provider=None,
- interface=None,
- tier=None,
- json=False,
- timeout=1,
- targeted=True,
+ asyncio.run(providers_cli._run_check(_args(targeted=True)))
+
+ gen_mock.assert_not_called()
+ assert not (tmp_path / "health" / "talents.json").exists()
+ lock_file.close()
+
+
+def test_explicit_provider_uses_single_default_model(tmp_path, monkeypatch):
+ import solstone.think.providers_cli as providers_cli
+
+ _patch_health_journal(monkeypatch, providers_cli, tmp_path)
+ monkeypatch.setattr(
+ "solstone.think.providers.PROVIDER_REGISTRY",
+ {"fake": object(), "other": object()},
)
+ monkeypatch.setattr(
+ "solstone.think.models.default_model_for_provider",
+ lambda provider: f"{provider}-default",
+ )
+ gen_mock = MagicMock(return_value=("ok", "ok", None))
+ monkeypatch.setattr(providers_cli, "_check_generate", gen_mock)
- # Should return silently (no SystemExit, no checks run)
- asyncio.run(providers_cli._run_check(args))
- assert gen_mock.call_count == 0
+ with pytest.raises(SystemExit):
+ asyncio.run(
+ providers_cli._run_check(
+ _args(provider=["fake"], interface="generate", json=True)
+ )
+ )
- # No health file written
- assert not (tmp_path / "health" / "talents.json").exists()
+ gen_mock.assert_called_once_with("fake", "fake-default", 1)
- lock_file.close()
+
+def test_explicit_provider_can_override_model(tmp_path, monkeypatch):
+ import solstone.think.providers_cli as providers_cli
+
+ _patch_health_journal(monkeypatch, providers_cli, tmp_path)
+ monkeypatch.setattr(
+ "solstone.think.providers.PROVIDER_REGISTRY", {"fake": object()}
+ )
+ gen_mock = MagicMock(return_value=("ok", "ok", None))
+ monkeypatch.setattr(providers_cli, "_check_generate", gen_mock)
+
+ with pytest.raises(SystemExit):
+ asyncio.run(
+ providers_cli._run_check(
+ _args(
+ provider=["fake"],
+ interface="generate",
+ model="custom-model",
+ json=True,
+ )
+ )
+ )
+
+ gen_mock.assert_called_once_with("fake", "custom-model", 1)
def test_check_generate_logs_token_usage(monkeypatch):
- """_check_generate logs token usage when result includes usage data."""
import solstone.think.providers_cli as providers_cli
fake_module = MagicMock()
@@ -419,56 +249,31 @@ def test_check_generate_logs_token_usage(monkeypatch):
"text": "OK",
"usage": {"input_tokens": 5, "output_tokens": 2},
}
-
monkeypatch.setattr(
"solstone.think.providers.get_provider_module", lambda _: fake_module
)
monkeypatch.setattr(
"solstone.think.providers.PROVIDER_METADATA",
- {"fake": {"env_key": "FAKE_API_KEY"}},
- )
- monkeypatch.setattr(
- "solstone.think.models.PROVIDER_DEFAULTS", {"fake": {2: "fake-flash"}}
+ {"fake": {"env_key": "FAKE_API_KEY", "label": "Fake Provider"}},
)
monkeypatch.setenv("FAKE_API_KEY", "test-key")
-
log_mock = MagicMock()
monkeypatch.setattr("solstone.think.models.log_token_usage", log_mock)
- status, msg, reason_code = providers_cli._check_generate("fake", 2, 30)
+ status, msg, reason_code = providers_cli._check_generate("fake", "fake-model", 30)
assert status == "ok"
assert msg == "OK"
assert reason_code is None
log_mock.assert_called_once_with(
- model="fake-flash",
+ model="fake-model",
usage={"input_tokens": 5, "output_tokens": 2},
context="health.check.generate",
type="generate",
)
-def test_cortex_start_emits_providers_check(tmp_path):
- """Cortex startup requests a providers health check via supervisor."""
- from solstone.think.cortex import CortexService
-
- cortex = CortexService(journal_path=str(tmp_path))
- cortex.callosum = MagicMock()
- cortex.callosum.start.return_value = None
- cortex.shutdown_requested.set()
-
- with patch("solstone.think.cortex.threading.Thread") as mock_thread:
- mock_thread.return_value = MagicMock()
- with patch("solstone.think.cortex.time.sleep", return_value=None):
- cortex.start()
-
- cortex.callosum.emit.assert_any_call(
- "supervisor", "request", cmd=["journal", "providers", "check"]
- )
-
-
def test_missing_env_key_returns_skip(monkeypatch):
- """_check_generate returns skip status when env key is not set."""
import solstone.think.providers_cli as providers_cli
monkeypatch.setattr(
@@ -477,7 +282,7 @@ def test_missing_env_key_returns_skip(monkeypatch):
)
monkeypatch.delenv("FAKE_API_KEY", raising=False)
- status, msg, reason_code = providers_cli._check_generate("fake", 2, 30)
+ status, msg, reason_code = providers_cli._check_generate("fake", "fake-model", 30)
assert status == "skip"
assert reason_code == "provider_key_missing"
assert "Fake Provider not configured" in msg
@@ -499,7 +304,7 @@ def test_check_cogitate_cloud_configured_runs_without_install_skip(monkeypatch):
)
status, msg, reason_code = asyncio.run(
- providers_cli._check_cogitate("anthropic", 2, 30)
+ providers_cli._check_cogitate("anthropic", "claude-sonnet-4-6", 30)
)
assert (status, msg) == ("ok", "OK")
@@ -517,7 +322,6 @@ def test_check_cogitate_local_missing_runtime_names_local_install_hint(monkeypat
"cogitate_cli_found": False,
},
)
-
monkeypatch.setattr(
"solstone.think.providers.state.readiness_for_provider",
lambda *_args: type("FakeState", (), {"reason_code": "local_model_missing"})(),
@@ -528,7 +332,7 @@ def test_check_cogitate_local_missing_runtime_names_local_install_hint(monkeypat
)
status, msg, reason_code = asyncio.run(
- providers_cli._check_cogitate("local", 2, 30)
+ providers_cli._check_cogitate("local", "local/qwen3.5-4b", 30)
)
assert status == "skip"
@@ -561,7 +365,7 @@ def test_check_cogitate_local_endpoint_unreachable_uses_endpoint_reason(monkeypa
)
status, msg, reason_code = asyncio.run(
- providers_cli._check_cogitate("local", 2, 30)
+ providers_cli._check_cogitate("local", "local/qwen3.5-4b", 30)
)
assert status == "skip"
@@ -570,206 +374,19 @@ def test_check_cogitate_local_endpoint_unreachable_uses_endpoint_reason(monkeypa
assert "journal install-provider local" not in msg
-def test_check_cogitate_local_gpu_unavailable_uses_issue_copy(monkeypatch):
- import solstone.think.providers_cli as providers_cli
-
- monkeypatch.setattr(
- providers_cli,
- "_provider_status",
- lambda _name: {
- "configured": True,
- "cogitate_cli_found": True,
- "cogitate_ready": False,
- "issues": ["gpu_unavailable"],
- },
- )
-
- monkeypatch.setattr(
- "solstone.think.providers.state.readiness_for_provider",
- lambda *_args: type("FakeState", (), {"reason_code": "gpu_unavailable"})(),
- )
-
- status, msg, reason_code = asyncio.run(
- providers_cli._check_cogitate("local", 2, 30)
- )
-
- assert status == "skip"
- assert msg == "gpu_unavailable"
- assert reason_code == "gpu_unavailable"
-
-
-def test_all_skip_exits_zero(tmp_path, monkeypatch):
- """Exit code is 0 when all results are skipped (no fails)."""
- import solstone.think.providers_cli as providers_cli
-
- fake_registry = {"fake": object()}
- fake_defaults = {"fake": {1: "m1", 2: "m2", 3: "m3"}}
-
- monkeypatch.setattr("solstone.think.providers.PROVIDER_REGISTRY", fake_registry)
- monkeypatch.setattr("solstone.think.models.PROVIDER_DEFAULTS", fake_defaults)
- _patch_health_journal(monkeypatch, providers_cli, tmp_path)
- monkeypatch.setattr(
- providers_cli,
- "_check_generate",
- lambda *_args: ("skip", "not configured", "provider_key_missing"),
- )
-
- async def mock_check_cogitate(*_args):
- return "skip", "not configured", "provider_key_missing"
-
- monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate)
-
- args = argparse.Namespace(
- provider=None,
- interface=None,
- tier=None,
- json=False,
- timeout=1,
- targeted=False,
- )
-
- with pytest.raises(SystemExit) as exc_info:
- asyncio.run(providers_cli._run_check(args))
-
- assert exc_info.value.code == 0
-
- payload = json.loads((tmp_path / "health" / "talents.json").read_text())
- assert payload["summary"]["skipped"] == 6
- assert payload["summary"]["failed"] == 0
- assert payload["summary"]["passed"] == 0
- for result in payload["results"]:
- assert result["status"] == "skip"
- assert result["reason_code"] == "provider_key_missing"
- assert result["ok"] is True
-
-
-def test_mix_skip_and_fail_exits_one(tmp_path, monkeypatch):
- """Exit code is 1 when there's a mix of skip and fail results."""
- import solstone.think.providers_cli as providers_cli
-
- fake_registry = {"fake": object()}
- fake_defaults = {"fake": {1: "m1", 2: "m2", 3: "m3"}}
-
- monkeypatch.setattr("solstone.think.providers.PROVIDER_REGISTRY", fake_registry)
- monkeypatch.setattr("solstone.think.models.PROVIDER_DEFAULTS", fake_defaults)
- _patch_health_journal(monkeypatch, providers_cli, tmp_path)
- monkeypatch.setattr(
- providers_cli,
- "_check_generate",
- lambda *_args: ("skip", "not configured", "provider_key_missing"),
- )
-
- async def mock_check_cogitate(*_args):
- return "fail", "FAIL: broken", "unknown"
-
- monkeypatch.setattr(providers_cli, "_check_cogitate", mock_check_cogitate)
-
- args = argparse.Namespace(
- provider=None,
- interface=None,
- tier=None,
- json=False,
- timeout=1,
- targeted=False,
- )
-
- with pytest.raises(SystemExit) as exc_info:
- asyncio.run(providers_cli._run_check(args))
-
- assert exc_info.value.code == 1
-
- payload = json.loads((tmp_path / "health" / "talents.json").read_text())
- assert payload["summary"]["skipped"] == 3
- assert payload["summary"]["failed"] == 3
-
-
-def test_skipped_count_in_summary(tmp_path, monkeypatch):
- """Summary total equals passed + skipped + failed."""
- import solstone.think.providers_cli as providers_cli
-
- fake_registry = {"okp": object(), "skipP": object()}
- fake_defaults = {
- "okp": {1: "m1", 2: "m2", 3: "m3"},
- "skipP": {1: "s1", 2: "s2", 3: "s3"},
- }
-
- monkeypatch.setattr("solstone.think.providers.PROVIDER_REGISTRY", fake_registry)
- monkeypatch.setattr("solstone.think.models.PROVIDER_DEFAULTS", fake_defaults)
- _patch_health_journal(monkeypatch, providers_cli, tmp_path)
-
- def mock_gen(provider, tier, timeout):
- if provider == "okp":
- return "ok", "OK", None
- return "skip", "not configured", "provider_key_missing"
-
- monkeypatch.setattr(providers_cli, "_check_generate", mock_gen)
-
- async def mock_cog(provider, tier, timeout):
- if provider == "okp":
- return "ok", "OK", None
- return "skip", "not configured", "provider_key_missing"
-
- monkeypatch.setattr(providers_cli, "_check_cogitate", mock_cog)
-
- args = argparse.Namespace(
- provider=None,
- interface=None,
- tier=None,
- json=True,
- timeout=1,
- targeted=False,
- )
-
- with pytest.raises(SystemExit) as exc_info:
- asyncio.run(providers_cli._run_check(args))
-
- assert exc_info.value.code == 0
- payload = json.loads((tmp_path / "health" / "talents.json").read_text())
- summary = payload["summary"]
- assert (
- summary["total"] == summary["passed"] + summary["skipped"] + summary["failed"]
- )
- assert summary["passed"] == 6
- assert summary["skipped"] == 6
- assert summary["failed"] == 0
-
-
-def test_status_field_in_json_output(tmp_path, monkeypatch, capsys):
- """JSON output includes status per result and skipped in summary."""
- import solstone.think.providers_cli as providers_cli
-
- fake_registry = {"fake": object()}
- fake_defaults = {"fake": {1: "m1", 2: "m2", 3: "m3"}}
-
- monkeypatch.setattr("solstone.think.providers.PROVIDER_REGISTRY", fake_registry)
- monkeypatch.setattr("solstone.think.models.PROVIDER_DEFAULTS", fake_defaults)
- _patch_health_journal(monkeypatch, providers_cli, tmp_path)
- monkeypatch.setattr(
- providers_cli,
- "_check_generate",
- lambda *_args: ("ok", "OK", None),
- )
+def test_cortex_start_emits_providers_check(tmp_path):
+ from solstone.think.cortex import CortexService
- async def mock_cog(*_args):
- return "ok", "OK", None
+ cortex = CortexService(journal_path=str(tmp_path))
+ cortex.callosum = MagicMock()
+ cortex.callosum.start.return_value = None
+ cortex.shutdown_requested.set()
- monkeypatch.setattr(providers_cli, "_check_cogitate", mock_cog)
+ with patch("solstone.think.cortex.threading.Thread") as mock_thread:
+ mock_thread.return_value = MagicMock()
+ with patch("solstone.think.cortex.time.sleep", return_value=None):
+ cortex.start()
- args = argparse.Namespace(
- provider=None,
- interface=None,
- tier=None,
- json=True,
- timeout=1,
- targeted=False,
+ cortex.callosum.emit.assert_any_call(
+ "supervisor", "request", cmd=["journal", "providers", "check"]
)
-
- with pytest.raises(SystemExit):
- asyncio.run(providers_cli._run_check(args))
-
- captured = capsys.readouterr()
- data = json.loads(captured.out)
- for result in data["results"]:
- assert "status" in result
- assert result["status"] in ("ok", "skip", "fail")
- assert "skipped" in data["summary"]
diff --git a/tests/test_sense_schema.py b/tests/test_sense_schema.py
index ee0fa9490..40c88b8ef 100644
--- a/tests/test_sense_schema.py
+++ b/tests/test_sense_schema.py
@@ -111,7 +111,7 @@ def _render_sense_for_tmp_journal(
def test_sense_prompt_parses_and_documents_role_and_source():
post = frontmatter.load(SENSE_PATH)
- assert post.metadata["tier"] == 3
+ assert "tier" not in post.metadata
output_schema = _section(
post.content, "## Output Schema", "## Field-by-Field Instructions"
diff --git a/tests/test_talent_fallback.py b/tests/test_talent_fallback.py
index 415b16160..7ae4670ba 100644
--- a/tests/test_talent_fallback.py
+++ b/tests/test_talent_fallback.py
@@ -1,1446 +1,179 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright (c) 2026 sol pbc
-import asyncio
import json
-from datetime import datetime, timedelta, timezone
-from io import StringIO
-from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
-from solstone.think.models import (
- LOCAL_MODEL,
- NO_BRAIN_PROVIDER,
- TYPE_DEFAULTS,
- IncompleteJSONError,
- NoBrainConfiguredError,
- get_backup_provider,
- is_provider_healthy,
- is_provider_model_interface_healthy,
- should_recheck_health,
-)
+from solstone.think.models import LOCAL_MODEL, IncompleteJSONError
from solstone.think.providers.cli import QuotaExhaustedError
from solstone.think.providers.local import LocalCapacityExhausted
-from solstone.think.talents import (
- TalentHookError,
- _is_retryable_error,
- _should_fallback,
-)
-from solstone.think.utils import now_ms
+from solstone.think.talents import _execute_generate, _execute_with_tools
-def test_is_provider_healthy_all_failed():
- health_data = {
- "results": [
- {"provider": "google", "ok": False},
- {"provider": "google", "ok": False},
- ]
- }
- assert is_provider_healthy("google", health_data) is False
-
-
-def test_is_provider_healthy_some_passed():
- health_data = {
- "results": [
- {"provider": "google", "ok": False},
- {"provider": "google", "ok": True},
- ]
- }
- assert is_provider_healthy("google", health_data) is True
-
-
-def test_is_provider_healthy_no_data():
- assert is_provider_healthy("google", None) is True
-
-
-def test_is_provider_healthy_no_results_for_provider():
- health_data = {"results": [{"provider": "anthropic", "ok": False}]}
- assert is_provider_healthy("google", health_data) is True
-
-
-def test_is_provider_model_interface_healthy_match_failed():
- health_data = {
- "results": [
- {
- "provider": "google",
- "model": "gemini-3-flash-preview",
- "interface": "cogitate",
- "ok": False,
- }
- ]
- }
- assert (
- is_provider_model_interface_healthy(
- "google", "gemini-3-flash-preview", "cogitate", health_data
- )
- is False
- )
-
-
-def test_is_provider_model_interface_healthy_mismatch_is_healthy():
- health_data = {
- "results": [
- {
- "provider": "google",
- "model": "gemini-3-flash-preview",
- "interface": "generate",
- "ok": False,
- }
- ]
- }
- assert (
- is_provider_model_interface_healthy(
- "google", "gemini-3-flash-preview", "cogitate", health_data
- )
- is True
- )
-
-
-def test_is_provider_model_interface_healthy_missing_fields_are_healthy():
- health_data = {"results": [{"provider": "google", "ok": False}]}
- assert (
- is_provider_model_interface_healthy(
- "google", "gemini-3-flash-preview", "cogitate", health_data
- )
- is True
- )
-
-
-def test_is_provider_model_interface_healthy_none_data():
- assert (
- is_provider_model_interface_healthy(
- "google", "gemini-3-flash-preview", "cogitate", None
- )
- is True
- )
-
-
-def test_should_recheck_health_stale():
- checked_at = (datetime.now(timezone.utc) - timedelta(hours=2)).isoformat()
- health_data = {"checked_at": checked_at}
- assert should_recheck_health(health_data) is True
-
-
-def test_should_recheck_health_fresh():
- checked_at = (datetime.now(timezone.utc) - timedelta(minutes=10)).isoformat()
- health_data = {"checked_at": checked_at}
- assert should_recheck_health(health_data) is False
-
-
-def test_should_recheck_health_honors_reset_at_ms():
- checked_at = (datetime.now(timezone.utc) - timedelta(hours=2)).isoformat()
- pre_reset = {
- "checked_at": checked_at,
- "results": [{"ok": False, "reset_at_ms": now_ms() + 60_000}],
- }
- post_reset = {
- "checked_at": checked_at,
- "results": [{"ok": False, "reset_at_ms": now_ms() - 1_000}],
- }
- no_reset = {
- "checked_at": checked_at,
- "results": [{"ok": False}],
- }
-
- assert should_recheck_health(pre_reset) is False
- assert should_recheck_health(post_reset) is True
- assert should_recheck_health(no_reset) is True
-
-
-def test_get_backup_provider_from_config(monkeypatch):
- monkeypatch.setattr(
- "solstone.think.models.get_config",
- lambda: {"providers": {"generate": {"provider": "google", "backup": "openai"}}},
- )
- assert get_backup_provider("generate") == "openai"
-
-
-def test_get_backup_provider_no_brain_disables_backup(monkeypatch):
- monkeypatch.setattr("solstone.think.models.get_config", lambda: {})
- monkeypatch.setattr(
- "solstone.think.providers.state.local_runtime_ready", lambda: False
- )
- monkeypatch.delenv("GOOGLE_API_KEY", raising=False)
- monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
- monkeypatch.delenv("OPENAI_API_KEY", raising=False)
-
- assert get_backup_provider("generate") is None
- assert get_backup_provider("cogitate") is None
-
-
-def test_get_backup_provider_keyed_default_uses_fallback_constant(monkeypatch):
- monkeypatch.setattr("solstone.think.models.get_config", lambda: {})
- monkeypatch.setenv("GOOGLE_API_KEY", "test-key")
-
- assert get_backup_provider("generate") == TYPE_DEFAULTS["generate"]["backup"]
- assert get_backup_provider("cogitate") == TYPE_DEFAULTS["cogitate"]["backup"]
-
-
-def test_get_backup_provider_none_when_same_as_primary(monkeypatch):
- monkeypatch.setattr(
- "solstone.think.models.get_config",
- lambda: {
- "providers": {
- "generate": {"provider": "openai", "backup": "openai"},
- }
- },
- )
- assert get_backup_provider("generate") is None
-
-
-@pytest.mark.parametrize("agent_type", ["generate", "cogitate"])
-def test_get_backup_provider_local_disables_backup(monkeypatch, agent_type):
- monkeypatch.setattr(
- "solstone.think.models.get_config",
- lambda: {
- "providers": {
- agent_type: {"provider": "local", "backup": "anthropic"},
- }
- },
- )
- assert get_backup_provider(agent_type) is None
-
-
-@pytest.mark.parametrize("agent_type", ["generate", "cogitate"])
-def test_get_backup_provider_no_brain_primary_disables_backup(monkeypatch, agent_type):
- monkeypatch.setattr(
- "solstone.think.models.get_config",
- lambda: {
- "providers": {
- agent_type: {"provider": NO_BRAIN_PROVIDER, "backup": "anthropic"},
- }
- },
- )
- assert get_backup_provider(agent_type) is None
-
-
-def test_no_brain_error_is_non_retryable():
- assert _is_retryable_error(NoBrainConfiguredError()) is False
- assert _should_fallback(NoBrainConfiguredError()) is False
-
-
-def test_execute_with_tools_local_failure_does_not_consult_backup(monkeypatch):
- from solstone.think import talents
-
- class LocalModule:
- @staticmethod
- async def run_cogitate(config, on_event=None):
- raise RuntimeError("binary_missing")
-
- monkeypatch.setattr(
- "solstone.think.providers.get_provider_module",
- lambda provider: LocalModule,
- )
-
- def fail_backup(_agent_type):
- raise AssertionError("local failure must not consult cloud backup")
-
- monkeypatch.setattr("solstone.think.models.get_backup_provider", fail_backup)
-
- with pytest.raises(RuntimeError, match="binary_missing"):
- asyncio.run(
- talents._execute_with_tools(
- {
- "provider": "local",
- "model": LOCAL_MODEL,
- "output_path": None,
- },
- lambda _event: None,
- )
- )
-
-
-def _mock_base_agent_config() -> dict:
+def _generate_config(
+ *,
+ provider: str = "anthropic",
+ model: str = "claude-sonnet-4-6",
+) -> dict:
return {
- "type": "cogitate",
- "path": None,
- "sources": {},
- "system_instruction": "",
- "user_instruction": "",
- "prompt": "",
- "disabled": False,
+ "name": "test_generator",
+ "type": "generate",
+ "provider": provider,
+ "model": model,
+ "prompt": "say ok",
+ "output": "md",
+ "output_path": None,
+ "thinking_budget": 0,
+ "max_output_tokens": 32,
}
-def _patch_prepare_config_dependencies(monkeypatch):
- monkeypatch.setattr(
- "solstone.think.talent.get_talent",
- lambda *args, **kwargs: _mock_base_agent_config(),
- )
- monkeypatch.setattr(
- "solstone.think.talent.key_to_context", lambda _name: "talent.system.default"
- )
- monkeypatch.setattr(
- "solstone.think.models.resolve_provider",
- lambda _context, _type: ("google", "gemini-3-flash-preview"),
- )
- monkeypatch.setattr("solstone.think.models.get_context_registry", lambda: {})
-
-
-def test_prepare_config_rejects_frontmatter_outbound_approval(tmp_path, monkeypatch):
- import solstone.think.talent as talent_module
- from solstone.think.talents import prepare_config
-
- monkeypatch.setattr(talent_module, "TALENT_DIR", tmp_path)
- (tmp_path / "approval_static.md").write_text(
- "{\n"
- ' "type": "cogitate",\n'
- ' "outbound_approval": "static-template-value"\n'
- "}\n\n"
- "Prompt body\n"
- )
-
- with pytest.raises(
- ValueError,
- match=(
- "declares 'outbound_approval' in frontmatter; "
- "this field is launch-config-only"
- ),
- ):
- prepare_config({"name": "approval_static", "prompt": "hello"})
-
-
-def test_preflight_swap_unhealthy_primary(monkeypatch):
- from solstone.think.talents import prepare_config
-
- _patch_prepare_config_dependencies(monkeypatch)
- monkeypatch.setattr(
- "solstone.think.models.load_health_status",
- lambda: {
- "results": [
- {
- "provider": "google",
- "model": "gemini-3-flash-preview",
- "interface": "cogitate",
- "ok": False,
- }
- ]
- },
- )
- monkeypatch.setattr("solstone.think.models.should_recheck_health", lambda _h: False)
- monkeypatch.setattr(
- "solstone.think.models.get_backup_provider", lambda _type: "anthropic"
- )
- monkeypatch.setattr(
- "solstone.think.models.resolve_model_for_provider",
- lambda _context, _provider, _type="generate": "claude-sonnet-4-5",
- )
- monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
-
- config = prepare_config({"name": "chat", "prompt": "hello"})
-
- assert config["provider"] == "anthropic"
- assert config["model"] == "claude-sonnet-4-5"
- assert config["fallback_from"] == "google"
-
-
-def test_preflight_swap_reads_unified_backend(monkeypatch):
- from solstone.think.models import CLAUDE_SONNET_4
- from solstone.think.providers import state
- from solstone.think.talents import prepare_config
-
- _patch_prepare_config_dependencies(monkeypatch)
- monkeypatch.setattr(
- state,
- "read_health_status",
- lambda: {
- "results": [
- {
- "provider": "google",
- "model": "gemini-3-flash-preview",
- "interface": "cogitate",
- "ok": False,
- "reason_code": "provider_unavailable",
- }
- ]
- },
- )
- monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
-
- config = prepare_config({"name": "chat", "prompt": "hello"})
-
- assert config["provider"] == "anthropic"
- assert config["model"] == CLAUDE_SONNET_4
- assert config["fallback_from"] == "google"
-
-
-def test_preflight_no_swap_healthy_primary(monkeypatch):
- from solstone.think.talents import prepare_config
-
- _patch_prepare_config_dependencies(monkeypatch)
- monkeypatch.setattr(
- "solstone.think.models.load_health_status",
- lambda: {
- "results": [
- {
- "provider": "google",
- "model": "gemini-3-flash-preview",
- "interface": "cogitate",
- "ok": True,
- }
- ]
- },
- )
- monkeypatch.setattr("solstone.think.models.should_recheck_health", lambda _h: False)
-
- config = prepare_config({"name": "chat", "prompt": "hello"})
-
- assert config["provider"] == "google"
- assert "fallback_from" not in config
-
-
-def test_preflight_no_swap_no_backup_key(monkeypatch):
- from solstone.think.talents import prepare_config
-
- _patch_prepare_config_dependencies(monkeypatch)
- monkeypatch.setattr(
- "solstone.think.models.load_health_status",
- lambda: {
- "results": [
- {
- "provider": "google",
- "model": "gemini-3-flash-preview",
- "interface": "cogitate",
- "ok": False,
- }
- ]
- },
- )
- monkeypatch.setattr("solstone.think.models.should_recheck_health", lambda _h: False)
- monkeypatch.setattr(
- "solstone.think.models.get_backup_provider", lambda _type: "anthropic"
- )
- monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
-
- config = prepare_config({"name": "chat", "prompt": "hello"})
-
- assert config["provider"] == "google"
- assert "fallback_from" not in config
-
-
-def test_on_failure_retry_cogitate(monkeypatch):
- from solstone.think.talents import _execute_with_tools
-
- events = []
- attempts = {"primary": 0, "backup": 0}
-
- async def fail_cogitate(*_args, **_kwargs):
- attempts["primary"] += 1
- raise RuntimeError("primary down")
+def _health_rows(tmp_path):
+ return json.loads((tmp_path / "health" / "talents.json").read_text())["results"]
- async def pass_cogitate(*_args, **kwargs):
- attempts["backup"] += 1
- on_event = kwargs.get("on_event")
- if on_event:
- on_event({"event": "finish", "result": "backup result"})
- return "backup result"
- monkeypatch.setattr(
- "solstone.think.providers.PROVIDER_REGISTRY", {"google": "x", "anthropic": "y"}
- )
- monkeypatch.setattr(
- "solstone.think.providers.get_provider_module",
- lambda provider: SimpleNamespace(
- run_cogitate=fail_cogitate if provider == "google" else pass_cogitate
- ),
- )
- monkeypatch.setattr(
- "solstone.think.models.get_backup_provider", lambda _type: "anthropic"
- )
- monkeypatch.setattr(
- "solstone.think.models.resolve_model_for_provider",
- lambda _context, _provider, _type="cogitate": "claude-sonnet-4-5",
+@pytest.mark.asyncio
+async def test_execute_with_tools_quota_records_and_does_not_switch(
+ monkeypatch,
+ tmp_path,
+):
+ monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
+ run_cogitate = MagicMock(
+ side_effect=QuotaExhaustedError("quota exhausted", retry_delay_ms=5000)
)
- monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
-
- config = {
- "provider": "google",
- "model": "gemini-3-flash-preview",
- "health_stale": False,
- "context": "talent.system.default",
- }
-
- asyncio.run(_execute_with_tools(config, events.append))
-
- assert attempts["primary"] == 1
- assert attempts["backup"] == 1
- assert config["provider"] == "anthropic"
- assert config["model"] == "claude-sonnet-4-5"
- assert config["fallback_from"] == "google"
- assert any(e.get("event") == "fallback" for e in events)
-
-
-def test_quota_failure_records_health_and_falls_back(monkeypatch):
- from solstone.think.talents import _execute_with_tools
+ monkeypatch.setattr("solstone.think.providers.openhands.run_cogitate", run_cogitate)
events = []
- record_mock = MagicMock()
-
- async def fail_quota(*_args, **_kwargs):
- raise QuotaExhaustedError("quota exhausted", retry_delay_ms=1000)
-
- async def pass_cogitate(*_args, **kwargs):
- on_event = kwargs.get("on_event")
- if on_event:
- on_event({"event": "finish", "result": "backup result"})
- return "backup result"
-
- monkeypatch.setattr(
- "solstone.think.providers.PROVIDER_REGISTRY", {"google": "x", "anthropic": "y"}
- )
- monkeypatch.setattr(
- "solstone.think.providers.get_provider_module",
- lambda provider: SimpleNamespace(
- run_cogitate=fail_quota if provider == "google" else pass_cogitate
- ),
- )
- monkeypatch.setattr(
- "solstone.think.models.get_backup_provider", lambda _type: "anthropic"
- )
- monkeypatch.setattr(
- "solstone.think.models.resolve_model_for_provider",
- lambda _context, _provider, _type="cogitate": "claude-sonnet-4-5",
- )
- monkeypatch.setattr("solstone.think.models.record_provider_failure", record_mock)
- monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
-
config = {
+ "name": "test_agent",
"type": "cogitate",
- "provider": "google",
- "tier": "flash",
- "model": "gemini-3-flash-preview",
- "health_stale": False,
- "context": "talent.system.default",
- }
- before_ms = now_ms()
-
- asyncio.run(_execute_with_tools(config, events.append))
-
- quota_event = next(e for e in events if e.get("reason") == "quota_exhausted")
- assert quota_event["terminal"] is False
- assert quota_event["reset_at_ms"] >= before_ms + 1000
- record_mock.assert_called_once_with(
- "google",
- "flash",
- "gemini-3-flash-preview",
- "cogitate",
- quota_event["reset_at_ms"],
- )
- assert config["provider"] == "anthropic"
- assert events[-1]["event"] == "finish"
-
-
-def test_on_failure_retry_cogitate_uses_context_from_name(monkeypatch):
- from solstone.think.talents import _execute_with_tools
-
- events = []
- seen = {}
-
- async def fail_cogitate(*_args, **_kwargs):
- raise RuntimeError("primary down")
-
- async def pass_cogitate(*_args, **kwargs):
- on_event = kwargs.get("on_event")
- if on_event:
- on_event({"event": "finish", "result": "backup result"})
- return "backup result"
-
- def resolve_model(context, _provider, _type="cogitate"):
- seen["context"] = context
- return "claude-sonnet-4-5"
-
- monkeypatch.setattr(
- "solstone.think.providers.PROVIDER_REGISTRY", {"google": "x", "anthropic": "y"}
- )
- monkeypatch.setattr(
- "solstone.think.providers.get_provider_module",
- lambda provider: SimpleNamespace(
- run_cogitate=fail_cogitate if provider == "google" else pass_cogitate
- ),
- )
- monkeypatch.setattr(
- "solstone.think.talent.key_to_context",
- lambda _name: "talent.system.default",
- )
- monkeypatch.setattr(
- "solstone.think.models.get_backup_provider", lambda _type: "anthropic"
- )
- monkeypatch.setattr(
- "solstone.think.models.resolve_model_for_provider", resolve_model
- )
- monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
-
- config = {
- "name": "chat",
- "provider": "google",
- "model": "gemini-3-flash-preview",
- "health_stale": False,
- }
-
- asyncio.run(_execute_with_tools(config, events.append))
-
- assert seen["context"] == "talent.system.default"
-
-
-def test_execute_generate_uses_messages_when_present(monkeypatch):
- from solstone.think.talents import _execute_generate
-
- events = []
- seen = {}
- messages = [
- {"role": "user", "content": "first"},
- {"role": "assistant", "content": "second"},
- {"role": "user", "content": "third"},
- ]
-
- def mock_generate_with_result(**kwargs):
- seen["contents"] = kwargs["contents"]
- return {"text": "ok", "usage": {"input_tokens": 1, "output_tokens": 1}}
-
- monkeypatch.setattr(
- "solstone.think.talent.key_to_context", lambda _name: "talent.system.default"
- )
- monkeypatch.setattr(
- "solstone.think.models.generate_with_result", mock_generate_with_result
- )
-
- config = {
- "name": "chat",
- "messages": messages,
- "transcript": "ignored transcript",
- "user_instruction": "ignored instruction",
- "prompt": "ignored prompt",
- "health_stale": False,
- }
-
- asyncio.run(_execute_generate(config, events.append))
-
- assert seen["contents"] == messages
- assert events[-1]["event"] == "finish"
-
-
-def test_execute_generate_preserves_string_contents_order(monkeypatch):
- from solstone.think.talents import _execute_generate
-
- events = []
- seen = {}
-
- def mock_generate_with_result(**kwargs):
- seen["contents"] = kwargs["contents"]
- return {"text": "ok", "usage": {"input_tokens": 1, "output_tokens": 1}}
-
- monkeypatch.setattr(
- "solstone.think.talent.key_to_context", lambda _name: "talent.system.default"
- )
- monkeypatch.setattr(
- "solstone.think.models.generate_with_result", mock_generate_with_result
- )
-
- config = {
- "name": "chat",
- "transcript": "transcript",
- "user_instruction": "instruction",
- "prompt": "prompt",
- "health_stale": False,
- }
-
- asyncio.run(_execute_generate(config, events.append))
-
- assert seen["contents"] == ["transcript", "instruction", "prompt"]
- assert events[-1]["event"] == "finish"
-
-
-def test_execute_generate_passes_prepared_provider_and_model(monkeypatch):
- from solstone.think.talents import _execute_generate
-
- events = []
- seen = {}
-
- def mock_generate_with_result(**kwargs):
- seen["provider"] = kwargs.get("provider")
- seen["model"] = kwargs.get("model")
- return {"text": "ok", "usage": {"input_tokens": 1, "output_tokens": 1}}
-
- monkeypatch.setattr(
- "solstone.think.talent.key_to_context", lambda _name: "talent.system.default"
- )
- monkeypatch.setattr(
- "solstone.think.models.generate_with_result", mock_generate_with_result
- )
-
- config = {
- "name": "chat",
- "provider": "google",
- "model": "gemini-3-flash-preview",
- "prompt": "hello",
- "health_stale": False,
- }
-
- asyncio.run(_execute_generate(config, events.append))
-
- assert seen["provider"] == "google"
- assert seen["model"] == "gemini-3-flash-preview"
- assert events[-1]["event"] == "finish"
-
-
-def test_execute_generate_local_failure_does_not_consult_backup(monkeypatch):
- from solstone.think.talents import _execute_generate
-
- events = []
- calls = {"count": 0}
-
- def mock_generate_with_result(**_kwargs):
- calls["count"] += 1
- raise RuntimeError("binary_missing")
-
- def fail_backup(_agent_type):
- raise AssertionError("local failure must not consult cloud backup")
-
- monkeypatch.setattr(
- "solstone.think.talent.key_to_context", lambda _name: "talent.system.default"
- )
- monkeypatch.setattr(
- "solstone.think.models.generate_with_result", mock_generate_with_result
- )
- monkeypatch.setattr("solstone.think.models.get_backup_provider", fail_backup)
-
- config = {
- "name": "chat",
- "provider": "local",
- "prompt": "hello",
- "health_stale": False,
+ "provider": "anthropic",
+ "model": "claude-sonnet-4-6",
}
- with pytest.raises(RuntimeError, match="binary_missing"):
- asyncio.run(_execute_generate(config, events.append))
+ with pytest.raises(QuotaExhaustedError, match="quota exhausted"):
+ await _execute_with_tools(config, events.append)
- assert calls["count"] == 1
- assert not any(e.get("event") == "fallback" for e in events)
+ run_cogitate.assert_called_once()
+ assert run_cogitate.call_args.kwargs["config"]["provider"] == "anthropic"
+ assert run_cogitate.call_args.kwargs["config"]["model"] == "claude-sonnet-4-6"
+ rows = _health_rows(tmp_path)
+ assert len(rows) == 1
+ row = rows[0]
+ assert row["provider"] == "anthropic"
+ assert row["model"] == "claude-sonnet-4-6"
+ assert row["interface"] == "cogitate"
+ assert row["reason_code"] == "provider_quota_exceeded"
+ assert "tier" not in row
+ assert row["reset_at_ms"] > 0
+ assert [event["event"] for event in events] == ["error"]
-@pytest.mark.parametrize(
- ("base_temperature", "expected_retry_temperature"),
- [
- (None, 0.7),
- (0.8, 0.8),
- ],
-)
-def test_execute_generate_local_length_retry_succeeds(
- monkeypatch, base_temperature, expected_retry_temperature
+@pytest.mark.asyncio
+async def test_execute_generate_quota_records_and_does_not_switch(
+ monkeypatch,
+ tmp_path,
):
- from solstone.think.talents import _execute_generate
-
- events = []
+ monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
+ quota = QuotaExhaustedError("quota exhausted", retry_delay_ms=7000)
+ active = MagicMock(side_effect=quota)
+ inactive_google = MagicMock(side_effect=AssertionError("google called"))
+ inactive_openai = MagicMock(side_effect=AssertionError("openai called"))
+ inactive_local = MagicMock(side_effect=AssertionError("local called"))
+ monkeypatch.setattr("solstone.think.providers.anthropic.run_generate", active)
+ monkeypatch.setattr("solstone.think.providers.google.run_generate", inactive_google)
+ monkeypatch.setattr("solstone.think.providers.openai.run_generate", inactive_openai)
+ monkeypatch.setattr("solstone.think.providers.local.run_generate", inactive_local)
+
+ with pytest.raises(QuotaExhaustedError, match="quota exhausted"):
+ await _execute_generate(_generate_config(), lambda _event: None)
+
+ active.assert_called_once()
+ inactive_google.assert_not_called()
+ inactive_openai.assert_not_called()
+ inactive_local.assert_not_called()
+ row = _health_rows(tmp_path)[0]
+ assert row["provider"] == "anthropic"
+ assert row["model"] == "claude-sonnet-4-6"
+ assert row["interface"] == "generate"
+ assert row["reason_code"] == "provider_quota_exceeded"
+ assert "tier" not in row
+ assert row["reset_at_ms"] > 0
+
+
+@pytest.mark.asyncio
+async def test_execute_generate_local_length_retry_succeeds(monkeypatch):
calls = []
- def mock_generate_with_result(**kwargs):
+ def fake_generate_with_result(**kwargs):
calls.append(kwargs)
if len(calls) == 1:
raise IncompleteJSONError("length", '{"partial":')
- assert kwargs["temperature"] == expected_retry_temperature
- return {"text": "ok", "usage": {"input_tokens": 1, "output_tokens": 1}}
+ return {"text": '{"ok": true}', "usage": {}}
- def fail_backup(_agent_type):
- raise AssertionError("local retry must not consult cloud backup")
-
- monkeypatch.setattr(
- "solstone.think.talent.key_to_context", lambda _name: "talent.system.default"
- )
monkeypatch.setattr(
- "solstone.think.models.generate_with_result", mock_generate_with_result
+ "solstone.think.models.generate_with_result",
+ fake_generate_with_result,
)
- monkeypatch.setattr("solstone.think.models.get_backup_provider", fail_backup)
- config = {
- "name": "chat",
- "provider": "local",
- "model": LOCAL_MODEL,
- "prompt": "hello",
- "health_stale": False,
- }
- if base_temperature is not None:
- config["temperature"] = base_temperature
-
- asyncio.run(_execute_generate(config, events.append))
+ events = []
+ await _execute_generate(
+ _generate_config(provider="local", model=LOCAL_MODEL),
+ events.append,
+ )
assert len(calls) == 2
- assert calls[0]["temperature"] == (base_temperature or 0.3)
- assert calls[1]["temperature"] == expected_retry_temperature
assert calls[1]["inference_retry_index"] == 1
assert "local_exclusive_admission" not in calls[1]
- assert not any(e.get("event") == "fallback" for e in events)
assert events[-1]["event"] == "finish"
- assert events[-1]["retries"] == 1
-
-def test_execute_generate_local_capacity_retry_succeeds(monkeypatch):
- from solstone.think.talents import _execute_generate
- events = []
+@pytest.mark.asyncio
+async def test_execute_generate_local_capacity_retry_succeeds(monkeypatch):
calls = []
- def mock_generate_with_result(**kwargs):
+ def fake_generate_with_result(**kwargs):
calls.append(kwargs)
if len(calls) == 1:
raise LocalCapacityExhausted()
- return {"text": "ok", "usage": {"input_tokens": 1, "output_tokens": 1}}
-
- def fail_backup(_agent_type):
- raise AssertionError("local capacity retry must not consult cloud backup")
+ return {"text": "ok", "usage": {}}
monkeypatch.setattr(
- "solstone.think.talent.key_to_context", lambda _name: "talent.system.default"
+ "solstone.think.models.generate_with_result",
+ fake_generate_with_result,
)
- monkeypatch.setattr(
- "solstone.think.models.generate_with_result", mock_generate_with_result
- )
- monkeypatch.setattr("solstone.think.models.get_backup_provider", fail_backup)
-
- config = {
- "name": "chat",
- "provider": "local",
- "model": LOCAL_MODEL,
- "prompt": "hello",
- "health_stale": False,
- }
- asyncio.run(_execute_generate(config, events.append))
+ events = []
+ await _execute_generate(
+ _generate_config(provider="local", model=LOCAL_MODEL),
+ events.append,
+ )
assert len(calls) == 2
- assert "inference_retry_index" not in calls[0]
- assert "local_exclusive_admission" not in calls[0]
assert calls[1]["inference_retry_index"] == 1
assert calls[1]["local_exclusive_admission"] is True
- assert calls[0]["temperature"] == 0.3
- assert calls[1]["temperature"] == calls[0]["temperature"]
- assert not any(e.get("event") == "fallback" for e in events)
- assert not any(e.get("event") == "error" for e in events)
assert events[-1]["event"] == "finish"
- assert events[-1]["retries"] == 1
-
-def test_main_async_local_capacity_retry_second_failure_emits_one_error(
- monkeypatch,
- capsys,
-):
- from solstone.think import talents
- ndjson_input = json.dumps({"name": "chat", "prompt": "hello"})
+@pytest.mark.asyncio
+async def test_execute_generate_local_non_retry_error_propagates(monkeypatch):
calls = []
- def mock_generate_with_result(**kwargs):
+ def fake_generate_with_result(**kwargs):
calls.append(kwargs)
- raise LocalCapacityExhausted()
-
- def fail_backup(_agent_type):
- raise AssertionError("local capacity retry must not consult cloud backup")
-
- mock_args = MagicMock()
- mock_args.verbose = False
- mock_args.dry_run = False
- mock_args.subcommand = None
-
- config = {
- "type": "generate",
- "name": "chat",
- "provider": "local",
- "model": LOCAL_MODEL,
- "prompt": "hello",
- "health_stale": False,
- }
+ raise RuntimeError("not retryable")
- monkeypatch.setattr("sys.stdin", StringIO(ndjson_input))
- monkeypatch.setattr("solstone.think.talents.setup_cli", lambda _parser: mock_args)
- monkeypatch.setattr(
- "solstone.think.talents.setup_logging",
- lambda _verbose=False: MagicMock(),
- )
- monkeypatch.setattr(
- "solstone.think.talents.prepare_config", lambda _request: config
- )
- monkeypatch.setattr("solstone.think.talents.validate_config", lambda _config: None)
- monkeypatch.setattr("solstone.think.talents._run_pre_hooks", lambda _config: {})
- monkeypatch.setattr(
- "solstone.think.talent.key_to_context", lambda _name: "talent.system.default"
- )
- monkeypatch.setattr(
- "solstone.think.models.generate_with_result", mock_generate_with_result
- )
- monkeypatch.setattr("solstone.think.models.get_backup_provider", fail_backup)
monkeypatch.setattr(
- "solstone.think._extraction_utils.log_extraction_failure",
- lambda _exc, _name: None,
+ "solstone.think.models.generate_with_result",
+ fake_generate_with_result,
)
- asyncio.run(talents.main_async())
-
- lines = [line for line in capsys.readouterr().out.splitlines() if line.strip()]
- events = [json.loads(line) for line in lines]
- error_events = [event for event in events if event.get("event") == "error"]
- finish_events = [event for event in events if event.get("event") == "finish"]
-
- assert len(calls) == 2
- assert "inference_retry_index" not in calls[0]
- assert "local_exclusive_admission" not in calls[0]
- assert calls[1]["inference_retry_index"] == 1
- assert calls[1]["local_exclusive_admission"] is True
- assert calls[1]["temperature"] == calls[0]["temperature"]
- assert not any(e.get("event") == "fallback" for e in events)
- assert len(error_events) == 1
- assert error_events[0]["reason_code"] == "local_capacity_exhausted"
- assert error_events[0]["retries"] == 1
- assert finish_events == []
-
-
-def test_execute_generate_local_length_retry_success_writes_once_and_runs_hook_once(
- tmp_path, monkeypatch
-):
- from solstone.think import models, talents
- from solstone.think.talents import _execute_generate
-
- output_path = tmp_path / "out.json"
- events = []
- generate_calls = []
- hook_calls = []
-
- def mock_generate_with_result(**kwargs):
- generate_calls.append(kwargs)
- if len(generate_calls) == 1:
- raise IncompleteJSONError("length", '{"partial":')
- return {
- "text": '{"summary": "ok"}',
- "usage": {"input_tokens": 1, "output_tokens": 2},
- }
-
- def post_hook(result, config):
- hook_calls.append((result, config["name"]))
- return result
-
- def fail_backup(_agent_type):
- raise AssertionError("local retry must not consult cloud backup")
-
- monkeypatch.setattr(
- "solstone.think.talent.key_to_context", lambda _name: "talent.system.default"
- )
- monkeypatch.setattr(models, "generate_with_result", mock_generate_with_result)
- monkeypatch.setattr(models, "get_backup_provider", fail_backup)
- monkeypatch.setattr(talents, "load_post_hook", lambda _config: post_hook)
-
- asyncio.run(
- _execute_generate(
- {
- "name": "chat",
- "provider": "local",
- "model": LOCAL_MODEL,
- "prompt": "hello",
- "health_stale": False,
- "output": "json",
- "output_path": str(output_path),
- "hook": {"post": "test"},
- "json_schema": {
- "type": "object",
- "additionalProperties": False,
- "required": ["summary"],
- "properties": {"summary": {"type": "string"}},
- },
- },
- events.append,
+ with pytest.raises(RuntimeError, match="not retryable"):
+ await _execute_generate(
+ _generate_config(provider="local", model=LOCAL_MODEL),
+ lambda _event: None,
)
- )
-
- assert len(generate_calls) == 2
- assert hook_calls == [('{"summary": "ok"}', "chat")]
- assert output_path.read_text(encoding="utf-8") == '{"summary": "ok"}'
- assert [path.name for path in tmp_path.iterdir()] == ["out.json"]
- assert events[-1]["event"] == "finish"
- assert events[-1]["retries"] == 1
-
-
-def test_main_async_local_length_retry_second_failure_emits_one_error(
- monkeypatch, capsys
-):
- from solstone.think import talents
-
- ndjson_input = json.dumps({"name": "chat", "prompt": "hello"})
- calls = {"count": 0}
-
- def mock_generate_with_result(**_kwargs):
- calls["count"] += 1
- raise IncompleteJSONError("length", '{"partial":')
-
- mock_args = MagicMock()
- mock_args.verbose = False
- mock_args.dry_run = False
- mock_args.subcommand = None
-
- config = {
- "type": "generate",
- "name": "chat",
- "provider": "local",
- "model": LOCAL_MODEL,
- "prompt": "hello",
- "health_stale": False,
- }
-
- monkeypatch.setattr("sys.stdin", StringIO(ndjson_input))
- monkeypatch.setattr("solstone.think.talents.setup_cli", lambda _parser: mock_args)
- monkeypatch.setattr(
- "solstone.think.talents.setup_logging",
- lambda _verbose=False: MagicMock(),
- )
- monkeypatch.setattr(
- "solstone.think.talents.prepare_config", lambda _request: config
- )
- monkeypatch.setattr("solstone.think.talents.validate_config", lambda _config: None)
- monkeypatch.setattr("solstone.think.talents._run_pre_hooks", lambda _config: {})
- monkeypatch.setattr(
- "solstone.think.talent.key_to_context", lambda _name: "talent.system.default"
- )
- monkeypatch.setattr(
- "solstone.think.models.generate_with_result", mock_generate_with_result
- )
- monkeypatch.setattr(
- "solstone.think._extraction_utils.log_extraction_failure",
- lambda _exc, _name: None,
- )
-
- asyncio.run(talents.main_async())
-
- lines = [line for line in capsys.readouterr().out.splitlines() if line.strip()]
- events = [json.loads(line) for line in lines]
- error_events = [event for event in events if event.get("event") == "error"]
- finish_events = [event for event in events if event.get("event") == "finish"]
-
- assert calls["count"] == 2
- assert len(error_events) == 1
- assert error_events[0]["reason_code"] == "incomplete_json_length"
- assert error_events[0]["retries"] == 1
- assert finish_events == []
-
-
-@pytest.mark.parametrize(
- "exc",
- [
- IncompleteJSONError("safety", '{"partial":'),
- RuntimeError("binary_missing"),
- ],
-)
-def test_execute_generate_local_non_length_errors_do_not_retry(monkeypatch, exc):
- from solstone.think.talents import _execute_generate
-
- events = []
- calls = {"count": 0}
-
- def mock_generate_with_result(**_kwargs):
- calls["count"] += 1
- raise exc
-
- def fail_backup(_agent_type):
- raise AssertionError("local failure must not consult cloud backup")
-
- monkeypatch.setattr(
- "solstone.think.talent.key_to_context", lambda _name: "talent.system.default"
- )
- monkeypatch.setattr(
- "solstone.think.models.generate_with_result", mock_generate_with_result
- )
- monkeypatch.setattr("solstone.think.models.get_backup_provider", fail_backup)
-
- config = {
- "name": "chat",
- "provider": "local",
- "model": LOCAL_MODEL,
- "prompt": "hello",
- "health_stale": False,
- }
-
- with pytest.raises(type(exc)):
- asyncio.run(_execute_generate(config, events.append))
-
- assert calls["count"] == 1
- assert not any(e.get("event") == "fallback" for e in events)
-
-
-def test_execute_generate_cloud_incomplete_json_preserves_existing_nonlocal_path(
- monkeypatch,
-):
- from solstone.think.talents import _execute_generate
-
- events = []
- calls = {"count": 0}
-
- def mock_generate_with_result(**_kwargs):
- calls["count"] += 1
- raise IncompleteJSONError("length", '{"partial":')
-
- def fail_backup(_agent_type):
- raise AssertionError("IncompleteJSONError is ValueError; no fallback today")
-
- monkeypatch.setattr(
- "solstone.think.talent.key_to_context", lambda _name: "talent.system.default"
- )
- monkeypatch.setattr(
- "solstone.think.models.generate_with_result", mock_generate_with_result
- )
- monkeypatch.setattr("solstone.think.models.get_backup_provider", fail_backup)
-
- config = {
- "name": "chat",
- "provider": "google",
- "model": "gemini-3-flash-preview",
- "prompt": "hello",
- "health_stale": False,
- }
-
- with pytest.raises(IncompleteJSONError):
- asyncio.run(_execute_generate(config, events.append))
-
- assert calls["count"] == 1
- assert not any(e.get("event") == "fallback" for e in events)
-
-
-def test_on_failure_retry_generate(monkeypatch):
- from solstone.think.talents import _execute_generate
-
- events = []
- calls = {"count": 0}
-
- def mock_generate_with_result(**kwargs):
- calls["count"] += 1
- if calls["count"] == 1:
- raise RuntimeError("primary generate failed")
- assert kwargs.get("provider") == "anthropic"
- assert kwargs.get("model") == "claude-sonnet-4-5"
- return {"text": "backup text", "usage": {"input_tokens": 1, "output_tokens": 1}}
-
- monkeypatch.setattr(
- "solstone.think.talent.key_to_context", lambda _name: "talent.system.default"
- )
- monkeypatch.setattr(
- "solstone.think.models.generate_with_result", mock_generate_with_result
- )
- monkeypatch.setattr(
- "solstone.think.models.get_backup_provider", lambda _type: "anthropic"
- )
- monkeypatch.setattr(
- "solstone.think.models.resolve_model_for_provider",
- lambda _context, _provider, _type="generate": "claude-sonnet-4-5",
- )
- monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
- config = {
- "name": "chat",
- "provider": "google",
- "model": "gemini-3-flash-preview",
- "prompt": "hello",
- "health_stale": False,
- }
-
- asyncio.run(_execute_generate(config, events.append))
-
- assert calls["count"] == 2
- assert config["provider"] == "anthropic"
- assert config["fallback_from"] == "google"
- assert any(e.get("event") == "fallback" for e in events)
- assert events[-1]["event"] == "finish"
- assert events[-1]["result"] == "backup text"
-
-
-def test_on_failure_no_retry_value_error(monkeypatch):
- from solstone.think.talents import _execute_generate
-
- events = []
- assert _is_retryable_error(ValueError("bad input")) is False
-
- def bad_generate(**_kwargs):
- raise ValueError("bad input")
-
- monkeypatch.setattr(
- "solstone.think.talent.key_to_context", lambda _name: "talent.system.default"
- )
- monkeypatch.setattr("solstone.think.models.generate_with_result", bad_generate)
-
- config = {
- "name": "chat",
- "provider": "google",
- "model": "gemini-3-flash-preview",
- "prompt": "hello",
- "health_stale": False,
- }
-
- with pytest.raises(ValueError, match="bad input"):
- asyncio.run(_execute_generate(config, events.append))
-
- assert not any(e.get("event") == "fallback" for e in events)
-
-
-def test_talent_hook_error_is_non_retryable():
- exc = TalentHookError("post", "broken", "chat", RuntimeError("boom"))
-
- assert _is_retryable_error(exc) is False
- assert _should_fallback(exc) is False
-
-
-def test_on_failure_both_fail_raises_original(monkeypatch):
- from solstone.think.talents import _execute_generate
-
- events = []
- calls = {"count": 0}
-
- def always_fail(**kwargs):
- calls["count"] += 1
- if kwargs.get("provider") == "anthropic":
- raise RuntimeError("backup failed")
- raise RuntimeError("primary failed")
-
- monkeypatch.setattr(
- "solstone.think.talent.key_to_context", lambda _name: "talent.system.default"
- )
- monkeypatch.setattr("solstone.think.models.generate_with_result", always_fail)
- monkeypatch.setattr(
- "solstone.think.models.get_backup_provider", lambda _type: "anthropic"
- )
- monkeypatch.setattr(
- "solstone.think.models.resolve_model_for_provider",
- lambda _context, _provider, _type="generate": "claude-sonnet-4-5",
- )
- monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key")
-
- config = {
- "name": "chat",
- "provider": "google",
- "model": "gemini-3-flash-preview",
- "prompt": "hello",
- "health_stale": False,
- }
-
- with pytest.raises(RuntimeError, match="primary failed"):
- asyncio.run(_execute_generate(config, events.append))
-
- assert calls["count"] == 2
-
-
-def test_fallback_event_emitted():
- from solstone.think.talents import _run_talent
-
- events = []
- config = {
- "type": "cogitate",
- "name": "chat",
- "provider": "anthropic",
- "model": "claude-sonnet-4-5",
- "prompt": "hello",
- "fallback_from": "google",
- }
-
- asyncio.run(_run_talent(config, events.append, dry_run=True))
-
- fallback_events = [e for e in events if e.get("event") == "fallback"]
- assert len(fallback_events) == 1
- assert fallback_events[0]["reason"] == "preflight"
-
-
-def test_run_talent_refresh_bypasses_output_exists_guard(tmp_path, monkeypatch):
- from solstone.think import talents
-
- out = tmp_path / "out"
- out.write_text("STALE", encoding="utf-8")
- events = []
- called = {"execute": False}
-
- async def fake_execute(config, emit_event):
- called["execute"] = True
- emit_event({"event": "finish", "ts": 0, "result": "FRESH"})
-
- monkeypatch.setattr(talents, "_execute_with_tools", fake_execute)
- monkeypatch.setattr(talents, "_run_pre_hooks", lambda config: {})
-
- config = {
- "type": "cogitate",
- "name": "alpha",
- "provider": "google",
- "model": "x",
- "prompt": "hi",
- "output_path": str(out),
- "refresh": True,
- }
-
- asyncio.run(talents._run_talent(config, events.append, dry_run=False))
-
- finish_events = [event for event in events if event.get("event") == "finish"]
- assert called["execute"] is True
- assert finish_events[-1]["result"] == "FRESH"
-
-
-def test_run_talent_regenerates_existing_output_without_provenance(
- tmp_path, monkeypatch
-):
- from solstone.think import talents
-
- out = tmp_path / "out"
- out.write_text("STALE", encoding="utf-8")
- events = []
- called = {"execute": False}
-
- async def fake_execute(config, emit_event):
- called["execute"] = True
- emit_event({"event": "finish", "ts": 0, "result": "FRESH"})
-
- monkeypatch.setattr(talents, "_execute_with_tools", fake_execute)
- monkeypatch.setattr(talents, "_run_pre_hooks", lambda config: {})
-
- config = {
- "type": "cogitate",
- "name": "alpha",
- "provider": "google",
- "model": "x",
- "prompt": "hi",
- "output_path": str(out),
- }
-
- asyncio.run(talents._run_talent(config, events.append, dry_run=False))
-
- finish_events = [event for event in events if event.get("event") == "finish"]
- assert called["execute"] is True
- assert finish_events[-1]["result"] == "FRESH"
-
-
-def test_recheck_requested_on_stale(monkeypatch):
- from solstone.think.talents import _execute_with_tools
-
- async def pass_cogitate(*_args, **kwargs):
- on_event = kwargs.get("on_event")
- if on_event:
- on_event({"event": "finish", "result": "ok"})
- return "ok"
-
- recheck_mock = MagicMock()
-
- monkeypatch.setattr("solstone.think.providers.PROVIDER_REGISTRY", {"google": "x"})
- monkeypatch.setattr(
- "solstone.think.providers.get_provider_module",
- lambda _provider: SimpleNamespace(run_cogitate=pass_cogitate),
- )
- monkeypatch.setattr("solstone.think.models.request_health_recheck", recheck_mock)
-
- config = {
- "provider": "google",
- "model": "gemini-3-flash-preview",
- "health_stale": True,
- }
-
- asyncio.run(_execute_with_tools(config, lambda _e: None))
-
- recheck_mock.assert_called_once()
- assert config["health_stale"] is False
-
-
-def test_main_async_no_duplicate_error_when_evented(monkeypatch, capsys):
- from solstone.think.talents import main_async
-
- ndjson_input = json.dumps({"name": "chat", "prompt": "hello"})
- monkeypatch.setattr("sys.stdin", StringIO(ndjson_input))
-
- async def fake_run_talent(_config, emit_event, dry_run=False):
- emit_event({"event": "error", "error": "provider failed"})
- exc = RuntimeError("provider failed")
- setattr(exc, "_evented", True)
- raise exc
-
- mock_args = MagicMock()
- mock_args.verbose = False
- mock_args.dry_run = False
- mock_args.subcommand = None
-
- monkeypatch.setattr("solstone.think.talents.setup_cli", lambda _parser: mock_args)
- monkeypatch.setattr(
- "solstone.think.talents.setup_logging",
- lambda _verbose=False: MagicMock(),
- )
- monkeypatch.setattr(
- "solstone.think.talents.prepare_config", lambda _request: {"type": "cogitate"}
- )
- monkeypatch.setattr("solstone.think.talents.validate_config", lambda _config: None)
- monkeypatch.setattr("solstone.think.talents._run_talent", fake_run_talent)
-
- asyncio.run(main_async())
-
- lines = [line for line in capsys.readouterr().out.splitlines() if line.strip()]
- events = [json.loads(line) for line in lines]
- error_events = [event for event in events if event.get("event") == "error"]
- assert len(error_events) == 1
-
-
-def test_main_async_cogitate_hook_error_no_fallback_no_double_emit(
- monkeypatch,
- capsys,
-):
- from solstone.think import talents
-
- ndjson_input = json.dumps({"name": "chat", "prompt": "hello"})
- monkeypatch.setattr("sys.stdin", StringIO(ndjson_input))
-
- provider_calls = []
-
- async def fake_run_cogitate(config, on_event):
- provider_calls.append(config["provider"])
- on_event({"event": "finish", "result": "provider result"})
-
- def broken_post_hook(result, context):
- raise RuntimeError("hook exploded")
-
- mock_args = MagicMock()
- mock_args.verbose = False
- mock_args.dry_run = False
- mock_args.subcommand = None
-
- config = {
- "type": "cogitate",
- "name": "chat",
- "provider": "google",
- "model": "gemini-3-flash-preview",
- "tier": "flash",
- "prompt": "hello",
- "hook": {"post": "broken_hook"},
- }
-
- monkeypatch.setattr("solstone.think.talents.setup_cli", lambda _parser: mock_args)
- monkeypatch.setattr(
- "solstone.think.talents.setup_logging",
- lambda _verbose=False: MagicMock(),
- )
- monkeypatch.setattr(
- "solstone.think.talents.prepare_config", lambda _request: config
- )
- monkeypatch.setattr("solstone.think.talents.validate_config", lambda _config: None)
- monkeypatch.setattr(
- "solstone.think.talents.load_post_hook", lambda _config: broken_post_hook
- )
- monkeypatch.setattr(
- "solstone.think.providers.PROVIDER_REGISTRY",
- {"google": object(), "anthropic": object()},
- )
- monkeypatch.setattr(
- "solstone.think.providers.get_provider_module",
- lambda _provider: SimpleNamespace(run_cogitate=fake_run_cogitate),
- )
- monkeypatch.setenv("ANTHROPIC_API_KEY", "x")
-
- asyncio.run(talents.main_async())
-
- lines = [line for line in capsys.readouterr().out.splitlines() if line.strip()]
- events = [json.loads(line) for line in lines]
- error_events = [event for event in events if event.get("event") == "error"]
-
- assert provider_calls == ["google"]
- assert [event for event in events if event.get("event") == "fallback"] == []
- assert [event for event in events if event.get("event") == "finish"] == []
- assert len(error_events) == 1
- assert error_events[0]["reason_code"] == "hook_error"
- assert error_events[0]["terminal"] is True
- assert (
- "post-hook 'broken_hook' failed for talent 'chat'" in error_events[0]["error"]
- )
- assert "hook exploded" in error_events[0]["error"]
+ assert len(calls) == 1
diff --git a/tests/test_think_segment_prephase.py b/tests/test_think_segment_prephase.py
index 772d4efd1..42063da0d 100644
--- a/tests/test_think_segment_prephase.py
+++ b/tests/test_think_segment_prephase.py
@@ -1003,7 +1003,6 @@ def test_json_cache_reuse_requires_current_schema_validation(
output_size=output_size,
provider="google",
model="x",
- fallback_from=None,
generation_params=talents._generation_params(config),
completed_at_ms=100,
use_id="seed",
diff --git a/tests/test_thinking_call_parity.py b/tests/test_thinking_call_parity.py
index 76afeb40e..d635bacbf 100644
--- a/tests/test_thinking_call_parity.py
+++ b/tests/test_thinking_call_parity.py
@@ -379,8 +379,8 @@ def test_providers_show_human_and_set_errors(
thinking_call.app,
["providers", "set-generate", "--tier", "9"],
)
- assert bad_tier.exit_code == 1
- assert bad_tier.stderr == "Invalid tier: 9. Must be one of: 1, 2, 3\n"
+ assert bad_tier.exit_code != 0
+ assert "No such option: --tier" in bad_tier.stderr
def test_local_endpoint_verbs_use_http_shapes(
diff --git a/tests/test_thinking_defaults.py b/tests/test_thinking_defaults.py
index 096d6959c..fd03cce82 100644
--- a/tests/test_thinking_defaults.py
+++ b/tests/test_thinking_defaults.py
@@ -95,16 +95,8 @@ def test_default_segment_workers_nonlocal_cpu_formula(monkeypatch):
# --- AC4 -------------------------------------------------------------------
-def test_default_segment_workers_mixed_segment_context_local_caps(
- monkeypatch, tmp_path
-):
- """A cloud default plus one segment talent pinned local still caps the fan-out.
-
- Resolution runs for real here — no monkeypatched predicate. Under
- default-provider-only semantics the default provider is google, so this
- would return the CPU formula (6) instead of the floor tier's 1. That
- difference is the point of the test.
- """
+def test_default_segment_workers_ignores_context_local_pin(monkeypatch, tmp_path):
+ """A retired context-local pin does not cap segment fan-out."""
_write_journal_config(
monkeypatch,
tmp_path,
@@ -116,7 +108,7 @@ def test_default_segment_workers_mixed_segment_context_local_caps(
_pin_cpu_count(monkeypatch, 12)
_pin_slots(monkeypatch, FLOOR_SLOTS)
- assert think._default_segment_workers() == FLOOR_SLOTS == 1
+ assert think._default_segment_workers() == 6
# --- AC6 -------------------------------------------------------------------
@@ -250,8 +242,7 @@ def test_describe_default_byo_endpoint_uses_configured_slot_cap_and_does_not_pro
monkeypatch,
tmp_path,
{
- "generate": {"provider": "google"},
- "contexts": {think.FRAME_CONTEXT: {"provider": "local"}},
+ "generate": {"provider": "local"},
"local": {
"endpoint_url": "https://example.invalid/v1",
"served_model_id": "some-model",
@@ -273,8 +264,7 @@ def test_describe_default_confidential_endpoint_uses_cpu_formula_and_does_not_pr
monkeypatch,
tmp_path,
{
- "generate": {"provider": "google"},
- "contexts": {think.FRAME_CONTEXT: {"provider": "local"}},
+ "generate": {"provider": "local"},
"local": {
"endpoint_url": "https://example.invalid/v1",
"served_model_id": "some-model",