apsimo 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apsimo/__init__.py +38 -0
- apsimo/__main__.py +6 -0
- apsimo/agent/__init__.py +6 -0
- apsimo/agent/client.py +276 -0
- apsimo/agent/models.py +46 -0
- apsimo/agents/__init__.py +20 -0
- apsimo/agents/models.py +264 -0
- apsimo/agents/store.py +861 -0
- apsimo/agents/websocket.py +522 -0
- apsimo/api/__init__.py +1 -0
- apsimo/api/auth_telemetry.py +287 -0
- apsimo/api/authority.py +1203 -0
- apsimo/api/contact_grants.py +347 -0
- apsimo/api/middleware.py +483 -0
- apsimo/api/routers/__init__.py +1 -0
- apsimo/api/routers/commitment_work.py +265 -0
- apsimo/api/routers/context_gate.py +123 -0
- apsimo/api/routers/executions.py +140 -0
- apsimo/api/routers/followup_plans.py +147 -0
- apsimo/api/routers/governed_actions.py +162 -0
- apsimo/api/routers/host.py +14473 -0
- apsimo/api/routers/initiative_work.py +115 -0
- apsimo/api/routers/mining.py +104 -0
- apsimo/api/routers/observations.py +110 -0
- apsimo/api/routers/social_state.py +225 -0
- apsimo/api/routers/task_queue.py +2715 -0
- apsimo/api/routers/temporal_followups.py +251 -0
- apsimo/api/routers/transport.py +110 -0
- apsimo/api/routers/transport_ingress_api.py +240 -0
- apsimo/api/schemas/__init__.py +1 -0
- apsimo/api/schemas/host.py +1949 -0
- apsimo/autonomy/cli.py +110 -0
- apsimo/autonomy/condition_worker.py +437 -0
- apsimo/autonomy/config.py +424 -0
- apsimo/autonomy/loop.py +4316 -0
- apsimo/autonomy/registry.py +339 -0
- apsimo/autonomy/scheduler.py +1822 -0
- apsimo/autonomy/synthesis.py +449 -0
- apsimo/backup.py +962 -0
- apsimo/beliefs/__init__.py +23 -0
- apsimo/beliefs/contradictions.py +109 -0
- apsimo/beliefs/decay.py +61 -0
- apsimo/beliefs/engine.py +479 -0
- apsimo/beliefs/models.py +67 -0
- apsimo/beliefs/promotion.py +41 -0
- apsimo/beliefs/resolve.py +58 -0
- apsimo/beliefs/source_claims.py +690 -0
- apsimo/beliefs/source_projection.py +883 -0
- apsimo/beliefs/source_time.py +208 -0
- apsimo/beliefs/store.py +133 -0
- apsimo/briefings/aggregators.py +824 -0
- apsimo/briefings/composer.py +420 -0
- apsimo/briefings/config.py +55 -0
- apsimo/briefings/delivery.py +439 -0
- apsimo/briefings/engagement.py +97 -0
- apsimo/briefings/engine.py +274 -0
- apsimo/briefings/enhancer.py +99 -0
- apsimo/briefings/models.py +183 -0
- apsimo/briefings/scheduler.py +382 -0
- apsimo/briefings/store.py +435 -0
- apsimo/chain/__init__.py +48 -0
- apsimo/chain/block.py +100 -0
- apsimo/chain/cli.py +704 -0
- apsimo/chain/genesis.py +443 -0
- apsimo/chain/identity.py +416 -0
- apsimo/chain/keys.py +1025 -0
- apsimo/chain/local_keys.py +187 -0
- apsimo/chain/manager.py +290 -0
- apsimo/chain/node.py +163 -0
- apsimo/chain/plugin_transactions.py +371 -0
- apsimo/chain/protocol.py +220 -0
- apsimo/chain/state_machine.py +676 -0
- apsimo/chain/storage.py +503 -0
- apsimo/chain/transactions.py +250 -0
- apsimo/chain/validation.py +397 -0
- apsimo/channels/__init__.py +1 -0
- apsimo/channels/manifest.py +31 -0
- apsimo/channels/migrations/001_channels_schema.sql +12 -0
- apsimo/channels/phone_gateways.py +42 -0
- apsimo/channels/presence.py +188 -0
- apsimo/channels/router.py +235 -0
- apsimo/channels/store.py +231 -0
- apsimo/cli.py +2688 -0
- apsimo/cognition/__init__.py +11 -0
- apsimo/cognition/charter.py +398 -0
- apsimo/cognition/drive_governance.py +3530 -0
- apsimo/cognition/evidence_pipeline.py +1627 -0
- apsimo/cognition/external_events.py +932 -0
- apsimo/cognition/goal_spine.py +3488 -0
- apsimo/cognition/introspection.py +214 -0
- apsimo/cognition/prompt.py +150 -0
- apsimo/cognition/runtime.py +108 -0
- apsimo/cognition/trigger.py +154 -0
- apsimo/commitments/__init__.py +18 -0
- apsimo/commitments/local_work.py +355 -0
- apsimo/commitments/store.py +1052 -0
- apsimo/commitments/work.py +91 -0
- apsimo/compat.py +53 -0
- apsimo/compression/__init__.py +467 -0
- apsimo/connectors/__init__.py +21 -0
- apsimo/connectors/base.py +152 -0
- apsimo/connectors/caldav_calendar.py +125 -0
- apsimo/connectors/fs_documents.py +85 -0
- apsimo/connectors/imap_email.py +138 -0
- apsimo/connectors/manager.py +218 -0
- apsimo/connectors/webhook_pull.py +88 -0
- apsimo/contacts/__init__.py +33 -0
- apsimo/contacts/comms.py +357 -0
- apsimo/contacts/config.py +79 -0
- apsimo/contacts/exporters/__init__.py +1 -0
- apsimo/contacts/exporters/vcard.py +71 -0
- apsimo/contacts/identity_links.py +251 -0
- apsimo/contacts/importer.py +280 -0
- apsimo/contacts/importers/__init__.py +1 -0
- apsimo/contacts/importers/batch.py +43 -0
- apsimo/contacts/importers/macos_contacts.py +101 -0
- apsimo/contacts/migrations/001_contacts_schema.sql +141 -0
- apsimo/contacts/migrations/002_trust_scopes.sql +36 -0
- apsimo/contacts/migrations/003_open_gateway_enum.sql +32 -0
- apsimo/contacts/migrations/004_contact_provision_operations.sql +18 -0
- apsimo/contacts/migrations/005_identity_links.sql +27 -0
- apsimo/contacts/models.py +308 -0
- apsimo/contacts/scoring.py +16 -0
- apsimo/contacts/store.py +1623 -0
- apsimo/contacts/transport_ingress.py +252 -0
- apsimo/contacts/world_bridge.py +314 -0
- apsimo/contextgate/__init__.py +69 -0
- apsimo/contextgate/chunker.py +169 -0
- apsimo/contextgate/estimate.py +54 -0
- apsimo/contextgate/gate.py +313 -0
- apsimo/contextgate/retrieve.py +115 -0
- apsimo/delivery/__init__.py +16 -0
- apsimo/delivery/bridge.py +1260 -0
- apsimo/delivery/channels.py +526 -0
- apsimo/delivery/classification.py +50 -0
- apsimo/delivery/rate_limiter.py +268 -0
- apsimo/delivery/reachout_policy.py +206 -0
- apsimo/directed/__init__.py +22 -0
- apsimo/directed/audit.py +167 -0
- apsimo/directed/intake.py +95 -0
- apsimo/directed/models.py +191 -0
- apsimo/directed/service.py +509 -0
- apsimo/directives/__init__.py +25 -0
- apsimo/directives/evidence.py +87 -0
- apsimo/directives/extractor.py +188 -0
- apsimo/directives/guard.py +364 -0
- apsimo/directives/models.py +206 -0
- apsimo/directives/service.py +372 -0
- apsimo/directives/store.py +167 -0
- apsimo/doctor.py +2173 -0
- apsimo/environment.py +43 -0
- apsimo/events/__init__.py +33 -0
- apsimo/events/broadcaster.py +98 -0
- apsimo/events/bus.py +217 -0
- apsimo/events/journal.py +863 -0
- apsimo/events/stream.py +131 -0
- apsimo/events/types.py +150 -0
- apsimo/execution_results.py +357 -0
- apsimo/feedback/__init__.py +5 -0
- apsimo/feedback/store.py +76 -0
- apsimo/feeds/__init__.py +19 -0
- apsimo/feeds/cli.py +84 -0
- apsimo/feeds/engine.py +437 -0
- apsimo/feeds/example-feed.yaml +77 -0
- apsimo/feeds/hermes_cron.py +126 -0
- apsimo/feeds/manager.py +235 -0
- apsimo/feeds/spec.py +250 -0
- apsimo/feeds/template.py +202 -0
- apsimo/gate/__init__.py +18 -0
- apsimo/gate/audit.py +61 -0
- apsimo/gate/communication_policy.py +166 -0
- apsimo/gate/config.py +72 -0
- apsimo/gate/context_provenance.py +170 -0
- apsimo/gate/env_risk.py +226 -0
- apsimo/gate/guard_audit.py +353 -0
- apsimo/gate/layers/__init__.py +1 -0
- apsimo/gate/layers/base.py +15 -0
- apsimo/gate/layers/l1_recipient.py +66 -0
- apsimo/gate/layers/l2_pii.py +134 -0
- apsimo/gate/layers/l3_cross_context.py +50 -0
- apsimo/gate/layers/l4_trust_tier.py +78 -0
- apsimo/gate/layers/l5_injection.py +199 -0
- apsimo/gate/layers/l6_review.py +86 -0
- apsimo/gate/layers/l7_delay.py +100 -0
- apsimo/gate/layers/tom2_epistemic.py +185 -0
- apsimo/gate/models.py +64 -0
- apsimo/gate/pending_dispatch.py +5 -0
- apsimo/gate/pipeline.py +206 -0
- apsimo/gate/rejection.py +259 -0
- apsimo/gate/response_guard.py +700 -0
- apsimo/gate/rulesets/injection_v1.yaml +51 -0
- apsimo/gate/surface_policy.py +189 -0
- apsimo/gate/taint.py +226 -0
- apsimo/genesis.json +9 -0
- apsimo/goals/__init__.py +100 -0
- apsimo/goals/config.py +38 -0
- apsimo/goals/decomposer.py +421 -0
- apsimo/goals/engine.py +617 -0
- apsimo/goals/inference.py +354 -0
- apsimo/goals/models.py +302 -0
- apsimo/goals/priority.py +270 -0
- apsimo/goals/queue_bridge.py +149 -0
- apsimo/goals/replan.py +450 -0
- apsimo/goals/schema.sql +89 -0
- apsimo/goals/store.py +692 -0
- apsimo/governed_actions.py +1708 -0
- apsimo/harness_integration/__init__.py +45 -0
- apsimo/harness_integration/context.py +41 -0
- apsimo/harness_integration/skills.py +231 -0
- apsimo/identity/__init__.py +26 -0
- apsimo/identity/participants.py +181 -0
- apsimo/identity/resolver.py +329 -0
- apsimo/identity_bootstrap/__init__.py +5 -0
- apsimo/identity_bootstrap/builder.py +208 -0
- apsimo/identity_bootstrap/corpus.py +443 -0
- apsimo/identity_bootstrap/models.py +54 -0
- apsimo/identity_bootstrap/runner.py +353 -0
- apsimo/identity_bootstrap/seeders/__init__.py +25 -0
- apsimo/identity_bootstrap/seeders/briefings.py +109 -0
- apsimo/identity_bootstrap/seeders/chain.py +57 -0
- apsimo/identity_bootstrap/seeders/goals.py +128 -0
- apsimo/identity_bootstrap/seeders/memory.py +191 -0
- apsimo/identity_bootstrap/seeders/neo4j_cognition.py +79 -0
- apsimo/identity_bootstrap/seeders/relationship.py +152 -0
- apsimo/identity_bootstrap/seeders/sessions.py +67 -0
- apsimo/identity_bootstrap/seeders/skills.py +92 -0
- apsimo/identity_bootstrap/seeders/task_queue.py +72 -0
- apsimo/identity_bootstrap/seeders/world_model.py +143 -0
- apsimo/identity_bootstrap/self_query.py +92 -0
- apsimo/identity_bootstrap/self_reflection.py +155 -0
- apsimo/identity_bootstrap/skill.py +37 -0
- apsimo/identity_bootstrap/verifier.py +436 -0
- apsimo/initiatives/__init__.py +20 -0
- apsimo/initiatives/action_registry.py +454 -0
- apsimo/initiatives/approval_authority.py +2105 -0
- apsimo/initiatives/approval_policy.py +123 -0
- apsimo/initiatives/assignment.py +263 -0
- apsimo/initiatives/backup_evidence.py +100 -0
- apsimo/initiatives/context_freshness.py +103 -0
- apsimo/initiatives/models.py +318 -0
- apsimo/initiatives/native_work.py +270 -0
- apsimo/initiatives/standing_approvals.py +232 -0
- apsimo/initiatives/store.py +1081 -0
- apsimo/initiatives/temporal_followup.py +410 -0
- apsimo/intelligence/__init__.py +1 -0
- apsimo/intelligence/cognition/__init__.py +24 -0
- apsimo/intelligence/cognition/gap_detector.py +148 -0
- apsimo/intelligence/cognition/metalearner.py +547 -0
- apsimo/intelligence/cognition/metrics_collector.py +217 -0
- apsimo/intelligence/cognition/performance_index.py +299 -0
- apsimo/intelligence/cognition/registry.py +192 -0
- apsimo/intelligence/cognition/strategy_adjuster.py +222 -0
- apsimo/intelligence/cognition/types.py +16 -0
- apsimo/intelligence/components/__init__.py +66 -0
- apsimo/intelligence/components/anomaly_detector.py +413 -0
- apsimo/intelligence/components/initiative_engine.py +2643 -0
- apsimo/intelligence/components/preference_learner.py +521 -0
- apsimo/intelligence/components/research_orchestrator.py +358 -0
- apsimo/intelligence/components/self_directed_thinker.py +221 -0
- apsimo/intelligence/components/self_reflector.py +252 -0
- apsimo/intelligence/components/session_continuity.py +154 -0
- apsimo/intelligence/components/task_planner.py +320 -0
- apsimo/intelligence/components/tool_learner.py +217 -0
- apsimo/intelligence/graph/__init__.py +79 -0
- apsimo/intelligence/graph/client.py +2483 -0
- apsimo/intelligence/graph/consolidator.py +405 -0
- apsimo/intelligence/graph/distiller.py +312 -0
- apsimo/intelligence/graph/migrations.py +129 -0
- apsimo/intelligence/graph/queries.py +248 -0
- apsimo/intelligence/graph/recall.py +281 -0
- apsimo/intelligence/graph/reconciler.py +144 -0
- apsimo/intelligence/graph/schema.py +337 -0
- apsimo/intelligence/graph/selection.py +252 -0
- apsimo/intelligence/learning/__init__.py +17 -0
- apsimo/intelligence/learning/continuous_learner.py +245 -0
- apsimo/intelligence/learning/feedback_store.py +321 -0
- apsimo/intelligence/mind_model/__init__.py +1 -0
- apsimo/intelligence/mind_model/graph_baseline.py +136 -0
- apsimo/intelligence/mind_model/signal_collector.py +361 -0
- apsimo/intelligence/relationships/__init__.py +11 -0
- apsimo/intelligence/relationships/profiler.py +389 -0
- apsimo/intelligence/relationships/scorer.py +560 -0
- apsimo/intelligence/relationships/signal_floor.py +66 -0
- apsimo/intelligence/relationships/trust_tiers.py +300 -0
- apsimo/intelligence/synthesis/__init__.py +40 -0
- apsimo/intelligence/synthesis/connection_discoverer.py +379 -0
- apsimo/intelligence/synthesis/cross_domain_analyzer.py +287 -0
- apsimo/intelligence/synthesis/insight_deliverer.py +171 -0
- apsimo/intelligence/synthesis/insight_store.py +79 -0
- apsimo/intelligence/synthesis/insight_validator.py +183 -0
- apsimo/intelligence/synthesis/novelty_scorer.py +267 -0
- apsimo/intelligence/turn_middleware/__init__.py +15 -0
- apsimo/intelligence/turn_middleware/memory_sync.py +119 -0
- apsimo/mcp/__init__.py +41 -0
- apsimo/mcp/__main__.py +6 -0
- apsimo/mcp/config.py +287 -0
- apsimo/mcp/server.py +501 -0
- apsimo/migrations.py +187 -0
- apsimo/mining/__init__.py +27 -0
- apsimo/mining/corpus.py +239 -0
- apsimo/mining/escalations.py +289 -0
- apsimo/mining/models.py +169 -0
- apsimo/mining/store.py +210 -0
- apsimo/models/__init__.py +30 -0
- apsimo/models/memory.py +80 -0
- apsimo/models/mesh.py +72 -0
- apsimo/models/person.py +104 -0
- apsimo/models/signal.py +108 -0
- apsimo/observations/__init__.py +15 -0
- apsimo/observations/store.py +277 -0
- apsimo/patterns/__init__.py +6 -0
- apsimo/patterns/extract.py +187 -0
- apsimo/patterns/store.py +227 -0
- apsimo/persona/__init__.py +1 -0
- apsimo/persona/engine.py +611 -0
- apsimo/persona/manifest.py +140 -0
- apsimo/projects/__init__.py +28 -0
- apsimo/projects/engine.py +1681 -0
- apsimo/projects/event_outbox.py +188 -0
- apsimo/projects/models.py +216 -0
- apsimo/projects/planner.py +181 -0
- apsimo/projects/store.py +1446 -0
- apsimo/proposals/__init__.py +12 -0
- apsimo/proposals/engine.py +114 -0
- apsimo/proposals/models.py +207 -0
- apsimo/qualification/__init__.py +1 -0
- apsimo/qualification/cases.py +75 -0
- apsimo/qualification/cli.py +51 -0
- apsimo/qualification/memory_cases.py +209 -0
- apsimo/qualification/records.py +92 -0
- apsimo/qualification/report.py +87 -0
- apsimo/qualification/runner.py +311 -0
- apsimo/qualification/structured_cases.py +131 -0
- apsimo/reasoning/__init__.py +13 -0
- apsimo/reasoning/executor.py +506 -0
- apsimo/reasoning/loop.py +373 -0
- apsimo/reasoning/native_tools/__init__.py +16 -0
- apsimo/reasoning/native_tools/calculate.py +141 -0
- apsimo/reasoning/native_tools/file_ops.py +150 -0
- apsimo/reasoning/native_tools/web_search.py +49 -0
- apsimo/reasoning/tool_policy.py +182 -0
- apsimo/redact/__init__.py +176 -0
- apsimo/repos/__init__.py +5 -0
- apsimo/repos/mirrors.py +204 -0
- apsimo/research/__init__.py +41 -0
- apsimo/research/artifact.py +482 -0
- apsimo/research/gatherer.py +387 -0
- apsimo/research/pipeline.py +513 -0
- apsimo/research/search/__init__.py +7 -0
- apsimo/research/search/base.py +41 -0
- apsimo/research/search/brave.py +59 -0
- apsimo/research/search/cache.py +51 -0
- apsimo/research/search/duckduckgo.py +103 -0
- apsimo/research/search/orchestrator.py +119 -0
- apsimo/research/search/serpapi.py +59 -0
- apsimo/research/search/tavily.py +59 -0
- apsimo/research/synthesizer.py +309 -0
- apsimo/router/__init__.py +30 -0
- apsimo/router/complexity_scorer.py +148 -0
- apsimo/router/endpoints.py +153 -0
- apsimo/router/fallback.py +58 -0
- apsimo/router/functions.py +243 -0
- apsimo/router/native_policy.py +52 -0
- apsimo/router/router.py +762 -0
- apsimo/router/self_learning.py +174 -0
- apsimo/router/tiers.py +677 -0
- apsimo/sandbox/__init__.py +21 -0
- apsimo/sandbox/backend.py +195 -0
- apsimo/sandbox/manager.py +173 -0
- apsimo/scope_bounds.py +7 -0
- apsimo/secrets/__init__.py +6 -0
- apsimo/secrets/backends/__init__.py +8 -0
- apsimo/secrets/backends/base.py +42 -0
- apsimo/secrets/backends/env.py +110 -0
- apsimo/secrets/backends/keyring.py +72 -0
- apsimo/secrets/backends/onepassword.py +232 -0
- apsimo/secrets/cli.py +191 -0
- apsimo/secrets/manager.py +160 -0
- apsimo/secrets/migration.py +101 -0
- apsimo/secrets/types.py +98 -0
- apsimo/seed.py +41 -0
- apsimo/self_model/__init__.py +37 -0
- apsimo/self_model/appraisals.py +673 -0
- apsimo/self_model/benchmark.py +1314 -0
- apsimo/self_model/brief.py +40 -0
- apsimo/self_model/event_concerns.py +1128 -0
- apsimo/self_model/execution_forecasts.py +353 -0
- apsimo/self_model/expectations.py +1595 -0
- apsimo/self_model/experiments.py +1150 -0
- apsimo/self_model/journal.py +148 -0
- apsimo/self_model/judgments.py +705 -0
- apsimo/self_model/native_outcomes.py +55 -0
- apsimo/self_model/params.py +220 -0
- apsimo/self_model/perspective.py +246 -0
- apsimo/self_model/reconcile.py +183 -0
- apsimo/self_model/reply_forecasts.py +381 -0
- apsimo/self_model/runtime_forecasts.py +296 -0
- apsimo/self_model/runtime_models.py +67 -0
- apsimo/self_model/settlement.py +207 -0
- apsimo/self_model/situation.py +1731 -0
- apsimo/self_model/store.py +883 -0
- apsimo/self_model/supervised.py +137 -0
- apsimo/self_model/thinker.py +99 -0
- apsimo/self_model/trust.py +388 -0
- apsimo/self_model/workspace.py +2388 -0
- apsimo/server.py +4197 -0
- apsimo/services/__init__.py +1 -0
- apsimo/services/agent_bridge.py +474 -0
- apsimo/services/initiative_executor.py +914 -0
- apsimo/services/instance.py +297 -0
- apsimo/sessions/__init__.py +22 -0
- apsimo/sessions/config.py +13 -0
- apsimo/sessions/context_loader.py +88 -0
- apsimo/sessions/federation_session.py +75 -0
- apsimo/sessions/isolated_session.py +98 -0
- apsimo/sessions/reports.py +84 -0
- apsimo/sessions/store.py +148 -0
- apsimo/setup.py +2818 -0
- apsimo/setup_hermes.py +879 -0
- apsimo/setup_local_work.py +218 -0
- apsimo/setup_native_goals.py +134 -0
- apsimo/setup_native_reviews.py +115 -0
- apsimo/skills/__init__.py +10 -0
- apsimo/skills/base.py +108 -0
- apsimo/skills/budget.py +28 -0
- apsimo/skills/executor.py +493 -0
- apsimo/skills/executors/__init__.py +1 -0
- apsimo/skills/executors/behavioral_correction.py +75 -0
- apsimo/skills/executors/capability_gap.py +38 -0
- apsimo/skills/executors/data_quality.py +163 -0
- apsimo/skills/executors/knowledge_acquisition.py +41 -0
- apsimo/skills/executors/operational_hygiene.py +185 -0
- apsimo/skills/executors/subsystem_health.py +169 -0
- apsimo/skills/hermes_export.py +431 -0
- apsimo/skills/index.py +123 -0
- apsimo/skills/learning/__init__.py +21 -0
- apsimo/skills/learning/novelty_detector.py +206 -0
- apsimo/skills/learning/pattern_extractor.py +199 -0
- apsimo/skills/learning/triggers.py +159 -0
- apsimo/skills/loader.py +246 -0
- apsimo/skills/migrations/002_progressive_loading.sql +6 -0
- apsimo/skills/migrations/backfill_triggers.py +20 -0
- apsimo/skills/models.py +202 -0
- apsimo/skills/packager.py +128 -0
- apsimo/skills/protocols.py +70 -0
- apsimo/skills/registry.py +191 -0
- apsimo/skills/runtime.py +58 -0
- apsimo/skills/sandbox_runner.py +229 -0
- apsimo/skills/scheduler.py +129 -0
- apsimo/skills/schema.py +79 -0
- apsimo/skills/security/__init__.py +12 -0
- apsimo/skills/security/guards.py +53 -0
- apsimo/skills/security/scanner.py +223 -0
- apsimo/skills_memory/__init__.py +26 -0
- apsimo/skills_memory/distill.py +159 -0
- apsimo/skills_memory/models.py +85 -0
- apsimo/skills_memory/retrieve.py +62 -0
- apsimo/skills_memory/store.py +172 -0
- apsimo/surprise/__init__.py +6 -0
- apsimo/surprise/accumulation.py +57 -0
- apsimo/surprise/scorer.py +102 -0
- apsimo/surprise/store.py +203 -0
- apsimo/task_queue/__init__.py +69 -0
- apsimo/task_queue/action_receipts.py +148 -0
- apsimo/task_queue/approval_relay_canary.py +108 -0
- apsimo/task_queue/config.py +85 -0
- apsimo/task_queue/contract.py +361 -0
- apsimo/task_queue/events.py +130 -0
- apsimo/task_queue/governor.py +1031 -0
- apsimo/task_queue/handlers/__init__.py +16 -0
- apsimo/task_queue/handlers/base.py +37 -0
- apsimo/task_queue/handlers/inference.py +640 -0
- apsimo/task_queue/handlers/monitoring.py +116 -0
- apsimo/task_queue/handlers/registry.py +75 -0
- apsimo/task_queue/handlers/subtask_handler.py +173 -0
- apsimo/task_queue/handlers/system_maintenance.py +147 -0
- apsimo/task_queue/mesh_integration.py +111 -0
- apsimo/task_queue/models.py +317 -0
- apsimo/task_queue/queue_manager.py +8286 -0
- apsimo/task_queue/routing.py +287 -0
- apsimo/task_queue/scheduler.py +252 -0
- apsimo/task_queue/schema.sql +197 -0
- apsimo/task_queue/work_control.py +342 -0
- apsimo/task_queue/worker.py +993 -0
- apsimo/telemetry.py +145 -0
- apsimo/tom/__init__.py +6 -0
- apsimo/tom/affect.py +387 -0
- apsimo/tom/approvals.py +171 -0
- apsimo/tom/arcs.py +896 -0
- apsimo/tom/asymmetry.py +131 -0
- apsimo/tom/eligibility.py +248 -0
- apsimo/tom/engagement.py +214 -0
- apsimo/tom/exposure.py +214 -0
- apsimo/tom/extractor.py +306 -0
- apsimo/tom/fact_adapters.py +144 -0
- apsimo/tom/facts.py +326 -0
- apsimo/tom/integration.py +592 -0
- apsimo/tom/leveled.py +118 -0
- apsimo/tom/levels.py +247 -0
- apsimo/tom/recipient_audit.py +995 -0
- apsimo/tom/recipient_simulator.py +593 -0
- apsimo/tom/source_lineage.py +93 -0
- apsimo/tom/tom2.py +277 -0
- apsimo/tom/visibility.py +559 -0
- apsimo/tom/visibility_store.py +414 -0
- apsimo/tools/__init__.py +0 -0
- apsimo/tools/definitions.py +740 -0
- apsimo/tools/handlers.py +943 -0
- apsimo/toolsmith/__init__.py +26 -0
- apsimo/toolsmith/authority.py +166 -0
- apsimo/toolsmith/engine.py +559 -0
- apsimo/toolsmith/integrity.py +100 -0
- apsimo/toolsmith/miner.py +145 -0
- apsimo/toolsmith/policy.py +110 -0
- apsimo/toolsmith/registry.py +635 -0
- apsimo/turns/__init__.py +17 -0
- apsimo/turns/audio.py +134 -0
- apsimo/turns/documents.py +235 -0
- apsimo/turns/executions.py +486 -0
- apsimo/turns/hermes_history.py +245 -0
- apsimo/turns/hermes_kanban.py +268 -0
- apsimo/turns/hermes_work.py +96 -0
- apsimo/turns/idempotency.py +752 -0
- apsimo/turns/local_work.py +115 -0
- apsimo/turns/media.py +581 -0
- apsimo/turns/reported_workers.py +196 -0
- apsimo/turns/source_annotations.py +283 -0
- apsimo/turns/source_attribution.py +154 -0
- apsimo/turns/source_read.py +351 -0
- apsimo/turns/source_vectors.py +263 -0
- apsimo/turns/video.py +210 -0
- apsimo/util/autonomy_preset.py +220 -0
- apsimo/util/instance.py +92 -0
- apsimo/util/model_output.py +25 -0
- apsimo/util/quiet_hours.py +27 -0
- apsimo/util/session_safety.py +37 -0
- apsimo/util/temporal.py +343 -0
- apsimo/vector/__init__.py +75 -0
- apsimo/vector/backfill.py +171 -0
- apsimo/vector/caption.py +114 -0
- apsimo/vector/collections.py +51 -0
- apsimo/vector/config.py +102 -0
- apsimo/vector/embedder.py +670 -0
- apsimo/vector/image_preprocess.py +406 -0
- apsimo/vector/image_store.py +296 -0
- apsimo/vector/indexes.py +162 -0
- apsimo/vector/migrate.py +334 -0
- apsimo/vector/multimodal_provider.py +417 -0
- apsimo/vector/multimodal_types.py +87 -0
- apsimo/vector/openai_provider.py +119 -0
- apsimo/vector/query.py +49 -0
- apsimo/vector/reranker.py +565 -0
- apsimo/vector/safety_image.py +159 -0
- apsimo/vector/scanner.py +197 -0
- apsimo/vector/setup.py +289 -0
- apsimo/vector/store.py +533 -0
- apsimo/vector/tiers.py +263 -0
- apsimo/work_orders.py +925 -0
- apsimo/workers/__init__.py +21 -0
- apsimo/workers/agent_bridge.py +640 -0
- apsimo/workers/colony_worker.py +382 -0
- apsimo/workers/queue_worker.py +441 -0
- apsimo/workers/skills_sync.py +152 -0
- apsimo/world_model/__init__.py +71 -0
- apsimo/world_model/causal_maintenance.py +131 -0
- apsimo/world_model/causal_policy.py +43 -0
- apsimo/world_model/causal_query.py +125 -0
- apsimo/world_model/confidence.py +54 -0
- apsimo/world_model/config.py +64 -0
- apsimo/world_model/constants.py +97 -0
- apsimo/world_model/entities.py +145 -0
- apsimo/world_model/expectation_resolvers.py +177 -0
- apsimo/world_model/extraction/__init__.py +7 -0
- apsimo/world_model/extraction/base.py +62 -0
- apsimo/world_model/extraction/conversation_extractor.py +262 -0
- apsimo/world_model/extraction/detector.py +74 -0
- apsimo/world_model/extraction/document_extractor.py +78 -0
- apsimo/world_model/extraction/formats/__init__.py +24 -0
- apsimo/world_model/extraction/formats/csv_fmt.py +68 -0
- apsimo/world_model/extraction/formats/html_fmt.py +72 -0
- apsimo/world_model/extraction/formats/json_fmt.py +68 -0
- apsimo/world_model/extraction/formats/pdf.py +43 -0
- apsimo/world_model/extraction/formats/text.py +27 -0
- apsimo/world_model/extraction/llm_extractor.py +164 -0
- apsimo/world_model/extraction/pipeline.py +73 -0
- apsimo/world_model/integrations/__init__.py +5 -0
- apsimo/world_model/integrations/mind_model_bridge.py +115 -0
- apsimo/world_model/integrations/social_intel_bridge.py +120 -0
- apsimo/world_model/jobs/__init__.py +4 -0
- apsimo/world_model/jobs/extraction_job.py +168 -0
- apsimo/world_model/llm_extract.py +572 -0
- apsimo/world_model/neo4j/__init__.py +5 -0
- apsimo/world_model/neo4j/backend.py +654 -0
- apsimo/world_model/observations.py +155 -0
- apsimo/world_model/populator.py +307 -0
- apsimo/world_model/postgres/__init__.py +1 -0
- apsimo/world_model/postgres/backend.py +683 -0
- apsimo/world_model/relationships.py +25 -0
- apsimo/world_model/resolution/__init__.py +13 -0
- apsimo/world_model/resolution/entity_resolver.py +232 -0
- apsimo/world_model/resolution/merge_audit.py +16 -0
- apsimo/world_model/resolution/merge_workflow.py +117 -0
- apsimo/world_model/source_reports.py +121 -0
- apsimo/world_model/sqlite/__init__.py +4 -0
- apsimo/world_model/sqlite/backend.py +855 -0
- apsimo/world_model/sqlite/schema.sql +132 -0
- apsimo/world_model/store.py +545 -0
- apsimo-1.3.0.dist-info/METADATA +78 -0
- apsimo-1.3.0.dist-info/RECORD +614 -0
- apsimo-1.3.0.dist-info/WHEEL +5 -0
- apsimo-1.3.0.dist-info/entry_points.txt +11 -0
- apsimo-1.3.0.dist-info/licenses/LICENSE +21 -0
- apsimo-1.3.0.dist-info/top_level.txt +2 -0
- colony_sidecar/__init__.py +4 -0
apsimo/router/tiers.py
ADDED
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
"""Model tier definitions for the LLM cost router.
|
|
2
|
+
|
|
3
|
+
Tiers are configured by the host (OpenClaw, Hermes, etc.) at startup
|
|
4
|
+
via ``POST /v1/host/configure``. Colony does not manage its own LLM
|
|
5
|
+
credentials — it inherits them from whichever host mounts it.
|
|
6
|
+
|
|
7
|
+
Provider presets define sensible model assignments per tier for known
|
|
8
|
+
providers (Anthropic, OpenAI, ZAI, Ollama, LM Studio, vLLM).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
from dataclasses import dataclass, replace
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ModelTier(Enum):
|
|
24
|
+
HEURISTIC = "heuristic" # No LLM call — rule-based response
|
|
25
|
+
SMALL = "small" # Fast/cheap: haiku-class, gpt-4o-mini
|
|
26
|
+
MEDIUM = "medium" # Balanced: sonnet-class, gpt-4o
|
|
27
|
+
LARGE = "large" # Best quality: opus-class, o3
|
|
28
|
+
VISION = "vision" # Explicit image role; never selected by cost scoring
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class TierConfig:
|
|
33
|
+
tier: ModelTier
|
|
34
|
+
model_id: str # LiteLLM model string
|
|
35
|
+
max_tokens: int
|
|
36
|
+
cost_per_1k_input: float # USD
|
|
37
|
+
cost_per_1k_output: float
|
|
38
|
+
latency_p50_ms: int # approximate
|
|
39
|
+
# Per-tier endpoint overrides (empty = inherit the provider-wide
|
|
40
|
+
# endpoint configured via OPENAI_API_BASE / provider defaults).
|
|
41
|
+
# These let different tiers live on different servers — e.g. a fast
|
|
42
|
+
# small model on one endpoint and a large reasoning model on another.
|
|
43
|
+
base_url: str = ""
|
|
44
|
+
api_key: str = ""
|
|
45
|
+
# Extra request-body fields forwarded verbatim on every call to this
|
|
46
|
+
# tier (e.g. vLLM's per-request ``priority`` for --scheduling-policy
|
|
47
|
+
# priority, or provider-specific sampling knobs).
|
|
48
|
+
extra_body: dict[str, Any] | None = None
|
|
49
|
+
# The model's *useful* context window in tokens — the point up to
|
|
50
|
+
# which exact retrieval stays reliable, which is often well below the
|
|
51
|
+
# advertised maximum. 0 = unknown/unlimited. Consumed by the context
|
|
52
|
+
# gate to decide when to chunk/retrieve instead of passing whole.
|
|
53
|
+
useful_context_tokens: int = 0
|
|
54
|
+
# Explicit host capability declaration, not inferred from a model name.
|
|
55
|
+
supports_vision: bool = False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
# Provider presets — used by build_tiers_from_host()
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
_PROVIDER_PRESETS: dict[str, dict[ModelTier, TierConfig]] = {
|
|
63
|
+
"anthropic": {
|
|
64
|
+
ModelTier.SMALL: TierConfig(
|
|
65
|
+
tier=ModelTier.SMALL,
|
|
66
|
+
model_id="anthropic/claude-haiku-4-5-20251001",
|
|
67
|
+
max_tokens=4096,
|
|
68
|
+
cost_per_1k_input=0.0008,
|
|
69
|
+
cost_per_1k_output=0.004,
|
|
70
|
+
latency_p50_ms=800,
|
|
71
|
+
),
|
|
72
|
+
ModelTier.MEDIUM: TierConfig(
|
|
73
|
+
tier=ModelTier.MEDIUM,
|
|
74
|
+
model_id="anthropic/claude-sonnet-4-6",
|
|
75
|
+
max_tokens=8192,
|
|
76
|
+
cost_per_1k_input=0.003,
|
|
77
|
+
cost_per_1k_output=0.015,
|
|
78
|
+
latency_p50_ms=2000,
|
|
79
|
+
),
|
|
80
|
+
ModelTier.LARGE: TierConfig(
|
|
81
|
+
tier=ModelTier.LARGE,
|
|
82
|
+
model_id="anthropic/claude-opus-4-6",
|
|
83
|
+
max_tokens=32768,
|
|
84
|
+
cost_per_1k_input=0.015,
|
|
85
|
+
cost_per_1k_output=0.075,
|
|
86
|
+
latency_p50_ms=5000,
|
|
87
|
+
),
|
|
88
|
+
},
|
|
89
|
+
"openai": {
|
|
90
|
+
ModelTier.SMALL: TierConfig(
|
|
91
|
+
tier=ModelTier.SMALL,
|
|
92
|
+
model_id="openai/gpt-4o-mini",
|
|
93
|
+
max_tokens=4096,
|
|
94
|
+
cost_per_1k_input=0.00015,
|
|
95
|
+
cost_per_1k_output=0.0006,
|
|
96
|
+
latency_p50_ms=600,
|
|
97
|
+
),
|
|
98
|
+
ModelTier.MEDIUM: TierConfig(
|
|
99
|
+
tier=ModelTier.MEDIUM,
|
|
100
|
+
model_id="openai/gpt-4o",
|
|
101
|
+
max_tokens=8192,
|
|
102
|
+
cost_per_1k_input=0.0025,
|
|
103
|
+
cost_per_1k_output=0.01,
|
|
104
|
+
latency_p50_ms=2000,
|
|
105
|
+
),
|
|
106
|
+
ModelTier.LARGE: TierConfig(
|
|
107
|
+
tier=ModelTier.LARGE,
|
|
108
|
+
model_id="openai/o3",
|
|
109
|
+
max_tokens=32768,
|
|
110
|
+
cost_per_1k_input=0.015,
|
|
111
|
+
cost_per_1k_output=0.06,
|
|
112
|
+
latency_p50_ms=5000,
|
|
113
|
+
),
|
|
114
|
+
},
|
|
115
|
+
"zai": {
|
|
116
|
+
ModelTier.SMALL: TierConfig(
|
|
117
|
+
tier=ModelTier.SMALL,
|
|
118
|
+
model_id="openai/glm-4.7-flash",
|
|
119
|
+
max_tokens=8192,
|
|
120
|
+
cost_per_1k_input=0.06,
|
|
121
|
+
cost_per_1k_output=0.4,
|
|
122
|
+
latency_p50_ms=800,
|
|
123
|
+
),
|
|
124
|
+
ModelTier.MEDIUM: TierConfig(
|
|
125
|
+
tier=ModelTier.MEDIUM,
|
|
126
|
+
model_id="openai/glm-5.1",
|
|
127
|
+
max_tokens=131072,
|
|
128
|
+
cost_per_1k_input=1.5,
|
|
129
|
+
cost_per_1k_output=5,
|
|
130
|
+
latency_p50_ms=2000,
|
|
131
|
+
),
|
|
132
|
+
ModelTier.LARGE: TierConfig(
|
|
133
|
+
tier=ModelTier.LARGE,
|
|
134
|
+
model_id="openai/glm-5.1",
|
|
135
|
+
max_tokens=131072,
|
|
136
|
+
cost_per_1k_input=1.5,
|
|
137
|
+
cost_per_1k_output=5,
|
|
138
|
+
latency_p50_ms=2000,
|
|
139
|
+
),
|
|
140
|
+
},
|
|
141
|
+
"local": {
|
|
142
|
+
ModelTier.SMALL: TierConfig(
|
|
143
|
+
tier=ModelTier.SMALL,
|
|
144
|
+
model_id="openai/local-model",
|
|
145
|
+
max_tokens=4096,
|
|
146
|
+
cost_per_1k_input=0,
|
|
147
|
+
cost_per_1k_output=0,
|
|
148
|
+
latency_p50_ms=1000,
|
|
149
|
+
),
|
|
150
|
+
ModelTier.MEDIUM: TierConfig(
|
|
151
|
+
tier=ModelTier.MEDIUM,
|
|
152
|
+
model_id="openai/local-model",
|
|
153
|
+
max_tokens=8192,
|
|
154
|
+
cost_per_1k_input=0,
|
|
155
|
+
cost_per_1k_output=0,
|
|
156
|
+
latency_p50_ms=2000,
|
|
157
|
+
),
|
|
158
|
+
ModelTier.LARGE: TierConfig(
|
|
159
|
+
tier=ModelTier.LARGE,
|
|
160
|
+
model_id="openai/local-model",
|
|
161
|
+
max_tokens=32768,
|
|
162
|
+
cost_per_1k_input=0,
|
|
163
|
+
cost_per_1k_output=0,
|
|
164
|
+
latency_p50_ms=3000,
|
|
165
|
+
),
|
|
166
|
+
},
|
|
167
|
+
"ollama": {
|
|
168
|
+
ModelTier.SMALL: TierConfig(
|
|
169
|
+
tier=ModelTier.SMALL,
|
|
170
|
+
model_id="ollama/llama3.2",
|
|
171
|
+
max_tokens=4096,
|
|
172
|
+
cost_per_1k_input=0,
|
|
173
|
+
cost_per_1k_output=0,
|
|
174
|
+
latency_p50_ms=800,
|
|
175
|
+
),
|
|
176
|
+
ModelTier.MEDIUM: TierConfig(
|
|
177
|
+
tier=ModelTier.MEDIUM,
|
|
178
|
+
model_id="ollama/mistral",
|
|
179
|
+
max_tokens=8192,
|
|
180
|
+
cost_per_1k_input=0,
|
|
181
|
+
cost_per_1k_output=0,
|
|
182
|
+
latency_p50_ms=1500,
|
|
183
|
+
),
|
|
184
|
+
ModelTier.LARGE: TierConfig(
|
|
185
|
+
tier=ModelTier.LARGE,
|
|
186
|
+
model_id="ollama/deepseek-r1",
|
|
187
|
+
max_tokens=32768,
|
|
188
|
+
cost_per_1k_input=0,
|
|
189
|
+
cost_per_1k_output=0,
|
|
190
|
+
latency_p50_ms=5000,
|
|
191
|
+
),
|
|
192
|
+
},
|
|
193
|
+
"lmstudio": {
|
|
194
|
+
ModelTier.SMALL: TierConfig(
|
|
195
|
+
tier=ModelTier.SMALL,
|
|
196
|
+
model_id="openai/lmstudio-small",
|
|
197
|
+
max_tokens=4096,
|
|
198
|
+
cost_per_1k_input=0,
|
|
199
|
+
cost_per_1k_output=0,
|
|
200
|
+
latency_p50_ms=800,
|
|
201
|
+
),
|
|
202
|
+
ModelTier.MEDIUM: TierConfig(
|
|
203
|
+
tier=ModelTier.MEDIUM,
|
|
204
|
+
model_id="openai/lmstudio-medium",
|
|
205
|
+
max_tokens=8192,
|
|
206
|
+
cost_per_1k_input=0,
|
|
207
|
+
cost_per_1k_output=0,
|
|
208
|
+
latency_p50_ms=1500,
|
|
209
|
+
),
|
|
210
|
+
ModelTier.LARGE: TierConfig(
|
|
211
|
+
tier=ModelTier.LARGE,
|
|
212
|
+
model_id="openai/lmstudio-large",
|
|
213
|
+
max_tokens=32768,
|
|
214
|
+
cost_per_1k_input=0,
|
|
215
|
+
cost_per_1k_output=0,
|
|
216
|
+
latency_p50_ms=3000,
|
|
217
|
+
),
|
|
218
|
+
},
|
|
219
|
+
"vllm": {
|
|
220
|
+
ModelTier.SMALL: TierConfig(
|
|
221
|
+
tier=ModelTier.SMALL,
|
|
222
|
+
model_id="openai/vllm-small",
|
|
223
|
+
max_tokens=4096,
|
|
224
|
+
cost_per_1k_input=0,
|
|
225
|
+
cost_per_1k_output=0,
|
|
226
|
+
latency_p50_ms=600,
|
|
227
|
+
),
|
|
228
|
+
ModelTier.MEDIUM: TierConfig(
|
|
229
|
+
tier=ModelTier.MEDIUM,
|
|
230
|
+
model_id="openai/vllm-medium",
|
|
231
|
+
max_tokens=8192,
|
|
232
|
+
cost_per_1k_input=0,
|
|
233
|
+
cost_per_1k_output=0,
|
|
234
|
+
latency_p50_ms=1200,
|
|
235
|
+
),
|
|
236
|
+
ModelTier.LARGE: TierConfig(
|
|
237
|
+
tier=ModelTier.LARGE,
|
|
238
|
+
model_id="openai/vllm-large",
|
|
239
|
+
max_tokens=32768,
|
|
240
|
+
cost_per_1k_input=0,
|
|
241
|
+
cost_per_1k_output=0,
|
|
242
|
+
latency_p50_ms=2500,
|
|
243
|
+
),
|
|
244
|
+
},
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# Default tier configurations (Anthropic) — used only when no host
|
|
249
|
+
# has configured the router yet.
|
|
250
|
+
DEFAULT_TIERS: dict[ModelTier, TierConfig] = _PROVIDER_PRESETS["anthropic"]
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# ---------------------------------------------------------------------------
|
|
254
|
+
# Local model discovery
|
|
255
|
+
# ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
def discover_ollama_models(base_url: str = "") -> list[dict[str, Any]]:
|
|
258
|
+
"""Query an Ollama server for installed models.
|
|
259
|
+
|
|
260
|
+
Returns a list of dicts with ``name``, ``size``, and ``digest``.
|
|
261
|
+
The *name* field can be used directly as a LiteLLM model ID
|
|
262
|
+
(``ollama/<name>``).
|
|
263
|
+
"""
|
|
264
|
+
import urllib.request
|
|
265
|
+
|
|
266
|
+
url = base_url.rstrip("/") + "/api/tags" if base_url else "http://127.0.0.1:11434/api/tags"
|
|
267
|
+
try:
|
|
268
|
+
with urllib.request.urlopen(url, timeout=5) as resp:
|
|
269
|
+
data = json.loads(resp.read().decode())
|
|
270
|
+
models = []
|
|
271
|
+
for m in data.get("models", []):
|
|
272
|
+
models.append({
|
|
273
|
+
"name": m.get("name", ""),
|
|
274
|
+
"size": m.get("size", 0),
|
|
275
|
+
"digest": m.get("digest", "")[:12],
|
|
276
|
+
})
|
|
277
|
+
return models
|
|
278
|
+
except Exception as exc:
|
|
279
|
+
logger.debug("Ollama model discovery failed (%s): %s", url, exc)
|
|
280
|
+
return []
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def discover_openai_compatible_models(base_url: str, api_key: str = "") -> list[dict[str, Any]]:
|
|
284
|
+
"""Query an OpenAI-compatible server (vLLM, LM Studio, etc.) for models.
|
|
285
|
+
|
|
286
|
+
Returns a list of dicts with ``id`` and ``owned_by``.
|
|
287
|
+
The *id* can be used as a LiteLLM model ID (``openai/<id>`` when
|
|
288
|
+
routed through the OpenAI compat layer).
|
|
289
|
+
"""
|
|
290
|
+
import urllib.request
|
|
291
|
+
|
|
292
|
+
from .endpoints import models_url
|
|
293
|
+
url = models_url(base_url)
|
|
294
|
+
req = urllib.request.Request(url, method="GET")
|
|
295
|
+
if api_key:
|
|
296
|
+
req.add_header("Authorization", f"Bearer {api_key}")
|
|
297
|
+
try:
|
|
298
|
+
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
299
|
+
data = json.loads(resp.read().decode())
|
|
300
|
+
models = []
|
|
301
|
+
for m in data.get("data", []):
|
|
302
|
+
models.append({
|
|
303
|
+
"id": m.get("id", ""),
|
|
304
|
+
"owned_by": m.get("owned_by", ""),
|
|
305
|
+
})
|
|
306
|
+
return models
|
|
307
|
+
except Exception as exc:
|
|
308
|
+
logger.debug("OpenAI-compat model discovery failed (%s): %s", url, exc)
|
|
309
|
+
return []
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def discover_local_models(provider: str, base_url: str = "", api_key: str = "") -> list[dict[str, Any]]:
|
|
313
|
+
"""Discover models for a local provider.
|
|
314
|
+
|
|
315
|
+
Attempts Ollama discovery first (for ``provider == "ollama"`` or when
|
|
316
|
+
the base URL looks like an Ollama endpoint), then falls back to the
|
|
317
|
+
OpenAI-compatible ``/v1/models`` endpoint.
|
|
318
|
+
"""
|
|
319
|
+
if provider == "ollama":
|
|
320
|
+
return discover_ollama_models(base_url)
|
|
321
|
+
|
|
322
|
+
if provider in ("local", "custom", "openai", "lmstudio", "vllm") and base_url:
|
|
323
|
+
# Try OpenAI-compatible endpoint first
|
|
324
|
+
models = discover_openai_compatible_models(base_url, api_key)
|
|
325
|
+
if models:
|
|
326
|
+
return models
|
|
327
|
+
# Some local servers (e.g. Ollama with OpenAI compat) may also
|
|
328
|
+
# expose the Ollama API on the same port
|
|
329
|
+
ollama_models = discover_ollama_models(base_url)
|
|
330
|
+
if ollama_models:
|
|
331
|
+
return [{"id": m["name"], "source": "ollama"} for m in ollama_models]
|
|
332
|
+
|
|
333
|
+
return []
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
# ---------------------------------------------------------------------------
|
|
337
|
+
# LiteLLM prefix helpers
|
|
338
|
+
# ---------------------------------------------------------------------------
|
|
339
|
+
|
|
340
|
+
_LITELLM_PREFIXES: frozenset[str] = frozenset(
|
|
341
|
+
{
|
|
342
|
+
"openai",
|
|
343
|
+
"anthropic",
|
|
344
|
+
"ollama",
|
|
345
|
+
"azure",
|
|
346
|
+
"azure_ai",
|
|
347
|
+
"azure_text",
|
|
348
|
+
"azure_chat",
|
|
349
|
+
"bedrock",
|
|
350
|
+
"vertex_ai",
|
|
351
|
+
"gemini",
|
|
352
|
+
"groq",
|
|
353
|
+
"together_ai",
|
|
354
|
+
"huggingface",
|
|
355
|
+
"fireworks_ai",
|
|
356
|
+
"mistral",
|
|
357
|
+
"deepseek",
|
|
358
|
+
"xai",
|
|
359
|
+
"text-completion-openai",
|
|
360
|
+
"cohere",
|
|
361
|
+
"perplexity",
|
|
362
|
+
"ai21",
|
|
363
|
+
"replicate",
|
|
364
|
+
"baseten",
|
|
365
|
+
"vllm",
|
|
366
|
+
"sagemaker",
|
|
367
|
+
"cloudflare",
|
|
368
|
+
"watsonx",
|
|
369
|
+
"databricks",
|
|
370
|
+
"zhipu",
|
|
371
|
+
"clarifai",
|
|
372
|
+
"jina_ai",
|
|
373
|
+
"novita",
|
|
374
|
+
"siliconflow",
|
|
375
|
+
"openrouter",
|
|
376
|
+
"maritalk",
|
|
377
|
+
}
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _has_litellm_prefix(model_id: str) -> bool:
|
|
382
|
+
"""Return True if *model_id* starts with a known LiteLLM provider prefix."""
|
|
383
|
+
if "/" not in model_id:
|
|
384
|
+
return False
|
|
385
|
+
prefix = model_id.split("/", 1)[0].lower()
|
|
386
|
+
return prefix in _LITELLM_PREFIXES
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
# ---------------------------------------------------------------------------
|
|
390
|
+
# Tier building from host config
|
|
391
|
+
# ---------------------------------------------------------------------------
|
|
392
|
+
|
|
393
|
+
def build_tiers_from_host(config: dict, *, configure_environment=True, discover=True) -> dict[ModelTier, TierConfig]:
|
|
394
|
+
"""Build tier configurations from host-provided LLM config.
|
|
395
|
+
|
|
396
|
+
The host (OpenClaw, Hermes, etc.) sends its LLM provider details
|
|
397
|
+
via ``POST /v1/host/configure``. This function maps that config
|
|
398
|
+
to TierConfig objects that the LLMRouter can use.
|
|
399
|
+
|
|
400
|
+
Supports arbitrary local and remote models. Hosts may send either
|
|
401
|
+
fully-qualified LiteLLM model IDs (``ollama/llama3.2``,
|
|
402
|
+
``openai/gpt-4o``) or bare model names for known providers.
|
|
403
|
+
|
|
404
|
+
When no explicit model overrides are provided for a local provider,
|
|
405
|
+
the function attempts to auto-discover installed models and maps
|
|
406
|
+
them to tiers heuristically (smallest → SMALL, largest → LARGE).
|
|
407
|
+
|
|
408
|
+
Expected config shape::
|
|
409
|
+
|
|
410
|
+
{
|
|
411
|
+
"provider": "ollama",
|
|
412
|
+
"apiKey": "",
|
|
413
|
+
"baseUrl": "http://localhost:11434",
|
|
414
|
+
"models": {
|
|
415
|
+
"small": "llama3.2",
|
|
416
|
+
"medium": "mistral",
|
|
417
|
+
"large": "deepseek-r1"
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
Each ``models`` value may also be an object instead of a bare model
|
|
422
|
+
string, enabling *per-tier endpoints* — different tiers served by
|
|
423
|
+
different servers — plus per-tier request extras::
|
|
424
|
+
|
|
425
|
+
{
|
|
426
|
+
"provider": "vllm",
|
|
427
|
+
"baseUrl": "http://fast-host:8000/v1", # default for all tiers
|
|
428
|
+
"models": {
|
|
429
|
+
"small": "fast-model",
|
|
430
|
+
"large": {
|
|
431
|
+
"model": "big-model",
|
|
432
|
+
"baseUrl": "http://big-host:8000/v1",
|
|
433
|
+
"apiKey": "…", # optional
|
|
434
|
+
"extraBody": {"priority": 10}, # optional, sent verbatim
|
|
435
|
+
"usefulContextTokens": 65536, # optional, context gate hint
|
|
436
|
+
"maxTokens": 32768 # optional, completion cap
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
Snake_case keys (``base_url``, ``api_key``, ``extra_body``,
|
|
442
|
+
``useful_context_tokens``, ``max_tokens``) are accepted as aliases.
|
|
443
|
+
|
|
444
|
+
For OpenAI-compatible providers (zai, local, custom, lmstudio, vllm),
|
|
445
|
+
the function sets ``OPENAI_API_KEY`` and ``OPENAI_API_BASE`` so
|
|
446
|
+
LiteLLM routes ``openai/*`` model IDs correctly. For Ollama, it
|
|
447
|
+
sets ``OLLAMA_API_BASE``.
|
|
448
|
+
"""
|
|
449
|
+
provider = config.get("provider", "anthropic").lower()
|
|
450
|
+
api_key = config.get("apiKey", "")
|
|
451
|
+
base_url = config.get("baseUrl", "")
|
|
452
|
+
models_override = config.get("models", {})
|
|
453
|
+
|
|
454
|
+
def _spec_field(spec: dict, *names: str, default: Any = None) -> Any:
|
|
455
|
+
"""First present key among camelCase/snake_case aliases."""
|
|
456
|
+
for name in names:
|
|
457
|
+
if name in spec:
|
|
458
|
+
return spec[name]
|
|
459
|
+
return default
|
|
460
|
+
|
|
461
|
+
def _parse_model_spec(value: Any) -> tuple[str, dict[str, Any]]:
|
|
462
|
+
"""Normalize a ``models`` entry (string or object) to (model_id, overrides).
|
|
463
|
+
|
|
464
|
+
Overrides may contain: base_url, api_key, extra_body,
|
|
465
|
+
useful_context_tokens, max_tokens.
|
|
466
|
+
"""
|
|
467
|
+
if isinstance(value, str):
|
|
468
|
+
return value, {}
|
|
469
|
+
if isinstance(value, dict):
|
|
470
|
+
model_id = str(_spec_field(value, "model", "model_id", default="") or "")
|
|
471
|
+
overrides: dict[str, Any] = {}
|
|
472
|
+
v = _spec_field(value, "baseUrl", "base_url")
|
|
473
|
+
if v:
|
|
474
|
+
overrides["base_url"] = str(v)
|
|
475
|
+
v = _spec_field(value, "apiKey", "api_key")
|
|
476
|
+
if v:
|
|
477
|
+
overrides["api_key"] = str(v)
|
|
478
|
+
v = _spec_field(value, "supportsVision", "supports_vision")
|
|
479
|
+
if isinstance(v, bool):
|
|
480
|
+
overrides["supports_vision"] = v
|
|
481
|
+
v = _spec_field(value, "extraBody", "extra_body")
|
|
482
|
+
if isinstance(v, dict) and v:
|
|
483
|
+
overrides["extra_body"] = dict(v)
|
|
484
|
+
v = _spec_field(value, "usefulContextTokens", "useful_context_tokens")
|
|
485
|
+
if v:
|
|
486
|
+
try:
|
|
487
|
+
overrides["useful_context_tokens"] = int(v)
|
|
488
|
+
except (TypeError, ValueError):
|
|
489
|
+
logger.warning("Invalid usefulContextTokens %r — ignoring", v)
|
|
490
|
+
v = _spec_field(value, "maxTokens", "max_tokens")
|
|
491
|
+
if v:
|
|
492
|
+
try:
|
|
493
|
+
overrides["max_tokens"] = int(v)
|
|
494
|
+
except (TypeError, ValueError):
|
|
495
|
+
logger.warning("Invalid maxTokens %r — ignoring", v)
|
|
496
|
+
return model_id, overrides
|
|
497
|
+
logger.warning("Unsupported model spec %r — expected string or object", value)
|
|
498
|
+
return "", {}
|
|
499
|
+
|
|
500
|
+
# Determine whether the provider is OpenAI-compatible or Ollama-native
|
|
501
|
+
openai_compat_providers = {"zai", "local", "custom", "lmstudio", "vllm", "openai"}
|
|
502
|
+
ollama_providers = {"ollama"}
|
|
503
|
+
|
|
504
|
+
# ------------------------------------------------------------------
|
|
505
|
+
# Build tier skeleton
|
|
506
|
+
# ------------------------------------------------------------------
|
|
507
|
+
tiers = _PROVIDER_PRESETS.get(provider)
|
|
508
|
+
if tiers is None:
|
|
509
|
+
if models_override:
|
|
510
|
+
# Unknown provider with explicit models — build generic zero-cost tiers
|
|
511
|
+
# rather than silently falling back to anthropic.
|
|
512
|
+
logger.info(
|
|
513
|
+
"Unknown provider %r — building tiers from host model overrides.",
|
|
514
|
+
provider,
|
|
515
|
+
)
|
|
516
|
+
tiers = {}
|
|
517
|
+
for tier in (ModelTier.SMALL, ModelTier.MEDIUM, ModelTier.LARGE):
|
|
518
|
+
raw_spec = models_override.get(tier.value)
|
|
519
|
+
model_id = _parse_model_spec(raw_spec)[0] if raw_spec is not None else None
|
|
520
|
+
if not model_id:
|
|
521
|
+
# Fallback for missing tier — use a generic placeholder
|
|
522
|
+
# that will be overridden below if the host provides it.
|
|
523
|
+
model_id = "openai/gpt-4o-mini"
|
|
524
|
+
tiers[tier] = TierConfig(
|
|
525
|
+
tier=tier,
|
|
526
|
+
model_id=model_id,
|
|
527
|
+
max_tokens=8192,
|
|
528
|
+
cost_per_1k_input=0,
|
|
529
|
+
cost_per_1k_output=0,
|
|
530
|
+
latency_p50_ms=1500,
|
|
531
|
+
)
|
|
532
|
+
else:
|
|
533
|
+
logger.warning(
|
|
534
|
+
"Unknown LLM provider %r and no model overrides — falling back to anthropic preset. "
|
|
535
|
+
"Known providers: %s",
|
|
536
|
+
provider,
|
|
537
|
+
", ".join(sorted(_PROVIDER_PRESETS)),
|
|
538
|
+
)
|
|
539
|
+
tiers = dict(_PROVIDER_PRESETS["anthropic"])
|
|
540
|
+
|
|
541
|
+
# Deep-copy so we don't mutate the preset
|
|
542
|
+
tiers = {tier: replace(cfg) for tier, cfg in tiers.items()}
|
|
543
|
+
|
|
544
|
+
# ------------------------------------------------------------------
|
|
545
|
+
# Apply model overrides from host
|
|
546
|
+
# ------------------------------------------------------------------
|
|
547
|
+
if models_override:
|
|
548
|
+
for tier_name, raw_spec in models_override.items():
|
|
549
|
+
try:
|
|
550
|
+
tier = ModelTier(tier_name)
|
|
551
|
+
except ValueError:
|
|
552
|
+
logger.warning("Unknown tier %r in model override — skipping", tier_name)
|
|
553
|
+
continue
|
|
554
|
+
if tier == ModelTier.HEURISTIC:
|
|
555
|
+
# HEURISTIC is a rule-based non-LLM tier — ignore model overrides for it
|
|
556
|
+
continue
|
|
557
|
+
|
|
558
|
+
model_id, tier_overrides = _parse_model_spec(raw_spec)
|
|
559
|
+
if not model_id:
|
|
560
|
+
logger.warning("No model in spec for tier %r — skipping", tier_name)
|
|
561
|
+
continue
|
|
562
|
+
|
|
563
|
+
if tier not in tiers:
|
|
564
|
+
# Host sent a model for a tier we don't have yet (generic build case)
|
|
565
|
+
tiers[tier] = TierConfig(
|
|
566
|
+
tier=tier,
|
|
567
|
+
model_id=model_id,
|
|
568
|
+
max_tokens=8192,
|
|
569
|
+
cost_per_1k_input=0,
|
|
570
|
+
cost_per_1k_output=0,
|
|
571
|
+
latency_p50_ms=1500,
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
# If the model ID already contains a known LiteLLM provider prefix
|
|
575
|
+
# (e.g. "ollama/llama3.2", "huggingface/mistral-7b"), preserve it exactly.
|
|
576
|
+
# Otherwise apply provider-specific prefixing so bare names work.
|
|
577
|
+
if not _has_litellm_prefix(model_id):
|
|
578
|
+
if provider in ollama_providers:
|
|
579
|
+
model_id = f"ollama/{model_id}"
|
|
580
|
+
elif provider in openai_compat_providers:
|
|
581
|
+
model_id = f"openai/{model_id}"
|
|
582
|
+
# For anthropic and other providers, leave bare names as-is
|
|
583
|
+
# (LiteLLM accepts "claude-sonnet-4-6" without a prefix when
|
|
584
|
+
# ANTHROPIC_API_KEY is set).
|
|
585
|
+
|
|
586
|
+
tiers[tier] = replace(tiers[tier], model_id=model_id, **tier_overrides)
|
|
587
|
+
logger.info(
|
|
588
|
+
"Host override: %s tier -> %s%s",
|
|
589
|
+
tier.value,
|
|
590
|
+
model_id,
|
|
591
|
+
f" @ {tier_overrides['base_url']}" if tier_overrides.get("base_url") else "",
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
# ------------------------------------------------------------------
|
|
595
|
+
# Auto-discovery when no overrides are provided for local providers
|
|
596
|
+
# ------------------------------------------------------------------
|
|
597
|
+
elif discover and provider in ("ollama", "local", "custom", "lmstudio", "vllm"):
|
|
598
|
+
discovered = discover_local_models(provider, base_url, api_key)
|
|
599
|
+
if discovered:
|
|
600
|
+
logger.info(
|
|
601
|
+
"Discovered %d local model(s) for provider=%s; applying to tiers",
|
|
602
|
+
len(discovered),
|
|
603
|
+
provider,
|
|
604
|
+
)
|
|
605
|
+
# Map discovered models to tiers heuristically:
|
|
606
|
+
# smallest -> SMALL, largest -> LARGE, middle -> MEDIUM
|
|
607
|
+
sorted_models = sorted(discovered, key=lambda m: m.get("size", 0))
|
|
608
|
+
if len(sorted_models) >= 1 and ModelTier.SMALL in tiers:
|
|
609
|
+
small_id = sorted_models[0].get("name") or sorted_models[0].get("id", "")
|
|
610
|
+
if small_id:
|
|
611
|
+
tiers[ModelTier.SMALL] = replace(
|
|
612
|
+
tiers[ModelTier.SMALL],
|
|
613
|
+
model_id=f"{provider}/{small_id}" if provider == "ollama" else f"openai/{small_id}",
|
|
614
|
+
)
|
|
615
|
+
if len(sorted_models) >= 2 and ModelTier.LARGE in tiers:
|
|
616
|
+
large_id = sorted_models[-1].get("name") or sorted_models[-1].get("id", "")
|
|
617
|
+
if large_id:
|
|
618
|
+
tiers[ModelTier.LARGE] = replace(
|
|
619
|
+
tiers[ModelTier.LARGE],
|
|
620
|
+
model_id=f"{provider}/{large_id}" if provider == "ollama" else f"openai/{large_id}",
|
|
621
|
+
)
|
|
622
|
+
if len(sorted_models) >= 3 and ModelTier.MEDIUM in tiers:
|
|
623
|
+
mid_idx = len(sorted_models) // 2
|
|
624
|
+
mid_id = sorted_models[mid_idx].get("name") or sorted_models[mid_idx].get("id", "")
|
|
625
|
+
if mid_id:
|
|
626
|
+
tiers[ModelTier.MEDIUM] = replace(
|
|
627
|
+
tiers[ModelTier.MEDIUM],
|
|
628
|
+
model_id=f"{provider}/{mid_id}" if provider == "ollama" else f"openai/{mid_id}",
|
|
629
|
+
)
|
|
630
|
+
elif len(sorted_models) == 2 and ModelTier.MEDIUM in tiers:
|
|
631
|
+
# Only two models — use the larger one for medium too
|
|
632
|
+
mid_id = sorted_models[-1].get("name") or sorted_models[-1].get("id", "")
|
|
633
|
+
if mid_id:
|
|
634
|
+
tiers[ModelTier.MEDIUM] = replace(
|
|
635
|
+
tiers[ModelTier.MEDIUM],
|
|
636
|
+
model_id=f"{provider}/{mid_id}" if provider == "ollama" else f"openai/{mid_id}",
|
|
637
|
+
)
|
|
638
|
+
else:
|
|
639
|
+
logger.warning(
|
|
640
|
+
"No model overrides provided and discovery failed for provider=%s. "
|
|
641
|
+
"The router will use placeholder model IDs which likely do not exist "
|
|
642
|
+
"on your local server. Pass explicit models in the host config, e.g.:\n"
|
|
643
|
+
' {"models": {"small": "llama3.2", "medium": "mistral", "large": "deepseek-r1"}}',
|
|
644
|
+
provider,
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
# ------------------------------------------------------------------
|
|
648
|
+
# Set provider-specific environment variables for LiteLLM
|
|
649
|
+
# ------------------------------------------------------------------
|
|
650
|
+
if not configure_environment:
|
|
651
|
+
return tiers
|
|
652
|
+
if provider in openai_compat_providers:
|
|
653
|
+
if api_key:
|
|
654
|
+
os.environ["OPENAI_API_KEY"] = api_key
|
|
655
|
+
if base_url:
|
|
656
|
+
os.environ["OPENAI_API_BASE"] = base_url
|
|
657
|
+
elif provider == "zai":
|
|
658
|
+
os.environ["OPENAI_API_BASE"] = "https://api.z.ai/api/paas/v4"
|
|
659
|
+
logger.info(
|
|
660
|
+
"Configured OpenAI-compat provider: base=%s key_present=%s",
|
|
661
|
+
os.environ.get("OPENAI_API_BASE", "(none)"),
|
|
662
|
+
bool(api_key),
|
|
663
|
+
)
|
|
664
|
+
elif provider in ollama_providers:
|
|
665
|
+
if base_url:
|
|
666
|
+
os.environ["OLLAMA_API_BASE"] = base_url
|
|
667
|
+
else:
|
|
668
|
+
os.environ.setdefault("OLLAMA_API_BASE", "http://127.0.0.1:11434")
|
|
669
|
+
logger.info(
|
|
670
|
+
"Configured Ollama provider: base=%s",
|
|
671
|
+
os.environ.get("OLLAMA_API_BASE", "(default localhost:11434)"),
|
|
672
|
+
)
|
|
673
|
+
elif provider == "anthropic":
|
|
674
|
+
if api_key:
|
|
675
|
+
os.environ["ANTHROPIC_API_KEY"] = api_key
|
|
676
|
+
|
|
677
|
+
return tiers
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Exploration sandbox: gated, isolated code execution (cognition item 6).
|
|
2
|
+
|
|
3
|
+
Safe curiosity -- run a script inside a locked-down backend (no egress, no
|
|
4
|
+
credentials, capped resources, read-only rootfs + one writable workdir) behind
|
|
5
|
+
a mode flag, a DirectiveGuard boundary check, and approval tiering. Server-side
|
|
6
|
+
enforcement: the caller can never widen containment. Default off.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from apsimo.sandbox.backend import (
|
|
10
|
+
DisabledSandbox, DockerSandbox, SandboxBackend, SandboxLimits,
|
|
11
|
+
SandboxResult, select_backend,
|
|
12
|
+
)
|
|
13
|
+
from apsimo.sandbox.manager import (
|
|
14
|
+
SandboxManager, resolve_limits, sandbox_mode,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"SandboxBackend", "SandboxLimits", "SandboxResult",
|
|
19
|
+
"DockerSandbox", "DisabledSandbox", "select_backend",
|
|
20
|
+
"SandboxManager", "sandbox_mode", "resolve_limits",
|
|
21
|
+
]
|