diff --git a/AGENTS.md b/AGENTS.md index c64363b35..6166ac3c0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -207,7 +207,6 @@ Each domain has exactly **one** write-owning module (or one tightly-scoped famil | Hosted backup binding (`backup/hosted/binding.json`) | `solstone/think/backup/hosted.py` | | Convey config (`config/convey.json`) | `solstone/convey/config.py` + `solstone/think/facets.py` | | Chat config (`config/chat.json`) | `solstone/apps/chat/config.py` | -| Vertex credentials (`.config/vertex-credentials.json`) | `solstone/apps/thinking/vertex_credentials.py` | | Speaker labels (`chronicle/**/talents/speaker_labels.json`) | `solstone/apps/speakers/attribution.py` | | Speaker corrections (`chronicle/**/talents/speaker_corrections.json`) | `solstone/apps/speakers/attribution.py` | | Stream identity (`chronicle/**//stream.json` marker + `streams/.json` state) | `solstone/think/streams.py` | diff --git a/CHANGELOG.md b/CHANGELOG.md index 55ecd5659..a39916ebe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ Format adapted from [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), al ## [Unreleased] +### Changed + +- thinking now has one active brain for every task: bundled local by default, a personal OpenAI, Anthropic, or Google AI Studio key, or an owner-supplied OpenAI-compatible endpoint. legacy split lanes, tier and talent routing, Vertex support, and duplicate cloud-provider adapters have been removed. + ## [0.8.7] - 2026-07-15 ### Added diff --git a/docs/CORTEX.md b/docs/CORTEX.md index e0bbd4e97..954da31b3 100644 --- a/docs/CORTEX.md +++ b/docs/CORTEX.md @@ -280,25 +280,18 @@ The JSON frontmatter for an agent can include: ### Model Resolution -Models are resolved automatically by interface: -1. `providers.generate.provider/model` or `providers.cogitate.provider/model` - pins the active brain when present. -2. If no provider is pinned, managed cloud-key presence is honored in the - grandfathered `google` -> `anthropic` -> `openai` order. -3. If no cloud key is configured and the local runtime is ready, `local` is used. -4. If no brain is available, the request fails closed. - -`providers.contexts` no longer selects provider/model. Its `disabled` and -`extract` fields remain live talent metadata. +Generate and cogitate use the single explicit `providers.active` provider/model +selected in the Thinking app. If it is missing or invalid, the request fails +closed. Key presence, tiers, backup maps, and talent frontmatter never select a +different provider or model. Talent `disabled` and `extract` metadata lives in +the top-level `talent_overrides` map. ## Agent Providers The system supports multiple provider identities through `solstone/think/providers/__init__.py`: -- **OpenAI** (`solstone/think/providers/openhands.py`): GPT cogitate via OpenHands; generate redispatched to `solstone/think/providers/openai.py` -- **Google** (`solstone/think/providers/openhands.py`): Gemini cogitate via OpenHands; generate redispatched to `solstone/think/providers/google.py` -- **Anthropic** (`solstone/think/providers/openhands.py`): Claude cogitate via OpenHands; generate redispatched to `solstone/think/providers/anthropic.py` +- **OpenAI, Google AI Studio, and Anthropic** (`solstone/think/providers/openhands.py`): one OpenHands/LiteLLM transport for generate and cogitate - **Local** (`solstone/think/providers/local.py`): bundled llama-server, BYO OpenAI-compatible endpoint, or confidential local endpoint Effective providers: diff --git a/docs/PROVIDERS.md b/docs/PROVIDERS.md index 6ade0054e..d8b51d423 100644 --- a/docs/PROVIDERS.md +++ b/docs/PROVIDERS.md @@ -1,233 +1,168 @@ -# Provider Architecture - -This guide describes the provider behavior that ships today. The core code paths -are `solstone/think/models.py`, `solstone/think/talents.py`, -`solstone/think/providers/__init__.py`, `solstone/think/providers/openhands.py`, -and `solstone/think/providers/local.py`. +# Thinking Provider Architecture -For the broader think pipeline, see `docs/THINK.md`. - -## Active Brain Resolution - -Solstone runs one active provider/model pair per interface: - -- `generate` for single-shot model calls and generators. -- `cogitate` for tool-using OpenHands runs. - -`solstone/think/models.py` owns active-brain resolution. `resolve_provider()` -checks `providers..provider` first, then managed cloud-key presence in -the grandfathered `google` -> `anthropic` -> `openai` order, then local runtime -readiness, then the no-brain state. `providers..model` can pin the -model for that interface. Without a model pin, each provider has one default -model in `DEFAULT_MODEL_BY_PROVIDER`. - -`resolve_provider()` intentionally ignores retired routing keys: `tier`, -`backup`, and `providers.models`. `providers.contexts` no longer steers -provider/model routing either. Its `disabled` and `extract` fields are still live -talent metadata because `solstone/think/talent.py` merges exactly those fields -from `providers.contexts.`. - -## Provider Registry - -`solstone/think/providers/__init__.py` is the registry. Cloud provider names -`google`, `openai`, and `anthropic` all resolve to the OpenHands facade module, -`solstone.think.providers.openhands`; `local` resolves to -`solstone.think.providers.local`. - -The effective provider modules expose the interface that `models.py` and -`talents.py` call: - -- `run_generate()` returns a `GenerateResult`. -- `run_agenerate()` returns a `GenerateResult` from async callers. -- `run_cogitate()` runs a tool-using conversation and emits events. - -The cloud vendor leaf modules `solstone/think/providers/google.py`, -`solstone/think/providers/openai.py`, and -`solstone/think/providers/anthropic.py` implement generate/agenerate only. They -are not cogitate providers. The OpenHands facade owns cloud cogitate. - -## Generate Dispatch - -`solstone/think/models.py` resolves the active generate brain, prepares the -provider-facing schema, calls `provider_mod.run_generate()` or -`provider_mod.run_agenerate()`, and logs usage centrally. - -For cloud generate, `provider_mod` is the OpenHands facade. The facade keeps -`run_generate()` and `run_agenerate()` as redispatchers: at call time it imports -the vendor leaf from `_GENERATE_MODULES` in -`solstone/think/providers/openhands.py` and calls the matching leaf function. -This keeps vendor-specific transport behavior in the leaf modules while leaving -the registry stable. - -The native cloud leaves remain intentionally thin transport adapters. Solstone -keeps them because the OpenAI-compatible surfaces do not provide full parity for -the behavior the generate path needs, including strict/constrained schema support -and native-provider response details. Anthropic, OpenAI, and Google each keep -their provider-specific generate implementation in their own module. - -For local generate, `solstone/think/providers/local.py` owns both bundled-local -and configured endpoint traffic. Bundled local posts to the supervisor-owned -loopback llama-server. A configured local endpoint posts to the owner-supplied -OpenAI-compatible URL resolved by `solstone/think/providers/local_endpoint.py`. - -## Cogitate Dispatch - -`solstone/think/talents.py` runs cogitate talents through -`_execute_with_tools()`. It resolves the provider module through -`solstone/think/providers/__init__.py` and calls `run_cogitate()`. - -For cloud cogitate, `solstone/think/providers/openhands.py` builds and runs the -OpenHands conversation. For local cogitate, -`solstone/think/providers/local.py` performs local readiness/admission work and -then delegates to `openhands.run_cogitate()` with a local OpenAI-compatible -configuration. This is why local cogitate failures are classified in -`local.py`, not by a separate local tool-calling engine. - -## Local Lanes - -The owner-facing provider key is `local`. It has three runtime lanes: - -- Bundled local: `solstone/think/providers/local_server.py` manages the - loopback llama-server process and `solstone/think/providers/local.py` sends - OpenAI-compatible requests to it. -- BYO local endpoint: `solstone/think/providers/local_endpoint.py` activates the - endpoint only when both `providers.local.endpoint_url` and - `providers.local.served_model_id` are present. Optional - `providers.local.credential` supplies the bearer token. Optional - `providers.local.parallel_slots` governs non-confidential BYO admission. -- Confidential endpoint: `solstone/think/services/spp_transport.py` gates - confidential egress and `solstone/think/providers/local.py` uses that transport - before provider dispatch. - -Bundled local, BYO URL, and confidential local traffic all use an -OpenAI-compatible request shape. The difference is where the request is sent and -which readiness, admission, and attestation gates run first. - -## Local Admission and Capacity - -The `local` provider has one shared admission boundary for governed local lanes: -the supervisor-owned Qwen server and non-confidential OpenAI-compatible endpoint -overrides. The confidential-processing lane (`services.confidential` present) -and every cloud provider bypass this boundary. Bundled-local inference telemetry -remains bundled-only. - -Capacity remains explicit and intentionally small: - -| Runtime profile | Serving capacity | Evidence | -|---|---:|---| -| Linux floor | 1 | supervisor `ServerTier`; live `/props.total_slots` wins | -| Linux capable (at least 16 GiB tiering VRAM) | 2 | supervisor `ServerTier`; live `/props.total_slots` wins | -| Apple mlx-vlm local backend | 1 | conservative explicit default; mlx-vlm on darwin exposes neither `/props` nor `ServerTier` (`solstone/think/providers/local_server.py:185`) | -| Non-confidential BYO endpoint | configured | journal config `providers.local.parallel_slots`; resolved by `_configured_byo_parallel_slots()` (`solstone/think/providers/local_endpoint.py:68`) after `resolve_local_endpoint()` selects BYO (`solstone/think/providers/local_endpoint.py:84`) | - -For bundled local, the provider memoizes capacity once per process. It first -reads live `/props.total_slots`, then the persisted `health/local.ctx` launch -profile, then uses one slot when neither source is available. The supervisor -remains the configuration owner: -changing a Linux profile's `parallel_slots` changes both -`llama-server --parallel` and provider admission after journal processes restart. -Apple stays at one until that runtime exposes a stable capacity contract and a -separate measurement justifies raising it. - -Admission uses one `flock` file per slot under -`health/local-inference-admission/`. This coordinates independent journal -processes without a scheduler service or in-memory queue. Waiting async calls -are cancellation-safe; exceptions and cancellation release acquired locks; -process exit releases kernel locks. Queue time consumes the caller's existing -provider deadline, so waiting cannot silently extend a request beyond its -configured timeout. Cogitate holds one parent permit across model turns, but -temporarily yields that permit while the OpenHands `sol` tool runs a nested -`sol` child process. The parent reacquires through the same FIFO admission pool -before any further model request; failure to reacquire is a terminal -`local_queue_timeout`. - -Every bundled-local attempt appends a content-free JSON record to -`health/local-inference/YYYYMMDD.jsonl`. These files follow the configured -`retention.journal_logs.days` policy. Records contain request id, timestamp, -kind, provider, logical model, runtime profile, serving capacity, evidence -source, admission slot, client queue wait, timing, token counts, retry index, -finish reason, outcome, timeout/cancellation flags, and a safe reason code on -failure. Records never contain prompt text, generated text, messages, schemas, -images, endpoint URLs, or credentials. - -## Honest Failure Semantics - -Provider failure is not a routing signal. Solstone does not silently switch to -another provider when the active brain fails. - -- Quota failures are recorded by - `solstone/think/providers/state.py::record_quota_failure()` in - `health/talents.json` under the active journal root with provider, model, interface, - `provider_quota_exceeded`, and `reset_at_ms`. -- Local endpoint reachability and contract failures are classified by - `solstone/think/providers/local_endpoint.py` and - `solstone/think/providers/local.py`. -- Local retry is deliberately narrow in `solstone/think/talents.py`: generate - retries once only for `incomplete_json_length` or - `local_capacity_exhausted`, and it retries the same local provider. -- Segment deferral is represented in health JSONL, not by provider switching. - `solstone/think/pipeline_health.py` folds segment progress, and - `solstone/think/thinking.py` selects sensed-but-not-fully-thought segments for - repair. - -If the local runtime, model files, RAM gate, loopback server, BYO endpoint, or -confidential attestation is not ready, Solstone surfaces that recovery reason -instead of falling back to a cloud provider. - -## Live Configuration Keys - -Provider configuration lives in `config/journal.json` under the active journal -root; the canonical reader/writer is `solstone/think/journal_config.py`. - -Live routing keys: - -- `providers.generate.provider` -- `providers.generate.model` -- `providers.cogitate.provider` -- `providers.cogitate.model` - -Live local endpoint keys: +Solstone is local-first software for personal use. It supports one active +provider/model profile, configured in Thinking, and never silently switches +providers. The implementation deliberately has no special Vertex AI, Azure +OpenAI, Bedrock, or other enterprise-cloud integration. -- `providers.local.endpoint_url` -- `providers.local.served_model_id` -- `providers.local.credential` -- `providers.local.parallel_slots` +For the broader pipeline, see `docs/THINK.md`. -Live Google backend keys: +## One Active Brain -- `providers.google_backend`, read by Google provider code for Gemini Developer - API versus Vertex behavior. -- Vertex/ADC credential settings used by `solstone/apps/thinking/routes.py` and - `solstone/apps/thinking/vertex_credentials.py`. +`config/journal.json` stores the selected profile at: -Live managed key storage: +```json +{ + "providers": { + "active": { + "provider": "local", + "model": "local/qwen3.5-4b" + } + } +} +``` + +`solstone/think/models.py::resolve_provider()` is the only runtime resolver. +The `generate` and `cogitate` arguments identify the interface being invoked, +but both resolve the same `providers.active` profile. A missing profile is an +explicit no-brain state. Key presence and local readiness never choose a +provider implicitly. + +Provider and model overrides are rejected in talent frontmatter, cortex +requests, batch requests, and direct generate calls. Thinking is the sole +configuration surface for the active brain. Talent `disabled` and `extract` +controls are separate metadata under `talent_overrides`; they do not route +models. + +## Supported Owner Choices + +The Thinking app exposes five setup choices: + +- Bundled local, using Solstone's installed llama-server or mlx-vlm runtime. +- An owner-supplied OpenAI-compatible URL, model id, and optional bearer key. +- OpenAI with an owner-supplied API key and model id. +- Anthropic with an owner-supplied API key and model id. +- Google AI Studio with an owner-supplied API key and Gemini model id. + +The direct cloud options are convenience presets. The arbitrary endpoint is a +plain compatibility contract: Solstone sends OpenAI-compatible requests, but +does not add vendor-specific support for whatever sits behind that URL. + +Managed personal cloud keys remain journal-local: -- `env.GOOGLE_API_KEY` -- `env.ANTHROPIC_API_KEY` - `env.OPENAI_API_KEY` +- `env.ANTHROPIC_API_KEY` +- `env.GOOGLE_API_KEY` -Retired for provider/model routing: +## Dispatch -- `tier` -- `backup` -- `providers.models` -- `providers.contexts..provider` -- `providers.contexts..model` +`solstone/think/providers/__init__.py` has a deliberately small registry: -`providers.contexts..disabled` and -`providers.contexts..extract` remain live talent metadata. Do not remove -those fields as if the whole `providers.contexts` block were inert. +- `google`, `openai`, and `anthropic` all map to + `solstone/think/providers/openhands.py`. +- `local` maps to `solstone/think/providers/local.py`. -## Adding or Changing Providers +The effective modules implement: -Provider changes should start from the current registry and active-brain model, -not from the retired tier/backup system: +- `run_generate()` for synchronous single-shot generation. +- `run_agenerate()` for asynchronous single-shot generation. +- `run_cogitate()` for tool-using OpenHands conversations. -1. Update `solstone/think/providers/__init__.py` for provider identity and - metadata. -2. Implement the effective module surface that the registry points to. -3. If the provider is cloud generate-only, keep cogitate on the OpenHands facade - and add a vendor leaf only for generate/agenerate. -4. Add one default model in `solstone/think/models.py`. -5. Add focused provider tests and lane-honesty tests under `tests/`. -6. Update `docs/THINK.md`, `docs/CORTEX.md`, and this file. +### Personal cloud + +`openhands.py` is the single cloud transport. It builds an OpenHands `LLM` and +lets LiteLLM translate the request to OpenAI, Anthropic, or Google AI Studio. +It also normalizes text, usage, finish reasons, and thinking blocks back into +Solstone's `GenerateResult`. + +Generate calls explicitly neutralize OpenHands' agent-oriented defaults and +then apply only Solstone's requested behavior. The transport preserves: + +- sync and async calls; +- multimodal message content; +- JSON object and JSON Schema response formats; +- OpenAI reasoning-effort suffixes; +- Anthropic and Gemini thinking budgets; +- normalized usage, resolved model, and finish reason. + +Direct OpenAI generation uses the Responses API. Anthropic and Google use chat +completion through LiteLLM's provider translation. Key/model validation sends +a tiny request through this same runtime path, so validation can incur a small +provider charge. + +OpenHands/LiteLLM may internally contain code for many providers. That does not +make them Solstone-supported providers: Solstone exposes no registry entry, +config, UI, credential flow, or validation path for enterprise integrations. + +### Local and arbitrary endpoints + +`local.py` remains a thin product-policy wrapper rather than a second general +cloud adapter. It owns guarantees that OpenHands alone does not provide: + +- bundled runtime installation and readiness; +- context-budget fitting and local schema preparation; +- Qwen sampling and chat-template controls; +- cross-process local admission and bounded retry; +- content-free local inference telemetry; +- confidential egress/attestation gates; +- stable local error classification. + +Bundled local posts to the supervisor-owned loopback server. A configured +endpoint uses: + +- `providers.local.endpoint_url` +- `providers.local.served_model_id` +- `providers.local.credential` (optional) +- `providers.local.parallel_slots` (optional) + +Both generate and cogitate use the endpoint's OpenAI-compatible contract. The +configured logical provider remains `local`, so the same readiness and safety +boundary applies without maintaining vendor-specific adapters. + +## Local Admission + +Bundled local and non-confidential arbitrary endpoints share the governed local +admission boundary. Cloud and confidential processing bypass it. Capacity is +kept intentionally small: one slot on the Linux floor and Apple mlx-vlm, two on +the capable Linux tier, or the explicit `parallel_slots` value for an arbitrary +endpoint. + +Admission uses per-slot `flock` files under +`health/local-inference-admission/`, coordinating independent journal +processes. Queue time consumes the caller's existing timeout. Cogitate yields +its permit while a nested `sol` command runs and reacquires it before the next +model turn. + +Bundled attempts append content-free telemetry to +`health/local-inference/YYYYMMDD.jsonl`. Records include timing, capacity, +token counts, retry index, finish reason, and safe failure codes—never prompts, +responses, schemas, images, URLs, or credentials. + +## Failure Semantics + +Provider failure is not a routing signal. Solstone surfaces the failure and +recovery action for the active profile. + +- Quota failures are recorded in `health/talents.json`. +- Endpoint reachability and contract errors are classified by the local + endpoint wrapper. +- Local generate retries once only for narrow capacity/truncation cases, using + the same provider. +- Missing local runtime, model files, RAM, endpoint readiness, or confidential + attestation fails closed rather than falling back to cloud. + +## Migration Boundary + +The Thinking maintenance task collapses legacy `providers.generate` and +`providers.cogitate` into `providers.active`. If they differ, cogitate wins +because its model already satisfies the tool-capable interface. A key-only +legacy install is materialized once in Google, Anthropic, OpenAI order. The task +selects bundled local when no prior profile or personal cloud key exists. It +also: + +- removes tier, backup, model-map, Google-backend, and Vertex fields; +- deletes the canonical legacy Vertex credential file; +- moves `providers.contexts` enable/extract controls to `talent_overrides`; +- moves Rev.ai/Plaud validation state to `service_key_validation`. + +There are no runtime compatibility shims for the retired shapes. diff --git a/docs/SOLCLI.md b/docs/SOLCLI.md index e6480a9c4..71fe3ace9 100644 --- a/docs/SOLCLI.md +++ b/docs/SOLCLI.md @@ -430,7 +430,7 @@ solstone/ | `transcripts` | `solstone/apps/transcripts/call.py` | list, read, segments | | `support` | `solstone/apps/support/call.py` | register, search, article, create, list, show, reply, attach, feedback, announcements, diagnose | | `sol` | `solstone/apps/sol/call.py` | set-name, reset, set-owner, sol-init | -| `settings` | `solstone/apps/settings/call.py` | keys (show/set/delete), providers show, provider selection, vertex service-account. Provider install moved to `journal install-provider local`. | +| `settings` | `solstone/apps/settings/call.py` | personal service keys (show/set/delete). Thinking provider selection lives in the Thinking app; local provider install lives at `journal install-provider local`. | | `awareness` | `solstone/apps/awareness/call.py` | status, imports, log, log-read | | `journal` | `solstone/think/tools/call.py` | search, events, facets, facet (show/create/update/rename/mute/unmute/delete/merge), news, agents, read, imports, import, retention purge, storage-summary | diff --git a/docs/THINK.md b/docs/THINK.md index 31d638ad2..fb59525fd 100644 --- a/docs/THINK.md +++ b/docs/THINK.md @@ -179,13 +179,11 @@ The `journal providers check` command is an ad-hoc provider check CLI. Cortex do journal providers check [TASK_FILE] [--provider PROVIDER] [--model MODEL] [--max-tokens N] [-o OUT_FILE] ``` -Provider resolution lives in `solstone/think/models.py`. Each interface has one -active brain: explicit `providers.generate` / `providers.cogitate` provider and -model pins win first, then managed cloud-key presence in `google` -> `anthropic` --> `openai` order, then local runtime readiness, then the no-thinking-engine -state. Retired tier, backup, and context provider/model routing keys are ignored -by provider resolution. Configure managed cloud API keys in the `env` section of -`journal/config/journal.json`. The `local` provider requires no API key. +Provider resolution lives in `solstone/think/models.py`. Generate and cogitate +share the single explicit `providers.active` provider/model selected in the +Thinking app. There is no key-presence fallback, tier override, backup route, or +talent-specific provider route. Configure cloud API keys in the `env` section of +`journal/config/journal.json`; the bundled local provider requires no API key. ### Provider modules @@ -198,7 +196,7 @@ to `solstone/think/providers/openhands.py` and maps `local` to - `run_cogitate()` - Tool-calling execution via `sol call` commands and event streaming For direct LLM calls, use `think.models.generate()` or `think.models.agenerate()`; -they route through the active generate brain. +they route through the active brain. ## Generator map keys diff --git a/docs/deletion-sites-inventory.md b/docs/deletion-sites-inventory.md index 43e42be04..98e752e34 100644 --- a/docs/deletion-sites-inventory.md +++ b/docs/deletion-sites-inventory.md @@ -113,13 +113,6 @@ Out of scope for this sweep; keep visible because it is a destructive journal-do | `solstone/apps/import/call.py:401,437,452` | staged entity review file | merge/create/skip entity review resolution | `staged_path` must exist under `state_dir/entities/staged` | yes (`solstone/apps/import/call.py:402-463`) | no | `⚠️` | review-state cleanup after explicit operator resolution | | `solstone/apps/import/call.py:507,583,605` | staged facet review file | skip/apply facet review resolution | `staged_path` must exist under `state_dir/facets/staged` | yes (`solstone/apps/import/call.py:508-615`) | no | `⚠️` | review-state cleanup after explicit operator resolution | -## solstone/apps/settings - -| file:line | target | trigger | path validation | audit log | dry-run | class | why | -| --- | --- | --- | --- | --- | --- | --- | --- | -| `solstone/apps/settings/routes.py:878` | canonical `journal/.config/vertex-credentials.json` | provider update clears Vertex credentials | stored path must resolve to the canonical credential path before unlink | yes (`solstone/apps/settings/routes.py:892-899`) | no | `⚠️` | config artifact cleanup with a canonical-path guard | -| `solstone/apps/settings/call.py:511` | canonical `journal/.config/vertex-credentials.json` | `sol call settings vertex clear` | stored path must resolve to the canonical credential path before unlink | no | no | `⚠️` | CLI config cleanup outside the journal-domain sweep | - ## solstone/apps/support | file:line | target | trigger | path validation | audit log | dry-run | class | why | diff --git a/pyproject.toml b/pyproject.toml index b76ddaba6..4ae2f35ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -127,7 +127,6 @@ journal-host = [ "opentelemetry-instrumentation==0.60b1", "opentelemetry-instrumentation-threading==0.60b1", "opentelemetry-semantic-conventions==0.60b1", - "anthropic", # 0.6.2 = validated continuous-batching-fix baseline; <0.7 pins it # so on-device builds don't float past the soak-validated server-wedge fix. "mlx-vlm>=0.6.2,<0.7; sys_platform == 'darwin' and platform_machine == 'arm64'", diff --git a/scripts/check_journal_io_access.py b/scripts/check_journal_io_access.py index 7c5c03b06..fb1715b58 100644 --- a/scripts/check_journal_io_access.py +++ b/scripts/check_journal_io_access.py @@ -96,7 +96,6 @@ OWNER_FILES: frozenset[str] = frozenset( "solstone/apps/speakers/discovery.py", "solstone/apps/speakers/owner.py", "solstone/apps/speakers/routes.py", - "solstone/apps/thinking/vertex_credentials.py", "solstone/apps/timeline/maintenance.py", "solstone/apps/timeline/talent/segment_summary.py", "solstone/apps/import/ingest.py", diff --git a/solstone/apps/settings/call.py b/solstone/apps/settings/call.py index c6285f448..f421fe81c 100644 --- a/solstone/apps/settings/call.py +++ b/solstone/apps/settings/call.py @@ -23,28 +23,17 @@ _API_KEY_ENV_VARS = [ "REVAI_ACCESS_TOKEN", "PLAUD_ACCESS_TOKEN", ] -_AI_KEY_ENV_VARS = { - "GOOGLE_API_KEY", - "ANTHROPIC_API_KEY", - "OPENAI_API_KEY", -} _SERVICE_KEY_VALIDATION_NAME = { "REVAI_ACCESS_TOKEN": "revai", "PLAUD_ACCESS_TOKEN": "plaud", } app = typer.Typer( - help="Journal settings — keys, providers, transcription, identity, and observer." + help="Journal settings — service keys, transcription, identity, and observer." ) keys_app = typer.Typer(help="API key management.") app.add_typer(keys_app, name="keys") -providers_app = typer.Typer(help="AI provider configuration.") -app.add_typer(providers_app, name="providers") -google_backend_app = typer.Typer(help="Google backend selection.") -app.add_typer(google_backend_app, name="google-backend") -vertex_app = typer.Typer(help="Vertex AI service account credentials.") -app.add_typer(vertex_app, name="vertex-credentials") transcribe_app = typer.Typer(help="Transcription backend configuration.") app.add_typer(transcribe_app, name="transcribe") identity_app = typer.Typer(help="Journal owner identity.") @@ -97,23 +86,12 @@ def _exit_with(message: str) -> None: def _validate_env_var_or_exit(env_var: str) -> None: - if env_var in _AI_KEY_ENV_VARS: - typer.echo( - "Moved to `sol call thinking keys …` — run that instead.", - err=True, - ) - raise typer.Exit(2) if env_var not in _API_KEY_ENV_VARS: _exit_with( f"Invalid env var: {env_var}. Must be one of: {', '.join(_API_KEY_ENV_VARS)}" ) -def _moved_stub(command: str) -> None: - typer.echo(f"Moved to `sol call thinking {command}` — run that instead.", err=True) - raise typer.Exit(2) - - @processing_app.command("show") @convey_cli def processing_show() -> None: @@ -285,7 +263,7 @@ def keys_clear( @convey_cli def keys_validate( cache_result: bool = typer.Option( - False, "--cache-result", help="Persist results to providers.key_validation." + False, "--cache-result", help="Persist service-token validation results." ), ) -> None: """Validate all configured API keys without persisting by default.""" @@ -295,106 +273,6 @@ def keys_validate( _echo_json({"key_validation": response.get("key_validation", {})}) -@providers_app.command("show") -def providers_show( - human: bool = typer.Option(False, "--human", help="Print one-line statuses."), -) -> None: - """Moved to ``sol call thinking providers show``.""" - - _moved_stub("providers show") - - -@providers_app.command("install") -def providers_install( - name: str = typer.Argument(None, help="Provider name."), -) -> None: - """Moved to `journal install-provider`.""" - typer.echo("Moved to `journal install-provider` — run that instead.", err=True) - raise typer.Exit(2) - - -@providers_app.command("set-local-endpoint") -def providers_set_local_endpoint( - url: str = typer.Option(..., "--url", help="OpenAI-compatible endpoint URL."), - model: str = typer.Option(..., "--model", help="Served model id."), - credential: str | None = typer.Option( - None, - "--credential", - help="Optional bearer credential for the endpoint.", - ), -) -> None: - """Moved to ``sol call thinking set-local-endpoint``.""" - - _moved_stub("set-local-endpoint") - - -@providers_app.command("clear-local-endpoint") -def providers_clear_local_endpoint() -> None: - """Moved to ``sol call thinking clear-local-endpoint``.""" - - _moved_stub("clear-local-endpoint") - - -@providers_app.command("set-generate") -def providers_set_generate( - provider: str | None = typer.Option(None, "--provider", help="Primary provider."), -) -> None: - """Moved to ``sol call thinking providers set-generate``.""" - - _moved_stub("providers set-generate") - - -@providers_app.command("set-cogitate") -def providers_set_cogitate( - provider: str | None = typer.Option(None, "--provider", help="Primary provider."), -) -> None: - """Moved to ``sol call thinking providers set-cogitate``.""" - - _moved_stub("providers set-cogitate") - - -@google_backend_app.command("show") -def google_backend_show() -> None: - """Moved to ``sol call thinking google-backend show``.""" - - _moved_stub("google-backend show") - - -@google_backend_app.command("set") -def google_backend_set( - backend: str = typer.Argument(..., help="Google backend to use."), -) -> None: - """Moved to ``sol call thinking google-backend set``.""" - - _moved_stub("google-backend set") - - -@vertex_app.command("show") -def vertex_credentials_show() -> None: - """Moved to ``sol call thinking vertex-credentials show``.""" - - _moved_stub("vertex-credentials show") - - -@vertex_app.command("import") -def vertex_credentials_import( - file_path: str = typer.Argument(..., help="Path to service account JSON."), - skip_validation: bool = typer.Option( - False, "--skip-validation", help="Skip API validation of credentials." - ), -) -> None: - """Moved to ``sol call thinking vertex-credentials import``.""" - - _moved_stub("vertex-credentials import") - - -@vertex_app.command("clear") -def vertex_credentials_clear() -> None: - """Moved to ``sol call thinking vertex-credentials clear``.""" - - _moved_stub("vertex-credentials clear") - - @transcribe_app.command("show") @convey_cli def transcribe_show() -> None: diff --git a/solstone/apps/settings/routes.py b/solstone/apps/settings/routes.py index e8c11b671..9b09b30f1 100644 --- a/solstone/apps/settings/routes.py +++ b/solstone/apps/settings/routes.py @@ -152,12 +152,7 @@ def _compute_runtime_label() -> str: def _service_key_validation(config: dict[str, Any]) -> dict[str, Any]: - providers_config = config.get("providers", {}) - key_validation = ( - providers_config.get("key_validation", {}) - if isinstance(providers_config, dict) - else {} - ) + key_validation = config.get("service_key_validation", {}) if not isinstance(key_validation, dict): key_validation = {} return { @@ -211,6 +206,7 @@ def _project_public_config(config: dict[str, Any]) -> dict[str, Any]: service_validation = _service_key_validation(config) if service_validation: projected["key_validation"] = service_validation + projected.pop("service_key_validation", None) if "env" in projected: projected["env"] = {k: bool(v) for k, v in projected["env"].items()} projected.pop("providers", None) @@ -442,10 +438,7 @@ def update_config() -> Any: config[section][backend_key][nested_key] = new_value if section == "env" and changed_fields: - if "providers" not in config: - config["providers"] = {} - if "key_validation" not in config["providers"]: - config["providers"]["key_validation"] = {} + key_validation = config.setdefault("service_key_validation", {}) # Validate service tokens (Rev.ai, Plaud) — not AI providers, # so they use their own validators instead of think.providers. @@ -469,9 +462,9 @@ def update_config() -> Any: mod = importlib.import_module(module_path) result = mod.validate_token(new_val) result["timestamp"] = datetime.now(timezone.utc).isoformat() - config["providers"]["key_validation"][val_key] = result + key_validation[val_key] = result else: - config["providers"]["key_validation"].pop(val_key, None) + key_validation.pop(val_key, None) write_journal_config(config) @@ -856,8 +849,7 @@ def validate_all_keys() -> Any: with hold_config_lock(): config = get_journal_config() key_validation = _compute_key_validation(config) - providers_config = config.setdefault("providers", {}) - existing = providers_config.setdefault("key_validation", {}) + existing = config.setdefault("service_key_validation", {}) for key in ("revai", "plaud"): existing.pop(key, None) existing.update(key_validation) diff --git a/solstone/apps/settings/tests/conftest.py b/solstone/apps/settings/tests/conftest.py index c8243de05..603a1aa49 100644 --- a/solstone/apps/settings/tests/conftest.py +++ b/solstone/apps/settings/tests/conftest.py @@ -45,20 +45,10 @@ def settings_env(tmp_path, monkeypatch): "OPENAI_API_KEY": "test-openai-key", }, "providers": { - "generate": { + "active": { "provider": "google", "model": "gemini-flash-latest", }, - "cogitate": { - "provider": "openai", - "model": "gpt-5.4-mini", - }, - "auth": { - "google": "api_key", - "openai": "api_key", - "anthropic": "platform", - }, - "google_backend": "auto", "key_validation": {}, }, "transcribe": { diff --git a/solstone/apps/settings/tests/test_call.py b/solstone/apps/settings/tests/test_call.py deleted file mode 100644 index f99def2aa..000000000 --- a/solstone/apps/settings/tests/test_call.py +++ /dev/null @@ -1,35 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""Static settings CLI command tests not covered by HTTP parity.""" - -from __future__ import annotations - -import pytest -from typer.testing import CliRunner - -from solstone.think.call import call_app - -runner = CliRunner() - - -class TestProvidersInstall: - @pytest.mark.parametrize("args", [[], ["local"], ["anthropic"]]) - def test_install_redirects_to_journal_install_provider(self, settings_env, args): - settings_env() - - result = runner.invoke(call_app, ["settings", "providers", "install", *args]) - - assert result.exit_code != 0 - combined = result.output + result.stderr - assert "journal install-provider" in combined - - @pytest.mark.parametrize("verb", ["uninstall", "disable", "enable", "validate-key"]) - @pytest.mark.parametrize("name", ["anthropic", "openai", "openhands"]) - def test_retired_verbs_return_no_such_command(self, settings_env, verb, name): - settings_env() - - result = runner.invoke(call_app, ["settings", "providers", verb, name]) - - assert result.exit_code != 0 - assert "No such command" in (result.output + result.stderr) diff --git a/solstone/apps/settings/workspace.html b/solstone/apps/settings/workspace.html index ab4b3ad43..beb21cf7e 100644 --- a/solstone/apps/settings/workspace.html +++ b/solstone/apps/settings/workspace.html @@ -1109,140 +1109,6 @@ input:checked + .slider:before { text-decoration: underline; } -.provider-card { - border: 1px solid var(--facet-border, #e5e0db); - border-radius: 8px; - padding: 0.85em; - background: #fff; -} - -.provider-card__header { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 0.75em; - margin-bottom: 0.65em; -} - -.provider-card__title { - font-weight: 600; - color: #333; -} - -.provider-card__pills { - display: flex; - gap: 0.4em; - flex-wrap: wrap; - justify-content: flex-end; -} - -.provider-card__badge, -.provider-card__key-pill { - border-radius: 999px; - padding: 0.2em 0.55em; - font-size: 0.75em; - font-weight: 600; - white-space: nowrap; -} - -.provider-card__badge[data-tone="ok"], -.provider-card__key-pill[data-tone="ok"] { - background: #dcfce7; - color: #166534; -} - -.provider-card__badge[data-tone="progress"], -.provider-card__badge[data-tone="warn"], -.provider-card__key-pill[data-tone="progress"], -.provider-card__key-pill[data-tone="warn"] { - background: #fef3c7; - color: #92400e; -} - -.provider-card__badge[data-tone="error"], -.provider-card__key-pill[data-tone="error"] { - background: #fee2e2; - color: #991b1b; -} - -.provider-card__badge[data-tone="muted"], -.provider-card__key-pill[data-tone="muted"] { - background: #f3f4f6; - color: #374151; -} - -.provider-card__meta { - display: grid; - gap: 0.25em; - color: #666; - font-size: 0.85em; - margin-bottom: 0.75em; -} - -.provider-card__bytes, -.provider-card__issue { - color: #666; - font-size: 0.85em; - margin-bottom: 0.75em; -} - -.provider-card__issue { - color: #856404; -} - -.provider-card__actions { - display: flex; - align-items: center; - gap: 0.5em; - flex-wrap: wrap; -} - -.provider-card__actions button, -.provider-card__menu button { - border: 1px solid #d1d5db; - background: white; - color: #374151; - border-radius: 6px; - padding: 0.4em 0.75em; - font-size: 0.85em; - cursor: pointer; -} - -.provider-card__actions button:disabled { - color: #999; - cursor: default; -} - -.provider-card__menu { - position: relative; -} - -.provider-card__menu summary { - cursor: pointer; - color: #555; - font-size: 0.9em; -} - -.provider-card__menu-body { - display: grid; - gap: 0.35em; - position: absolute; - right: 0; - top: 1.5em; - min-width: 8em; - padding: 0.45em; - border: 1px solid var(--facet-border, #e5e0db); - border-radius: 8px; - background: #fff; - z-index: 3; - box-shadow: 0 6px 18px rgba(0,0,0,0.12); -} - -.provider-model-row { - align-items: flex-start; - margin-top: -0.25em; -} - /* Backend settings fieldsets */ .backend-settings { border: none; @@ -2590,7 +2456,7 @@ button:focus:not(:focus-visible) {

Gemini combines transcription and enrichment in a single API call, providing speaker diarization, emotion detection, and topic extraction automatically. - Model selection is handled by the provider settings. + Model selection is handled in Thinking.

@@ -3403,7 +3269,7 @@ let solVoiceCategories = []; // Reusable per-tab attention indicator. Any tab can opt in by calling // setTabAttention('
', true) when its content has a problem the // user should notice (incompatible backend on the transcription tab, -// missing API key referenced from the providers tab, etc.) and +// missing service token referenced by a backend, etc.) and // setTabAttention('
', false) once the issue clears. Implemented // by toggling data-attention on the
-
- advanced provider controls -
-
- - -
-
- - -
-
-
- -
-
- - -
-
- - -
-
-
- - -
-
-