diff --git a/CLAUDE.md b/CLAUDE.md index 2bd6dee9..99c57fcf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -187,8 +187,8 @@ SESSION_SECRET # Express session encryption (no fallback in prod) INTERNAL_API_SECRET # Express→Python shared secret (start-dev.sh generates a per-run value) DATABASE_URL # PostgreSQL connection string XAI_API_KEY # Grok energy provider -DEEPSEEK_API_KEY # DeepSeek V4 Flash/Pro; also used by standalone a0 -A0_PROVIDER # Optional standalone provider id (e.g. deepseek-pro) +DEEPSEEK_API_KEY # DeepSeek V4.1 Flash; also used by standalone a0 +A0_PROVIDER # Optional standalone provider id; deepseek is canonical STRIPE_SECRET_KEY # Stripe billing STRIPE_WEBHOOK_SECRET # Stripe webhook validation ADMIN_USER_ID # User ID allowed to write prompt contexts diff --git a/README.md b/README.md index 61292aca..2aea0264 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ## The Agent -ZFAE (`a0(zeta fun alpha echo)`) is the single persistent agent running on the instrument. Large language models (GPT-5 mini, Gemini 2.5 Flash, Claude Sonnet 4.5, Grok 4 Fast, DeepSeek V4) are treated as **energy providers** — they supply computational energy for each response but are not the agent itself. +ZFAE (`a0(zeta fun alpha echo)`) is the single persistent agent running on the instrument. Large language models (GPT-5 mini, Gemini 2.5 Flash, Claude Sonnet 4.5, Grok 4 Fast, DeepSeek V4.1 Flash) are treated as **energy providers** — they supply computational energy for each response but are not the agent itself. Sub-agents (`a0(zeta{n})`) can be spawned to fork the PCNA instance, execute in parallel, and merge results back into the primary agent. @@ -90,14 +90,17 @@ In development `scripts/start-dev.sh` generates a shared `INTERNAL_API_SECRET` a The standalone `run.sh` runtime reads the same `python/config/providers.json` registry as the FastAPI service. With a DeepSeek key present it auto-selects -V4 Flash through the generic `openai-compatible` adapter. Pin V4 Pro explicitly: +V4.1 Flash through the generic `openai-compatible` adapter: ```bash export DEEPSEEK_API_KEY="..." -export A0_PROVIDER=deepseek-pro # omit for deepseek-v4-flash printf '%s\n' '{"task_id":"ds1","input":{"text":"hello"},"tools_allowed":["none"],"mode":"analyze","hmmm":["direct model turn"]}' | ./run.sh ``` +The canonical API model is `deepseek-flash`. The retired provider choice +`A0_PROVIDER=deepseek-pro` remains a hidden compatibility alias and resolves to +the same V4.1 Flash route; it no longer represents a higher-capability tier. + Provider configuration contains only the credential environment-variable name; keys remain in the process environment and are never written to source. @@ -133,7 +136,7 @@ npx playwright test | `ANTHROPIC_API_KEY` | Claude Sonnet 4.5 | | `GEMINI_API_KEY` | Gemini 2.5 Flash | | `OPENAI_API_KEY` | GPT-5 mini | -| `DEEPSEEK_API_KEY` | DeepSeek V4 Flash/Pro through the OpenAI-compatible adapter | +| `DEEPSEEK_API_KEY` | DeepSeek V4.1 Flash through the OpenAI-compatible adapter | | `STRIPE_SECRET_KEY` | Stripe (donations + EDCMbone explainer) | | `STRIPE_PUBLISHABLE_KEY` | Stripe embedded checkout | | `STRIPE_WEBHOOK_SECRET` | Stripe webhook HMAC | diff --git a/a0/adapters/open_comp_adap_v0.0.0alpha.py b/a0/adapters/open_comp_adap_v0.0.0alpha.py index d3df0734..35a06b77 100644 --- a/a0/adapters/open_comp_adap_v0.0.0alpha.py +++ b/a0/adapters/open_comp_adap_v0.0.0alpha.py @@ -1,4 +1,4 @@ -# 100:47 0:0 2:0 +# 102:53 0:0 2:0 """Synchronous standalone adapter for registry-defined OpenAI-compatible APIs.""" from __future__ import annotations @@ -47,6 +47,12 @@ # then: the adapter raises the effective effort to that configured floor before transport # class: correctness # since: 2026-09-10 +# +# id: a0_openai_compatible_explicit_none_reasoning +# given: a compatible provider declares that reasoning effort none must be sent explicitly +# then: standalone Responses calls carry reasoning.effort=none rather than falling back to the provider default +# class: correctness +# since: 2026-09-10 # === END CONTRACTS === import os @@ -130,7 +136,9 @@ def complete(self, messages: List[Message], **kwargs: Any) -> Dict[str, Any]: ) if supports_store: request["store"] = bool(kwargs.get("store", False)) - if effort and effort != "none": + if effort and ( + effort != "none" or self.spec.get("explicit_none_reasoning") + ): request["reasoning"] = {"effort": effort} response = self._client.responses.create(**request) text = _response_text(response) @@ -162,4 +170,4 @@ def complete(self, messages: List[Message], **kwargs: Any) -> Dict[str, Any]: }, "subagents_used": [], } -# 100:47 0:0 2:0 +# 102:53 0:0 2:0 diff --git a/a0/prov_regi_v0.0.0alpha.py b/a0/prov_regi_v0.0.0alpha.py index f23c6faf..aba4f003 100644 --- a/a0/prov_regi_v0.0.0alpha.py +++ b/a0/prov_regi_v0.0.0alpha.py @@ -1,4 +1,4 @@ -# 36:29 0:0 1:0 +# 55:30 0:0 1:0 """Read standalone a0 model adapters from the canonical provider registry.""" from __future__ import annotations @@ -26,7 +26,7 @@ # === CONTRACTS === # id: a0_provider_selection # given: optional A0_PROVIDER, canonical provider registry data, and process credentials -# then: an explicit compatible provider resolves or fails closed while an unset choice may auto-select only a configured a0_default provider +# then: an explicit compatible provider resolves or fails closed, deprecated provider aliases resolve to their canonical provider, and an unset choice may auto-select only a configured a0_default provider # class: correctness # since: 2026-09-07 # === END CONTRACTS === @@ -49,6 +49,27 @@ def _is_openai_compatible(spec: dict[str, Any]) -> bool: return spec.get("adapter") == "openai-compatible" or spec.get("vendor") == "openai" +def _resolve_alias( + providers: dict[str, dict[str, Any]], + provider_id: str, +) -> tuple[str, dict[str, Any]]: + """Resolve one registry-declared provider alias without provider literals.""" + spec = providers[provider_id] + canonical_id = str(spec.get("deprecated_alias_for") or "").strip() + if not canonical_id: + return provider_id, spec + canonical = providers.get(canonical_id) + if canonical is None: + raise ValueError( + f"A0_PROVIDER {provider_id!r} aliases missing provider {canonical_id!r}" + ) + if canonical.get("deprecated_alias_for"): + raise ValueError( + f"A0_PROVIDER {provider_id!r} has a chained provider alias" + ) + return canonical_id, canonical + + def resolve_openai_compatible_provider( provider_id: str | None = None, ) -> tuple[str, dict[str, Any]] | None: @@ -59,11 +80,12 @@ def resolve_openai_compatible_provider( spec = providers.get(explicit) if spec is None: raise ValueError(f"Unknown A0_PROVIDER: {explicit!r}") + canonical_id, spec = _resolve_alias(providers, explicit) if not _is_openai_compatible(spec): raise ValueError( f"A0_PROVIDER {explicit!r} does not use the openai-compatible adapter" ) - return explicit, spec + return canonical_id, spec for candidate, spec in providers.items(): api_key_env = str(spec.get("api_key_env") or "") @@ -75,4 +97,4 @@ def resolve_openai_compatible_provider( ): return candidate, spec return None -# 36:29 0:0 1:0 +# 55:30 0:0 1:0 diff --git a/python/config/pricing.json b/python/config/pricing.json index 6a29aff0..ad54880e 100644 --- a/python/config/pricing.json +++ b/python/config/pricing.json @@ -1,8 +1,8 @@ { - "version": "2026-09-07.a0.pricing.v2", - "prices_as_of": "2026-09-07", + "version": "2026-09-10.a0.pricing.v3", + "prices_as_of": "2026-09-10", "_doc": { - "schema": "providers..models is a list of {id, context_window, input_per_1m, output_per_1m, cached_input_per_1m?, cache_write_per_1m?, supports_vision?, supports_thinking?, note?} entries. All prices are USD per 1,000,000 tokens.", + "schema": "providers..models is a list of {id, context_window, max_output_tokens?, input_per_1m, output_per_1m, cached_input_per_1m?, cache_write_per_1m?, supports_vision?, supports_thinking?, note?} entries. All prices are USD per 1,000,000 tokens.", "source-of-truth": "Boot source-of-truth for per-model pricing. estimate_cost(provider_id, ..., model=) consults this file first and uses per-model rates; without a model arg, falls back to the provider flagship rate from providers.json. Hydrates each ws_modules.route_config.available_models list on first boot and on POST /api/energy/refresh-pricing/{provider_id}.", "naming": "Model ids are the slugs the provider's API accepts. Keep in sync with providers.json presets section so optimizer presets and per-model pricing line up.", "live-refresh": "POST /api/energy/refresh-pricing/{provider_id} re-reads this file from disk and re-hydrates the seed's available_models. Live HTML extraction from pricing_url is a future phase; for now admins update this file in repo, redeploy, then refresh." @@ -187,13 +187,15 @@ "pricing_url": "https://api-docs.deepseek.com/quick_start/pricing/", "models": [ { - "id": "deepseek-v4-flash", + "id": "deepseek-flash", "context_window": 1000000, - "input_per_1m": 0.44, - "output_per_1m": 1.32, - "cached_input_per_1m": 0.014, + "max_output_tokens": 384000, + "input_per_1m": 0.3, + "output_per_1m": 1.2, + "cached_input_per_1m": 0.006, "supports_thinking": true, - "note": "Official peak rates; off-peak rates are lower." + "supports_vision": true, + "note": "DeepSeek V4.1 Flash official peak rates; off-peak rates are half." } ] }, @@ -201,13 +203,15 @@ "pricing_url": "https://api-docs.deepseek.com/quick_start/pricing/", "models": [ { - "id": "deepseek-v4-pro", + "id": "deepseek-flash", "context_window": 1000000, - "input_per_1m": 1.32, - "output_per_1m": 3.96, - "cached_input_per_1m": 0.044, + "max_output_tokens": 384000, + "input_per_1m": 0.3, + "output_per_1m": 1.2, + "cached_input_per_1m": 0.006, "supports_thinking": true, - "note": "Official peak rates; off-peak rates are lower." + "supports_vision": true, + "note": "Compatibility pricing for the retired deepseek-pro provider id; routes to DeepSeek V4.1 Flash." } ] } diff --git a/python/config/providers.json b/python/config/providers.json index 9d7ff3dc..62b9e753 100644 --- a/python/config/providers.json +++ b/python/config/providers.json @@ -1,8 +1,8 @@ { - "version": "2026-09-07.a0.providers.v2", + "version": "2026-09-10.a0.providers.v3", "attribution": "Externalized from energy_registry.py BUILTIN_PROVIDERS + _PROVIDER_PRESETS + _PROVIDER_PRICING_URLS so model slugs / provider IDs / capability flags live as data, not code literals (doctrine: no executable-data string literals).", "_doc": { - "providers[].* fields": "id label model adapter vendor base_url api_key_env model_env_prefix api_family tool_profile reasoning_efforts reasoning_effort_map cost_per_1k_input cost_per_1k_output cache_read_per_1k_input cache_write_per_1k_input max_tokens supports_streaming supports_prompt_caching supports_thinking supports_reasoning_effort min_tier note pricing_url", + "providers[].* fields": "id label model model_aliases adapter vendor base_url api_key_env model_env_prefix api_family tool_profile reasoning_efforts reasoning_effort_map explicit_none_reasoning cost_per_1k_input cost_per_1k_output cache_read_per_1k_input cache_write_per_1k_input max_tokens max_output_tokens supports_streaming supports_prompt_caching supports_thinking supports_reasoning_effort min_tier deprecated_alias_for hidden note pricing_url", "presets": "Per-provider role-map keyed by preset name. Roles: record practice conduct perform derive (the renamed pipeline slots). Single-model providers pin every preset to their one model so the active_preset switch still has effect." }, "providers": { @@ -300,8 +300,13 @@ }, "deepseek": { "id": "deepseek", - "label": "DeepSeek V4 Flash", - "model": "deepseek-v4-flash", + "label": "DeepSeek V4.1 Flash", + "model": "deepseek-flash", + "model_aliases": [ + "deepseek-v4-flash", + "deepseek-v4-flash-vision-exp", + "deepseek-v4-pro" + ], "adapter": "openai-compatible", "vendor": "openai-compatible", "base_url": "https://api.deepseek.com", @@ -309,35 +314,40 @@ "model_env_prefix": "DEEPSEEK_MODEL_", "api_family": "responses", "tool_profile": "functions-only", - "reasoning_efforts": ["low", "high", "max"], + "reasoning_efforts": ["none", "low", "high", "max"], "reasoning_effort_map": { - "none": "low", + "none": "none", "minimal": "low", "low": "low", "medium": "high", "high": "high", - "xhigh": "max", - "max": "max" + "xhigh": "high", + "max": "max", + "ultra": "max" }, + "explicit_none_reasoning": true, "default_reasoning_effort": "high", "a0_default": true, - "cost_per_1k_input": 0.00044, - "cost_per_1k_output": 0.00132, - "cache_read_per_1k_input": 0.000014, + "cost_per_1k_input": 0.0003, + "cost_per_1k_output": 0.0012, + "cache_read_per_1k_input": 0.000006, "max_tokens": 1000000, + "max_output_tokens": 384000, "supports_streaming": false, "supports_prompt_caching": true, "supports_thinking": true, "supports_reasoning_effort": true, "supports_store": false, - "supports_vision": false, + "supports_vision": true, "pricing_url": "https://api-docs.deepseek.com/quick_start/pricing/", - "note": "Default standalone a0 energy; official V4 alias and peak token rates as of 2026-09-07." + "note": "Default standalone a0 energy; V4.1 Flash canonical slug and official peak token rates as of 2026-09-10. Off-peak rates are half." }, "deepseek-pro": { "id": "deepseek-pro", - "label": "DeepSeek V4 Pro", - "model": "deepseek-v4-pro", + "label": "DeepSeek Pro (legacy alias to V4.1 Flash)", + "model": "deepseek-flash", + "deprecated_alias_for": "deepseek", + "hidden": true, "adapter": "openai-compatible", "vendor": "openai-compatible", "base_url": "https://api.deepseek.com", @@ -345,30 +355,32 @@ "model_env_prefix": "DEEPSEEK_PRO_MODEL_", "api_family": "responses", "tool_profile": "functions-only", - "reasoning_efforts": ["low", "high", "max"], + "reasoning_efforts": ["none", "low", "high", "max"], "reasoning_effort_map": { - "none": "low", + "none": "none", "minimal": "low", "low": "low", "medium": "high", "high": "high", - "xhigh": "max", - "max": "max" + "xhigh": "high", + "max": "max", + "ultra": "max" }, + "explicit_none_reasoning": true, "default_reasoning_effort": "high", - "cost_per_1k_input": 0.00132, - "cost_per_1k_output": 0.00396, - "cache_read_per_1k_input": 0.000044, + "cost_per_1k_input": 0.0003, + "cost_per_1k_output": 0.0012, + "cache_read_per_1k_input": 0.000006, "max_tokens": 1000000, + "max_output_tokens": 384000, "supports_streaming": false, "supports_prompt_caching": true, "supports_thinking": true, "supports_reasoning_effort": true, "supports_store": false, - "supports_vision": false, - "min_tier": "ws", + "supports_vision": true, "pricing_url": "https://api-docs.deepseek.com/quick_start/pricing/", - "note": "Higher-capability standalone a0 option; official V4 alias and peak token rates as of 2026-09-07." + "note": "Compatibility-only provider id. A0_PROVIDER=deepseek-pro resolves to deepseek; DeepSeek retires the distinct V4 Pro route at 2026-09-14 04:00 UTC." } }, "presets": { @@ -682,90 +694,46 @@ }, "deepseek": { "speed": { - "record": "deepseek-v4-flash", - "practice": "deepseek-v4-flash", - "conduct": "deepseek-v4-flash", - "perform": "deepseek-v4-flash", - "derive": "deepseek-v4-flash" - }, - "price": { - "record": "deepseek-v4-flash", - "practice": "deepseek-v4-flash", - "conduct": "deepseek-v4-flash", - "perform": "deepseek-v4-flash", - "derive": "deepseek-v4-flash" - }, - "balance": { - "record": "deepseek-v4-flash", - "practice": "deepseek-v4-flash", - "conduct": "deepseek-v4-flash", - "perform": "deepseek-v4-pro", - "derive": "deepseek-v4-flash" - }, - "depth": { - "record": "deepseek-v4-flash", - "practice": "deepseek-v4-pro", - "conduct": "deepseek-v4-pro", - "perform": "deepseek-v4-pro", - "derive": "deepseek-v4-pro" - }, - "coding": { - "record": "deepseek-v4-flash", - "practice": "deepseek-v4-flash", - "conduct": "deepseek-v4-pro", - "perform": "deepseek-v4-pro", - "derive": "deepseek-v4-flash" - }, - "creativity": { - "record": "deepseek-v4-flash", - "practice": "deepseek-v4-pro", - "conduct": "deepseek-v4-pro", - "perform": "deepseek-v4-pro", - "derive": "deepseek-v4-flash" - } - }, - "deepseek-pro": { - "speed": { - "record": "deepseek-v4-pro", - "practice": "deepseek-v4-pro", - "conduct": "deepseek-v4-pro", - "perform": "deepseek-v4-pro", - "derive": "deepseek-v4-pro" + "record": "deepseek-flash", + "practice": "deepseek-flash", + "conduct": "deepseek-flash", + "perform": "deepseek-flash", + "derive": "deepseek-flash" }, "price": { - "record": "deepseek-v4-pro", - "practice": "deepseek-v4-pro", - "conduct": "deepseek-v4-pro", - "perform": "deepseek-v4-pro", - "derive": "deepseek-v4-pro" + "record": "deepseek-flash", + "practice": "deepseek-flash", + "conduct": "deepseek-flash", + "perform": "deepseek-flash", + "derive": "deepseek-flash" }, "balance": { - "record": "deepseek-v4-pro", - "practice": "deepseek-v4-pro", - "conduct": "deepseek-v4-pro", - "perform": "deepseek-v4-pro", - "derive": "deepseek-v4-pro" + "record": "deepseek-flash", + "practice": "deepseek-flash", + "conduct": "deepseek-flash", + "perform": "deepseek-flash", + "derive": "deepseek-flash" }, "depth": { - "record": "deepseek-v4-pro", - "practice": "deepseek-v4-pro", - "conduct": "deepseek-v4-pro", - "perform": "deepseek-v4-pro", - "derive": "deepseek-v4-pro" + "record": "deepseek-flash", + "practice": "deepseek-flash", + "conduct": "deepseek-flash", + "perform": "deepseek-flash", + "derive": "deepseek-flash" }, "coding": { - "record": "deepseek-v4-pro", - "practice": "deepseek-v4-pro", - "conduct": "deepseek-v4-pro", - "perform": "deepseek-v4-pro", - "derive": "deepseek-v4-pro" + "record": "deepseek-flash", + "practice": "deepseek-flash", + "conduct": "deepseek-flash", + "perform": "deepseek-flash", + "derive": "deepseek-flash" }, "creativity": { - "record": "deepseek-v4-pro", - "practice": "deepseek-v4-pro", - "conduct": "deepseek-v4-pro", - "perform": "deepseek-v4-pro", - "derive": "deepseek-v4-pro" + "record": "deepseek-flash", + "practice": "deepseek-flash", + "conduct": "deepseek-flash", + "perform": "deepseek-flash", + "derive": "deepseek-flash" } } } diff --git a/python/routes/instances_api.py b/python/routes/instances_api.py index f0e7af00..4d3630b7 100644 --- a/python/routes/instances_api.py +++ b/python/routes/instances_api.py @@ -1,4 +1,4 @@ -# 344:42 3:17 1:4 +# 346:42 3:17 1:4 # DOC module: instances_api # DOC label: Model Instances # DOC description: CRUD for model instances (D&D party), per-instance memory, task board, and chat/archive sub-routes. @@ -37,7 +37,7 @@ from ..database import get_session from ._admin_gate import require_admin from ..services.agent_instance import AgentInstance -from ..services.model_catalog import resolve_model_id +from ..services.model_catalog import resolve_model_id, visible_provider_specs router = APIRouter(prefix="/api/v1", tags=["instances"]) _log = logging.getLogger("a0p.instances_api") @@ -99,12 +99,12 @@ def _provider_data() -> dict: async def list_models(): """All models from providers.json, grouped by vendor. - Returns flagship models from providers{} plus all sub-models referenced - in presets{} so every model that can be used in a slot or preset can also - be instantiated and given instance memory. + Returns visible flagship models from providers{} plus all sub-models + referenced in their presets{} so every selectable model can be instantiated + and given instance memory without exposing hidden compatibility aliases. """ data = _provider_data() - providers = data.get("providers", {}) + providers = visible_provider_specs(data.get("providers", {})) presets = data.get("presets", {}) # Build per-provider capability index for preset sub-model entries. @@ -141,6 +141,8 @@ async def list_models(): # Sub-models from presets that aren't already in the flagship list. for pid, preset_map in presets.items(): + if pid not in provider_meta: + continue meta = provider_meta.get(pid, {}) vendor = meta.get("vendor", "unknown") for _preset_name, slot_map in preset_map.items(): @@ -460,4 +462,4 @@ async def get_archives(iid: str): return [{"id": str(r["id"]), "label": r["label"], "archived_at": str(r["archived_at"]), "merge_status": r["merge_status"]} for r in rows] -# 344:42 3:17 1:4 +# 346:42 3:17 1:4 diff --git a/python/services/edcm_explainer.py b/python/services/edcm_explainer.py index 882fcb6d..646c82f1 100644 --- a/python/services/edcm_explainer.py +++ b/python/services/edcm_explainer.py @@ -1,4 +1,4 @@ -# 258:214 0:0 2:5 +# 256:215 0:0 2:5 """EDCMbone scoring report → 200-400 word human explanation with cited quoted spans from the transcript. Owner-only, idempotent per report (UNIQUE on report_id), strict-JSON output, refund-on-failure. @@ -36,6 +36,7 @@ from ..database import engine, get_session from ..services.energy_registry import BUILTIN_PROVIDERS from ..services.inference import call_provider +from ..services.model_catalog import resolve_model_id from ..services.run_logger import get_run_logger from ..storage import storage @@ -403,13 +404,11 @@ async def explain_report( ) )).first() if _slot_row: - _slot_mid = _slot_row[0] - _match = next( - (pid for pid, p in BUILTIN_PROVIDERS.items() if p.get("model") == _slot_mid), - None, - ) - if _match: - resolved_provider_id = _match + try: + resolved_provider_id, _ = await resolve_model_id(str(_slot_row[0])) + except ValueError: + # Unknown persisted slot ids intentionally use the default provider. + pass except Exception: pass # fall back to default on any DB error @@ -527,4 +526,4 @@ def _credits_view(row: Dict[str, Any]) -> Dict[str, Any]: # rolls it up by provider in the paid_explainer section # class: correctness # === END CONTRACTS === -# 258:214 0:0 2:5 +# 256:215 0:0 2:5 diff --git a/python/services/energy_registry.py b/python/services/energy_registry.py index 8b511c7b..cc2e4ddf 100644 --- a/python/services/energy_registry.py +++ b/python/services/energy_registry.py @@ -1,4 +1,4 @@ -# 291:95 0:0 20:3 +# 312:101 0:0 20:3 # === MODULE_BUILD === # id: a0_service_energy_registry # module_name: energy_registry @@ -6,7 +6,7 @@ # summary: Energy-provider catalog and pricing/cost layer — loads provider+pricing JSON data, resolves active/default/cheap providers, and estimates per-call cost and cache breakdown from usage. # owner: Erin Spencer # public_surface: BUILTIN_PROVIDERS, get_pricing_models, get_model_pricing, reload_pricing_doc, default_provider, active_provider, cheap_provider, estimate_cost, cache_breakdown, reset_per_call_usage -# internal_surface: _load_pricing_doc +# internal_surface: _canonical_provider_id, _load_pricing_doc # auth_boundary: none # storage_boundary: read # network_boundary: internal @@ -26,6 +26,11 @@ # then: cheap_provider selects DeepSeek Flash before registry-order fallback # class: correctness # since: 2026-09-09 +# id: deprecated_provider_aliases_canonicalize +# given: an internal caller or multi-provider request supplies a registry provider id with deprecated_alias_for +# then: provider selection canonicalizes and deduplicates the route before hub orchestration, dispatch, metering, and reporting +# class: correctness +# since: 2026-09-10 # === END CONTRACTS === import contextvars import json @@ -52,6 +57,19 @@ if spec.get("pricing_url") } + +def _canonical_provider_id(provider_id: str) -> str: + """Resolve one registry alias and fail visibly on a broken target.""" + spec = BUILTIN_PROVIDERS.get(provider_id, {}) + canonical_id = str(spec.get("deprecated_alias_for") or "").strip() + if not canonical_id: + return provider_id + if canonical_id not in BUILTIN_PROVIDERS: + raise ValueError( + f"Provider {provider_id!r} aliases missing provider {canonical_id!r}" + ) + return canonical_id + # Per-model pricing manifest — source of truth for input/output/cached rates # per individual model id. Used on boot and on POST /api/energy/refresh-pricing/{provider_id}. _PRICING_JSON_PATH = Path(__file__).parent.parent / "config" / "pricing.json" @@ -106,6 +124,8 @@ def default_provider() -> str | None: the async active_provider() which reads the conduct slot from the DB. """ for pid, info in BUILTIN_PROVIDERS.items(): + if info.get("hidden"): + continue api_key_env = info.get("api_key_env", "") if api_key_env and os.environ.get(api_key_env): return pid @@ -137,9 +157,13 @@ async def active_provider() -> str: raise RuntimeError("No instantiation selected") model_id = (_row["model_id"] or "").strip() if model_id in BUILTIN_PROVIDERS: - return model_id + return _canonical_provider_id(model_id) for pid, spec in BUILTIN_PROVIDERS.items(): - if spec.get("model") == model_id or spec.get("spec_model") == model_id: + if ( + spec.get("model") == model_id + or spec.get("spec_model") == model_id + or model_id in (spec.get("model_aliases") or []) + ): return pid raise RuntimeError("No instantiation selected") @@ -153,7 +177,7 @@ async def active_provider() -> str: "grok", # grok-4-fast-reasoning $0.20/1M "gemini", # gemini-2.5-flash $0.30/1M "openai", # gpt-5-mini $0.25/1M - "deepseek", # deepseek-v4-flash $0.44/1M + "deepseek", # deepseek-flash $0.30/1M peak ] @@ -287,6 +311,7 @@ async def _aimmh_call_fn(model_id, messages, system_context=None, max_history=30 """Bridge aimmh-lib's CallFn signature into call_provider.""" from .inference import call_provider as _cep from . import orch_progress as _op + model_id = _canonical_provider_id(model_id) state = _per_call_usage_cv.get() call_idx = None if state is not None: @@ -406,10 +431,12 @@ def get_multi_model_hub(): def build_model_instances() -> dict: - """Construct one aimmh ModelInstance per BUILTIN_PROVIDERS entry with an active key.""" + """Construct one aimmh ModelInstance per visible provider with an active key.""" from aimmh_lib import ModelInstance out: dict = {} for pid, info in BUILTIN_PROVIDERS.items(): + if info.get("hidden"): + continue api_key_env = info.get("api_key_env", "") if api_key_env and not os.environ.get(api_key_env): continue @@ -434,12 +461,14 @@ async def resolve_providers(providers: list[str] | None) -> list[str]: for p in providers: if p == "active": try: - a = await active_provider() + a = _canonical_provider_id(await active_provider()) if a not in out: out.append(a) except RuntimeError: pass - elif p in BUILTIN_PROVIDERS and p not in out: - out.append(p) + elif p in BUILTIN_PROVIDERS: + canonical_id = _canonical_provider_id(p) + if canonical_id not in out: + out.append(canonical_id) return out -# 291:95 0:0 20:3 +# 312:101 0:0 20:3 diff --git a/python/services/inference.py b/python/services/inference.py index d286f263..50be4af1 100644 --- a/python/services/inference.py +++ b/python/services/inference.py @@ -1,4 +1,4 @@ -# 388:157 0:0 16:15 +# 399:164 0:0 16:15 # === MODULE_BUILD === # id: a0_service_inference # module_name: inference @@ -50,6 +50,12 @@ # class: correctness # since: 2026-09-09 # +# id: inference_legacy_model_alias_memory_survives +# given: a canonical provider has persisted model instances stored under a declared legacy model alias +# then: provider memory lookup considers the canonical model and every declared alias while preferring the canonical row +# class: correctness +# since: 2026-09-10 +# # id: inference_compatible_provider_approval_gate # given: a tool-enabled provider turn requests an external write without a grant, whether text preflight or actual dispatch identifies it # then: inference returns a pending approval gate before mutation and replay bypasses text preflight only while dispatch remains limited to the gate's exact scopes @@ -81,7 +87,8 @@ async def _instance_memory_block(provider_id: str) -> str: """Fetch editable instance memory for the provider's primary model. Reads instance_memory rows for the model_instances row whose model_id - matches this provider's model string. Also prepends swarm_context if set. + matches this provider's current model or a declared legacy alias. Also + prepends swarm_context if set. Returns "" when no instance exists, no entries exist, or on any error — so inference is never blocked by this path. @@ -97,11 +104,17 @@ async def _instance_memory_block(provider_id: str) -> str: model_id = (spec.get("model") or "").strip() if not model_id: return "" + model_ids = [model_id, *( + alias for alias in (spec.get("model_aliases") or []) if alias != model_id + )] + model_ids.extend(pid for pid, candidate in BUILTIN_PROVIDERS.items() + if candidate.get("deprecated_alias_for") == provider_id) async with get_session() as session: inst = (await session.execute(_sa_text( "SELECT id, swarm_context FROM model_instances " - "WHERE model_id = :mid LIMIT 1" - ), {"mid": model_id})).mappings().first() + "WHERE model_id = ANY(CAST(:mids AS text[])) " + "ORDER BY CASE WHEN model_id = :mid THEN 0 ELSE 1 END LIMIT 1" + ), {"mid": model_id, "mids": model_ids})).mappings().first() if not inst: return "" iid = str(inst["id"]) @@ -168,7 +181,12 @@ async def _slot_routing_info(slot: str) -> tuple[str, "str | None"]: parts.append(f"[{(r['tier'] or '').upper()}] {r['content']}") mem = "\n".join(parts) resolved = next( - (pid for pid, p in BUILTIN_PROVIDERS.items() if p.get("model") == model_id), + ( + pid + for pid, p in BUILTIN_PROVIDERS.items() + if p.get("model") == model_id + or model_id in (p.get("model_aliases") or []) + ), None, ) return mem, resolved @@ -609,4 +627,4 @@ async def _call_anthropic( enable_caching=enable_caching) -# 388:157 0:0 16:15 +# 399:164 0:0 16:15 diff --git a/python/services/model_catalog.py b/python/services/model_catalog.py index e0809416..0ce7fda0 100644 --- a/python/services/model_catalog.py +++ b/python/services/model_catalog.py @@ -1,4 +1,4 @@ -# 150:98 0:0 7:1 +# 166:105 0:0 7:1 """model_catalog — single source of truth for "what models can this user use". Today three surfaces answer this question independently: @@ -28,7 +28,7 @@ # module_kind: service # summary: Single source of truth for "what models can this user invoke" — unifies Forge dropdown, chat chips, and subagent spawn into one tier-gated, provenance-annotated model list plus model_id resolution. # owner: Erin Spencer -# public_surface: resolve_model_id, resolve_routed_model, routed_model_owner, is_provider_enabled, list_models_for_user +# public_surface: resolve_model_id, resolve_routed_model, routed_model_owner, visible_provider_specs, is_provider_enabled, list_models_for_user # internal_surface: _tier_ok, _resolve_static, _user_tier # auth_boundary: none # storage_boundary: read @@ -49,6 +49,12 @@ # then: entitlement and provenance use the concrete model's owning provider, while unknown routed models fail closed when a caller tier is present # class: security # since: 2026-09-09 +# +# id: catalog_legacy_model_aliases_canonicalize +# given: a persisted or explicitly requested model id is a registry-declared legacy alias +# then: catalog resolution attributes it to the canonical provider, transport uses that provider's current primary model, and hidden provider aliases are omitted from registry-backed rosters +# class: correctness +# since: 2026-09-10 # === END CONTRACTS === from typing import Any, Optional @@ -64,6 +70,11 @@ _TIER_ORDER = {"free": 0, "supporter": 1, "ws": 2, "admin": 3} +def visible_provider_specs(providers: dict[str, dict]) -> dict[str, dict]: + """Return provider entries intended for user-facing model rosters.""" + return {pid: spec for pid, spec in providers.items() if not spec.get("hidden")} + + def _tier_ok(user_tier: str, min_tier: Optional[str]) -> bool: if not min_tier: return True @@ -77,13 +88,22 @@ def _resolve_static(model_id: str) -> Optional[tuple[str, dict]]: back to persisted route_config when this misses. """ if model_id in BUILTIN_PROVIDERS: - return model_id, BUILTIN_PROVIDERS[model_id] + spec = BUILTIN_PROVIDERS[model_id] + canonical_id = str(spec.get("deprecated_alias_for") or "").strip() + if canonical_id: + canonical = BUILTIN_PROVIDERS.get(canonical_id) + if canonical is not None: + return canonical_id, canonical + return model_id, spec # Every provider's primary model is more authoritative than any optimizer # preset reference. This prevents a shared preset model from being # attributed to whichever provider happens to appear first in JSON. for pid, spec in BUILTIN_PROVIDERS.items(): if spec.get("model") == model_id: return pid, spec + for pid, spec in BUILTIN_PROVIDERS.items(): + if model_id in (spec.get("model_aliases") or []): + return pid, spec for pid, spec in BUILTIN_PROVIDERS.items(): presets = _PROVIDER_PRESETS.get(pid, {}) for role_map in presets.values(): @@ -133,6 +153,9 @@ async def resolve_routed_model( if not model_id: raise ValueError(f"Provider {provider_id!r} has no routed model") owner = routed_model_owner(model_id, provider_id, user_tier) + owner_spec = BUILTIN_PROVIDERS[owner] + if model_id in (owner_spec.get("model_aliases") or []): + model_id = str(owner_spec.get("model") or "").strip() return owner, model_id @@ -216,6 +239,8 @@ async def list_models_for_user(user_id: Optional[str]) -> dict[str, Any]: cfgs: dict[str, dict] = {} for pid, spec in BUILTIN_PROVIDERS.items(): + if spec.get("hidden"): + continue api_key_env = spec.get("api_key_env") import os key_present = bool(api_key_env and os.environ.get(api_key_env)) @@ -284,4 +309,4 @@ def _touch(mid: str) -> dict: }) return {"user_tier": user_tier, "providers": out_providers} -# 150:98 0:0 7:1 +# 166:105 0:0 7:1 diff --git a/python/services/providers/open_comp_prov_v0.0.0alpha.py b/python/services/providers/open_comp_prov_v0.0.0alpha.py index 6ae7128f..216d9b0b 100644 --- a/python/services/providers/open_comp_prov_v0.0.0alpha.py +++ b/python/services/providers/open_comp_prov_v0.0.0alpha.py @@ -1,4 +1,4 @@ -# 342:55 0:0 2:5 +# 364:67 0:0 2:5 """Generic OpenAI-compatible provider transport. Provider identity, endpoint, credential name, model, API family, reasoning @@ -14,7 +14,7 @@ # summary: Registry-driven OpenAI-compatible transport supporting Responses and Chat Completions with the shared repeat-safe tool loop. # owner: Erin Spencer # public_surface: call -# internal_surface: _call_responses, _call_chat_completions, _normalize_reasoning_effort, _response_tools +# internal_surface: _format_responses_messages, _call_responses, _call_chat_completions, _normalize_reasoning_effort, _response_tools # auth_boundary: none # storage_boundary: none # network_boundary: external @@ -52,6 +52,18 @@ # then: the provider identity is active for the complete transport loop and the prior context is restored on every exit # class: correctness # since: 2026-09-09 +# +# id: openai_compatible_explicit_none_reasoning +# given: a compatible provider declares that reasoning effort none must be sent explicitly +# then: the Responses request carries reasoning.effort=none instead of omitting the field and activating the provider default +# class: correctness +# since: 2026-09-10 +# +# id: openai_compatible_responses_formats_vision_input +# given: attachment preparation supplies OpenAI Chat-style text and image_url content parts to a Responses-family provider +# then: transport converts them to input_text and input_image parts before the request is sent +# class: correctness +# since: 2026-09-10 # === END CONTRACTS === import copy @@ -123,7 +135,22 @@ def _format_responses_messages(messages: list[dict]) -> list[dict]: role = message.get("role", "user") content = message.get("content", "") if isinstance(content, list): - formatted.append({"role": role, "content": content}) + parts: list = [] + for part in content: + if not isinstance(part, dict): + parts.append(part) + continue + part_type = part.get("type") + if part_type == "text": + parts.append({"type": "input_text", "text": part.get("text", "")}) + elif part_type == "image_url": + image_url = part.get("image_url") or "" + if isinstance(image_url, dict): + image_url = image_url.get("url") or "" + parts.append({"type": "input_image", "image_url": image_url}) + else: + parts.append(part) + formatted.append({"role": role, "content": parts}) elif role in {"system", "assistant", "developer"}: formatted.append({"role": role, "content": content}) else: @@ -151,6 +178,7 @@ def _responses_kwargs( max_output_tokens: int, temperature: float, reasoning_effort: Optional[str], + explicit_none_reasoning: bool = False, store: bool, supports_store: bool, tools: list[dict] | None, @@ -164,9 +192,11 @@ def _responses_kwargs( } if supports_store: kwargs["store"] = store - if reasoning_effort and reasoning_effort != "none": + if reasoning_effort and ( + reasoning_effort != "none" or explicit_none_reasoning + ): kwargs["reasoning"] = {"effort": reasoning_effort} - if supports_store and not store: + if reasoning_effort != "none" and supports_store and not store: kwargs["include"] = ["reasoning.encrypted_content"] if tools: kwargs["tools"] = tools @@ -181,6 +211,7 @@ async def _call_responses( max_output_tokens: int, temperature: float, reasoning_effort: Optional[str], + explicit_none_reasoning: bool = False, store: bool, use_tools: bool, base_url: str | None = None, @@ -211,6 +242,7 @@ async def _call_responses( max_output_tokens=max_output_tokens, temperature=temperature, reasoning_effort=reasoning_effort, + explicit_none_reasoning=explicit_none_reasoning, store=store, supports_store=supports_store, tools=tools, @@ -246,6 +278,7 @@ async def _call_responses( max_output_tokens=max_output_tokens, temperature=temperature, reasoning_effort=reasoning_effort, + explicit_none_reasoning=explicit_none_reasoning, store=store, supports_store=supports_store, tools=None, @@ -419,6 +452,7 @@ async def call( max_output_tokens=max_tokens, temperature=temperature, reasoning_effort=effort, + explicit_none_reasoning=bool(spec.get("explicit_none_reasoning")), store=store, use_tools=use_tools, base_url=base_url, @@ -443,4 +477,4 @@ async def call( ) finally: reset_caller_provider(caller_provider_token) -# 342:55 0:0 2:5 +# 364:67 0:0 2:5 diff --git a/python/tests/chec_open_comp_cont_v0.0.0alpha.py b/python/tests/chec_open_comp_cont_v0.0.0alpha.py index b5cc0e70..3ca6e437 100644 --- a/python/tests/chec_open_comp_cont_v0.0.0alpha.py +++ b/python/tests/chec_open_comp_cont_v0.0.0alpha.py @@ -1,4 +1,4 @@ -# 117:19 0:0 0:0 +# 123:19 0:0 0:0 """Executable msdmd witness for the generic provider boundary.""" # === CHECKS === @@ -11,7 +11,7 @@ # cleanup: none # # id: check_openai_compatible_repair_regressions -# proves: a0_openai_compatible_error_suppresses_secret_cause, a0_openai_compatible_store_defaults_off, a0_openai_compatible_reasoning_floor, call_fn_resolved_model_pins_provider, call_fn_auto_model_keeps_role_slot_routing, inference_tool_repeat_fingerprint_ignores_transport_ids, inference_compatible_provider_receives_classified_role, inference_fanout_preserves_requested_provider, inference_auto_route_gates_and_reports_effective_provider, inference_explicit_openai_model_is_pinned, inference_compatible_provider_approval_gate, inference_compatible_transport_uses_effective_owner, tool_dispatch_enforces_approval_scope, approval_gate_uses_current_user_turn, approval_gate_replay_is_scope_bound, approval_gate_dispatch_denial_becomes_pending, openai_compatible_responses_preserves_reasoning_items, openai_stateless_reasoning_is_replayable, openai_compatible_caller_provider_is_scoped, cheap_provider_prefers_configured_low_cost_provider, catalog_routed_model_tier_follows_concrete_owner, chat_approval_replay_preserves_provider_pin, chat_routed_tier_denial_is_clean_403 +# proves: a0_openai_compatible_error_suppresses_secret_cause, a0_openai_compatible_store_defaults_off, a0_openai_compatible_reasoning_floor, a0_openai_compatible_explicit_none_reasoning, call_fn_resolved_model_pins_provider, call_fn_auto_model_keeps_role_slot_routing, inference_tool_repeat_fingerprint_ignores_transport_ids, inference_compatible_provider_receives_classified_role, inference_fanout_preserves_requested_provider, inference_auto_route_gates_and_reports_effective_provider, inference_explicit_openai_model_is_pinned, inference_legacy_model_alias_memory_survives, inference_compatible_provider_approval_gate, inference_compatible_transport_uses_effective_owner, tool_dispatch_enforces_approval_scope, approval_gate_uses_current_user_turn, approval_gate_replay_is_scope_bound, approval_gate_dispatch_denial_becomes_pending, openai_compatible_responses_preserves_reasoning_items, openai_stateless_reasoning_is_replayable, openai_compatible_caller_provider_is_scoped, openai_compatible_explicit_none_reasoning, openai_compatible_responses_formats_vision_input, cheap_provider_prefers_configured_low_cost_provider, deprecated_provider_aliases_canonicalize, catalog_routed_model_tier_follows_concrete_owner, catalog_legacy_model_aliases_canonicalize, chat_approval_replay_preserves_provider_pin, chat_routed_tier_denial_is_clean_403 # call: self::check_openai_compatible_repair_regressions # requires: python3, pytest # timeout: 60 @@ -48,8 +48,8 @@ def check_openai_compatible_registry_wiring() -> None: root = Path(__file__).resolve().parents[2] registry_text = (root / "python/config/providers.json").read_text(encoding="utf-8") - assert "deepseek-v4-flash" in registry_text - assert "deepseek-v4-pro" in registry_text + assert '"model": "deepseek-flash"' in registry_text + assert '"deprecated_alias_for": "deepseek"' in registry_text assert "deepseek-chat" not in registry_text assert "deepseek-reasoner" not in registry_text assert not (root / "python/services/providers/deepseek_provider.py").exists() @@ -74,7 +74,7 @@ async def create(**request): environment = {"DEEPSEEK_API_KEY": "witness-secret", "A0_PROVIDER": "deepseek-pro"} with patch.dict(os.environ, environment, clear=False): provider_id, spec = resolve_openai_compatible_provider() - assert provider_id == "deepseek-pro" + assert provider_id == "deepseek" with patch.object(standalone_adapter, "OpenAI", FakeSyncOpenAI): result = standalone_adapter.OpenAICompatibleAdapter(provider_id, spec).complete( [{"role": "user", "content": "hi"}] @@ -85,7 +85,7 @@ async def create(**request): async def run_service_call(): return await service_adapter.call( [{"role": "user", "content": "hi"}], - provider_id="deepseek-pro", + provider_id="deepseek", use_tools=False, ) @@ -96,7 +96,7 @@ async def run_service_call(): assert usage["output_tokens"] == 1 assert captured["sync_client"]["base_url"] == "https://api.deepseek.com" assert captured["async_client"]["base_url"] == "https://api.deepseek.com" - assert captured["async_request"]["model"] == "deepseek-v4-pro" + assert captured["async_request"]["model"] == "deepseek-flash" def check_openai_compatible_repair_regressions() -> None: @@ -118,10 +118,11 @@ def check_openai_compatible_repair_regressions() -> None: f"{routing_tests}::test_call_model_pins_the_explicit_model_provider", f"{routing_tests}::test_explicit_model_pin_ignores_cross_tier_role_override", f"{routing_tests}::test_cheap_provider_prefers_deepseek_before_expensive_fallback", + f"{routing_tests}::test_deepseek_none_reasoning_is_explicit", f"{routing_tests}::test_approval_replays_preserve_explicit_provider_pin", f"{routing_tests}::test_call_model_leaves_auto_selected_provider_unpinned", - f"{routing_tests}::test_auto_role_route_reapplies_tier_and_reports_effective_provider", - f"{routing_tests}::test_role_model_override_uses_concrete_owner_tier_and_provenance", + f"{routing_tests}::test_legacy_provider_slot_canonicalizes_to_current_deepseek_route", + f"{routing_tests}::test_legacy_model_env_override_canonicalizes_to_current_flash", f"{routing_tests}::test_agent_instance_caches_effective_routed_provider", f"{routing_tests}::test_explicit_openai_model_reaches_legacy_routed_branch", f"{provider_tests}::test_openai_approval_usage_retains_concrete_model", @@ -136,6 +137,11 @@ def check_openai_compatible_repair_regressions() -> None: f"{approval_tests}::test_legacy_openai_route_transports_through_concrete_owner", f"{approval_tests}::test_pending_replay_pins_every_resolved_model", f"{adapter_tests}::test_adapter_uses_registry_transport_and_sanitizes_failure", + f"{adapter_tests}::test_multi_provider_selection_canonicalizes_and_dedupes_aliases", + f"{adapter_tests}::test_hidden_provider_aliases_are_omitted_from_registry_rosters", + f"{adapter_tests}::test_responses_formatter_converts_chat_style_vision_parts", + f"{adapter_tests}::test_legacy_model_instance_memory_survives_canonical_routing", + f"{adapter_tests}::test_edcm_slot_uses_catalog_alias_resolution", ] environment = dict(os.environ) environment["PYTHONDONTWRITEBYTECODE"] = "1" @@ -157,4 +163,4 @@ def check_openai_compatible_repair_regressions() -> None: def test_openai_compatible_registry_wiring() -> None: check_openai_compatible_registry_wiring() -# 117:19 0:0 0:0 +# 123:19 0:0 0:0 diff --git a/tests/test_aone_open_comp_adap_v0.0.0alpha.py b/tests/test_aone_open_comp_adap_v0.0.0alpha.py index 1b781b46..11dcc725 100644 --- a/tests/test_aone_open_comp_adap_v0.0.0alpha.py +++ b/tests/test_aone_open_comp_adap_v0.0.0alpha.py @@ -1,6 +1,10 @@ -# 121:1 0:0 0:0 +# 187:1 0:0 0:0 """Standalone a0 selection and OpenAI-compatible adapter contracts.""" +from contextlib import asynccontextmanager +from pathlib import Path +import sys + from types import SimpleNamespace import pytest @@ -15,7 +19,7 @@ def model_dump(self) -> dict: return {"output": [], "usage": {"input_tokens": 1, "output_tokens": 1}} -def test_registry_auto_selects_flash_and_explicitly_selects_pro( +def test_registry_auto_selects_flash_and_canonicalizes_legacy_pro_alias( monkeypatch: pytest.MonkeyPatch, ) -> None: from a0 import resolve_openai_compatible_provider @@ -24,12 +28,91 @@ def test_registry_auto_selects_flash_and_explicitly_selects_pro( monkeypatch.setenv("DEEPSEEK_API_KEY", "test-secret") provider_id, spec = resolve_openai_compatible_provider() assert provider_id == "deepseek" - assert spec["model"] == "deepseek-v4-flash" + assert spec["model"] == "deepseek-flash" + assert spec["supports_vision"] is True monkeypatch.setenv("A0_PROVIDER", "deepseek-pro") provider_id, spec = resolve_openai_compatible_provider() - assert provider_id == "deepseek-pro" - assert spec["model"] == "deepseek-v4-pro" + assert provider_id == "deepseek" + assert spec["model"] == "deepseek-flash" + + +@pytest.mark.asyncio +async def test_multi_provider_selection_canonicalizes_and_dedupes_aliases() -> None: + from python.services.energy_registry import resolve_providers + + assert await resolve_providers(["deepseek-pro", "deepseek"]) == ["deepseek"] + + +def test_hidden_provider_aliases_are_omitted_from_registry_rosters() -> None: + from python.services.energy_registry import BUILTIN_PROVIDERS + from python.services.model_catalog import visible_provider_specs + + visible = visible_provider_specs(BUILTIN_PROVIDERS) + assert "deepseek" in visible + assert "deepseek-pro" not in visible + source = (Path(__file__).parents[1] / "python/routes/instances_api.py").read_text() + assert "providers = visible_provider_specs" in source + + +def test_responses_formatter_converts_chat_style_vision_parts() -> None: + from python.services.providers.openai_compatible_provider import ( + _format_responses_messages, + ) + + messages = [{"role": "user", "content": [ + {"type": "text", "text": "inspect"}, + {"type": "image_url", "image_url": {"url": "data:image/png;base64,eA=="}}, + ]}] + assert _format_responses_messages(messages)[0]["content"] == [ + {"type": "input_text", "text": "inspect"}, + {"type": "input_image", "image_url": "data:image/png;base64,eA=="}, + ] + + +@pytest.mark.asyncio +async def test_legacy_model_instance_memory_survives_canonical_routing( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from python.services import inference + + calls: list[tuple[str, dict]] = [] + + class Result: + def __init__(self, *, first=None, rows=None): + self._first = first + self._rows = rows or [] + + def mappings(self): + return self + + def first(self): + return self._first + + def all(self): + return self._rows + + class Session: + async def execute(self, statement, params): + calls.append((str(statement), params)) + if len(calls) == 1: + return Result(first={"id": "legacy", "swarm_context": "swarm"}) + return Result(rows=[{"tier": "seed", "content": "memory"}]) + + @asynccontextmanager + async def get_session(): + yield Session() + + monkeypatch.setitem(sys.modules, "python.database", SimpleNamespace(get_session=get_session)) + assert await inference._instance_memory_block("deepseek") == "swarm\n[SEED] memory" + assert calls[0][1]["mid"] == "deepseek-flash" + assert "deepseek-v4-pro" in calls[0][1]["mids"] + assert "deepseek-pro" in calls[0][1]["mids"] + + +def test_edcm_slot_uses_catalog_alias_resolution() -> None: + source = (Path(__file__).parents[1] / "python/services/edcm_explainer.py").read_text() + assert "resolved_provider_id, _ = await resolve_model_id" in source def test_explicit_unknown_and_noncompatible_provider_fail_closed( @@ -72,20 +155,26 @@ def create(**request): result = adapter.complete([{"role": "user", "content": "hi"}]) assert result["text"] == "standalone-ok" - assert result["raw"]["provider"] == "deepseek-pro" + assert result["raw"]["provider"] == "deepseek" assert "test-secret" not in repr(result) assert captured["client"] == { "api_key": "test-secret", "base_url": "https://api.deepseek.com", } - assert captured["request"]["model"] == "deepseek-v4-pro" + assert captured["request"]["model"] == "deepseek-flash" assert captured["request"]["reasoning"] == {"effort": "high"} + adapter.complete( + [{"role": "user", "content": "disable thinking"}], + reasoning_effort="none", + ) + assert captured["request"]["reasoning"] == {"effort": "none"} + def fail_with_secret(**request): raise RuntimeError("upstream echoed test-secret") adapter._client.responses.create = fail_with_secret - with pytest.raises(RuntimeError, match="deepseek-pro request failed") as caught: + with pytest.raises(RuntimeError, match="deepseek request failed") as caught: adapter.complete([{"role": "user", "content": "fail"}]) assert caught.value.__cause__ is None assert "test-secret" not in str(caught.value) @@ -94,7 +183,7 @@ def value_error_with_secret(**request): raise ValueError("upstream echoed test-secret") adapter._client.responses.create = value_error_with_secret - with pytest.raises(RuntimeError, match="deepseek-pro request failed") as caught: + with pytest.raises(RuntimeError, match="deepseek request failed") as caught: adapter.complete([{"role": "user", "content": "fail-value"}]) assert caught.value.__cause__ is None assert "test-secret" not in str(caught.value) @@ -159,4 +248,4 @@ def test_explicit_provider_missing_key_fails_closed( with pytest.raises(ValueError, match="DEEPSEEK_API_KEY not configured"): _select_adapter(request) -# 121:1 0:0 0:0 +# 187:1 0:0 0:0 diff --git a/tests/test_open_comp_prov_v0.0.0alpha.py b/tests/test_open_comp_prov_v0.0.0alpha.py index 4c878ab7..e72cd150 100644 --- a/tests/test_open_comp_prov_v0.0.0alpha.py +++ b/tests/test_open_comp_prov_v0.0.0alpha.py @@ -1,4 +1,4 @@ -# 400:1 0:0 0:0 +# 399:1 0:0 0:0 """Contract tests for registry-driven OpenAI-compatible providers.""" from pathlib import Path @@ -109,7 +109,7 @@ async def transport_must_not_run(*_args, **_kwargs): assert content.startswith("[APPROVAL REQUIRED") assert usage["approval_state"] == "pending" assert usage["provider_id"] == "deepseek" - assert usage["model_id"] == "deepseek-v4-flash" + assert usage["model_id"] == "deepseek-flash" def test_repeat_fingerprint_excludes_volatile_transport_ids() -> None: @@ -154,8 +154,7 @@ def test_deepseek_is_configuration_not_a_provider_specific_adapter() -> None: assert flash["adapter"] == pro["adapter"] == "openai-compatible" assert flash["base_url"] == pro["base_url"] == "https://api.deepseek.com" assert flash["api_key_env"] == pro["api_key_env"] == "DEEPSEEK_API_KEY" - assert flash["model"] == "deepseek-v4-flash" - assert pro["model"] == "deepseek-v4-pro" + assert flash["model"] == pro["model"] == "deepseek-flash" registry_text = (root / "python/config/providers.json").read_text(encoding="utf-8") assert "deepseek-chat" not in registry_text @@ -198,7 +197,7 @@ async def create(**request): try: content, usage = await provider.call( [{"role": "user", "content": "hi"}], - provider_id="deepseek-pro", + provider_id="deepseek", use_tools=False, reasoning_effort="medium", ) @@ -213,7 +212,7 @@ async def create(**request): "base_url": "https://api.deepseek.com", } request = captured["requests"][0] - assert request["model"] == "deepseek-v4-pro" + assert request["model"] == "deepseek-flash" assert request["reasoning"] == {"effort": "high"} assert "store" not in request assert "tools" not in request @@ -243,18 +242,18 @@ async def create(**request): monkeypatch.setattr(provider, "AsyncOpenAI", FakeAsyncOpenAI) monkeypatch.setenv("DEEPSEEK_API_KEY", "test-secret") - monkeypatch.setenv("DEEPSEEK_MODEL_CONDUCT", "deepseek-v4-flash-override") + monkeypatch.setenv("DEEPSEEK_MODEL_CONDUCT", "deepseek-flash-override") content, _ = await provider.call( [{"role": "user", "content": "hi"}], provider_id="deepseek", role="conduct", - model_override="deepseek-v4-flash", + model_override="deepseek-flash", use_tools=False, ) assert content == "override-ok" - assert captured["model"] == "deepseek-v4-flash-override" + assert captured["model"] == "deepseek-flash-override" @pytest.mark.asyncio @@ -491,4 +490,4 @@ async def create(**request): assert "[redacted]" in content -# 400:1 0:0 0:0 +# 399:1 0:0 0:0 diff --git a/tests/test_open_comp_rout_v0.0.0alpha.py b/tests/test_open_comp_rout_v0.0.0alpha.py index 6c74d146..c6f2bccd 100644 --- a/tests/test_open_comp_rout_v0.0.0alpha.py +++ b/tests/test_open_comp_rout_v0.0.0alpha.py @@ -1,4 +1,4 @@ -# 398:1 0:0 0:0 +# 399:1 0:0 0:0 """Routing and catalog tests for registry-driven compatible providers.""" import ast @@ -26,6 +26,17 @@ def _clear_provider_keys(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv(api_key_env, raising=False) +def test_deepseek_none_reasoning_is_explicit() -> None: + from python.services.providers import openai_compatible_provider + kwargs = openai_compatible_provider._responses_kwargs( + model="deepseek-flash", input_items=[], max_output_tokens=8, temperature=1.0, + reasoning_effort="none", explicit_none_reasoning=True, + store=False, supports_store=False, tools=None, + ) + assert kwargs["reasoning"] == {"effort": "none"} + assert "include" not in kwargs + + def test_cheap_provider_prefers_deepseek_before_expensive_fallback( monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -126,7 +137,7 @@ async def call_provider(**kwargs): @pytest.mark.asyncio -async def test_auto_role_route_reapplies_tier_and_reports_effective_provider( +async def test_legacy_provider_slot_canonicalizes_to_current_deepseek_route( monkeypatch: pytest.MonkeyPatch, ) -> None: from python.services import inference, openai_router @@ -142,17 +153,11 @@ async def no_memory(_provider_id: str): monkeypatch.setattr(inference, "_slot_routing_info", practice_slot) monkeypatch.setattr(inference, "_instance_memory_block", no_memory) - with pytest.raises(PermissionError, match="requires tier 'ws'"): - await inference.call_provider( - "deepseek", - [{"role": "user", "content": "practice this"}], - use_tools=False, - routed_user_tier="free", - ) - monkeypatch.setenv("DEEPSEEK_API_KEY", "test-secret") + captured: dict = {} async def fake_call(messages, **kwargs): + captured.update(kwargs) return "role-routed", {"total_tokens": 1} monkeypatch.setattr(provider, "call", fake_call) @@ -160,15 +165,18 @@ async def fake_call(messages, **kwargs): "deepseek", [{"role": "user", "content": "practice this"}], use_tools=False, - routed_user_tier="ws", + routed_user_tier="free", ) assert content == "role-routed" - assert usage["provider_id"] == "deepseek-pro" + assert captured["provider_id"] == "deepseek" + assert captured["model_override"] == "deepseek-flash" + assert usage["provider_id"] == "deepseek" + assert usage["model_id"] == "deepseek-flash" @pytest.mark.asyncio -async def test_role_model_override_uses_concrete_owner_tier_and_provenance( +async def test_legacy_model_env_override_canonicalizes_to_current_flash( monkeypatch: pytest.MonkeyPatch, ) -> None: from python.services import inference, openai_router @@ -184,15 +192,6 @@ async def no_memory(_provider_id: str): monkeypatch.setattr(inference, "_slot_routing_info", no_slot) monkeypatch.setattr(inference, "_instance_memory_block", no_memory) monkeypatch.setenv("DEEPSEEK_MODEL_PRACTICE", "deepseek-v4-pro") - - with pytest.raises(PermissionError, match="deepseek-v4-pro.*tier 'ws'"): - await inference.call_provider( - "deepseek", - [{"role": "user", "content": "practice this"}], - use_tools=False, - routed_user_tier="free", - ) - captured: dict = {} monkeypatch.setenv("DEEPSEEK_API_KEY", "test-secret") @@ -205,15 +204,15 @@ async def fake_call(messages, **kwargs): "deepseek", [{"role": "user", "content": "practice this"}], use_tools=False, - routed_user_tier="ws", + routed_user_tier="free", ) assert content == "pro-routed" - assert captured["provider_id"] == "deepseek-pro" - assert captured["model_override"] == "deepseek-v4-pro" + assert captured["provider_id"] == "deepseek" + assert captured["model_override"] == "deepseek-flash" assert captured["pin_model_override"] is True - assert usage["provider_id"] == "deepseek-pro" - assert usage["model_id"] == "deepseek-v4-pro" + assert usage["provider_id"] == "deepseek" + assert usage["model_id"] == "deepseek-flash" @pytest.mark.asyncio @@ -223,17 +222,17 @@ async def test_agent_instance_caches_effective_routed_provider( from python.services import agent_instance async def fake_call_model(*args, **kwargs): - return "role-routed", {"provider_id": "deepseek-pro"} + return "role-routed", {"provider_id": "deepseek"} monkeypatch.setattr(agent_instance, "call_model", fake_call_model) - instance = agent_instance.AgentInstance(model_id="deepseek-v4-flash") + instance = agent_instance.AgentInstance(model_id="deepseek-flash") await instance.run( [{"role": "user", "content": "practice this"}], pin_requested_provider=False, ) - assert instance.provider_id == "deepseek-pro" + assert instance.provider_id == "deepseek" @pytest.mark.asyncio @@ -267,7 +266,7 @@ async def fake_openai_routed(messages, system_prompt=None, **kwargs): @pytest.mark.asyncio -async def test_free_catalog_does_not_surface_cross_tier_deepseek_pro( +async def test_catalog_hides_legacy_deepseek_pro_provider_alias( monkeypatch: pytest.MonkeyPatch, ) -> None: from python.services import energy_registry, model_catalog @@ -284,10 +283,11 @@ async def active_provider(): catalog = await model_catalog.list_models_for_user(None) flash = next(item for item in catalog["providers"] if item["provider_id"] == "deepseek") - pro = next(item for item in catalog["providers"] if item["provider_id"] == "deepseek-pro") - assert "deepseek-v4-pro" not in {item["model_id"] for item in flash["models"]} - assert pro["tier_blocked"] is True + assert {item["model_id"] for item in flash["models"]} == {"deepseek-flash"} + assert "deepseek-pro" not in { + item["provider_id"] for item in catalog["providers"] + } @pytest.mark.asyncio @@ -351,10 +351,10 @@ async def fake_call(messages, **kwargs): assert usage == { "total_tokens": 1, "provider_id": "deepseek", - "model_id": "deepseek-v4-flash", + "model_id": "deepseek-flash", } assert captured["provider_id"] == "deepseek" - assert captured["model_override"] == "deepseek-v4-flash" + assert captured["model_override"] == "deepseek-flash" assert captured["role"] == "practice" @@ -373,7 +373,7 @@ async def conflicting_slot(_slot: str) -> tuple[str, str]: return "wrong-slot-memory", "openai" async def selected_memory(provider_id: str) -> str: - assert provider_id == "deepseek-pro" + assert provider_id == "deepseek" return "selected-provider-memory" captured: dict = {} @@ -393,7 +393,7 @@ async def fake_call(messages, **kwargs): ) assert content == "fanout-ok" - assert captured["provider_id"] == "deepseek-pro" + assert captured["provider_id"] == "deepseek" system_text = captured["messages"][0]["content"] assert "selected-provider-memory" in system_text assert "wrong-slot-memory" not in system_text @@ -429,7 +429,7 @@ async def fake_call(messages, **kwargs): monkeypatch.setattr(provider, "call", fake_call) content, _ = await call_fn.call_model( - "deepseek-v4-flash", + "deepseek-flash", [{"role": "user", "content": "practice this"}], enforce_tier=False, enforce_enabled=False, @@ -473,13 +473,13 @@ async def create(**request): [{"role": "user", "content": "practice this"}], provider_id="deepseek", role="practice", - model_override="deepseek-v4-flash", + model_override="deepseek-flash", pin_model_override=True, use_tools=False, ) assert content == "pinned" - assert captured["model"] == "deepseek-v4-flash" + assert captured["model"] == "deepseek-flash" @pytest.mark.asyncio @@ -494,13 +494,13 @@ async def test_catalog_resolver_pricing_and_missing_key_are_fail_closed( _clear_provider_keys(monkeypatch) assert routed_model_owner(make_call_config("practice")["model"], "openai", "free") == "openai" assert routed_model_owner(make_call_config("record")["model"], "openai", "free") == "openai-nano" - provider_id, spec = await resolve_model_id("deepseek-v4-pro") - assert provider_id == "deepseek-pro" - assert spec["model"] == "deepseek-v4-pro" - assert get_model_pricing("deepseek", "deepseek-v4-flash")["input_per_1m"] == 0.44 + for legacy_id in ("deepseek-pro", "deepseek-v4-pro"): + provider_id, spec = await resolve_model_id(legacy_id) + assert (provider_id, spec["model"]) == ("deepseek", "deepseek-flash") + assert get_model_pricing("deepseek", "deepseek-flash")["input_per_1m"] == 0.3 assert estimate_cost( - "deepseek", 1_000_000, 1_000_000, model="deepseek-v4-flash" - ) == pytest.approx(1.76) + "deepseek", 1_000_000, 1_000_000, model="deepseek-flash" + ) == pytest.approx(1.5) with pytest.raises(ValueError, match="DEEPSEEK_API_KEY not configured"): await provider.call( @@ -508,4 +508,4 @@ async def test_catalog_resolver_pricing_and_missing_key_are_fail_closed( provider_id="deepseek", use_tools=False, ) -# 398:1 0:0 0:0 +# 399:1 0:0 0:0