alysis-code 0.13.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.
- alysis_code/__init__.py +3 -0
- alysis_code/__main__.py +4 -0
- alysis_code/_build_info.py +14 -0
- alysis_code/account_login.py +468 -0
- alysis_code/agent/README.md +35 -0
- alysis_code/agent/__init__.py +11 -0
- alysis_code/agent/acceptance_contract.py +2217 -0
- alysis_code/agent/blast_radius.py +1403 -0
- alysis_code/agent/cache_keepalive.py +227 -0
- alysis_code/agent/completion_certificate.py +366 -0
- alysis_code/agent/completion_gate.py +306 -0
- alysis_code/agent/empty_response_stall.py +403 -0
- alysis_code/agent/errors.py +28 -0
- alysis_code/agent/llm_calls.py +475 -0
- alysis_code/agent/mutation_classification.py +227 -0
- alysis_code/agent/prompt_context.py +2508 -0
- alysis_code/agent/read_ledger.py +253 -0
- alysis_code/agent/regression_baseline.py +642 -0
- alysis_code/agent/reproduction_first.py +610 -0
- alysis_code/agent/sensitive_output.py +629 -0
- alysis_code/agent/session.py +3218 -0
- alysis_code/agent/steering.py +191 -0
- alysis_code/agent/subagent_execution.py +5177 -0
- alysis_code/agent/subagent_workspace.py +666 -0
- alysis_code/agent/tools_assembly.py +4728 -0
- alysis_code/agent/turn/__init__.py +101 -0
- alysis_code/agent/turn/core.py +8483 -0
- alysis_code/agent/turn/events.py +113 -0
- alysis_code/agent/turn/exploration.py +590 -0
- alysis_code/agent/turn/interventions.py +65 -0
- alysis_code/agent/turn/read_cache.py +420 -0
- alysis_code/agent/turn/snapshot.py +179 -0
- alysis_code/agent/turn_contract.py +661 -0
- alysis_code/agent/turn_path.py +129 -0
- alysis_code/agent/verification.py +2885 -0
- alysis_code/agent/verification_commands.py +512 -0
- alysis_code/agent/verification_evidence.py +738 -0
- alysis_code/agent_loop.py +693 -0
- alysis_code/agent_runtimes/__init__.py +51 -0
- alysis_code/agent_runtimes/base.py +114 -0
- alysis_code/agent_runtimes/builtins.py +129 -0
- alysis_code/agent_runtimes/codex_cli.py +664 -0
- alysis_code/agent_runtimes/host.py +263 -0
- alysis_code/agent_runtimes/registry.py +64 -0
- alysis_code/agent_runtimes/service.py +150 -0
- alysis_code/agentbox_client.py +416 -0
- alysis_code/agentbox_integration.py +310 -0
- alysis_code/alysis_cloud.py +152 -0
- alysis_code/approval_scope.py +276 -0
- alysis_code/assets/README.md +33 -0
- alysis_code/assets/__init__.py +126 -0
- alysis_code/assets/asset_read_core.py +281 -0
- alysis_code/assets/budget_allocator.py +456 -0
- alysis_code/assets/comprehender.py +759 -0
- alysis_code/assets/index.py +654 -0
- alysis_code/assets/ingestion.py +275 -0
- alysis_code/assets/legacy_migration.py +413 -0
- alysis_code/assets/models.py +263 -0
- alysis_code/assets/ocr.py +239 -0
- alysis_code/assets/owl/ascii/f-000.txt +13 -0
- alysis_code/assets/owl/ascii/f-001.txt +13 -0
- alysis_code/assets/owl/ascii/f-002.txt +13 -0
- alysis_code/assets/owl/ascii/f-003.txt +13 -0
- alysis_code/assets/owl/ascii/f-004.txt +13 -0
- alysis_code/assets/owl/ascii/f-005.txt +13 -0
- alysis_code/assets/owl/ascii/f-006.txt +13 -0
- alysis_code/assets/owl/ascii/f-007.txt +13 -0
- alysis_code/assets/owl/ascii/f-008.txt +13 -0
- alysis_code/assets/owl/ascii/f-009.txt +13 -0
- alysis_code/assets/owl/ascii/f-010.txt +13 -0
- alysis_code/assets/owl/ascii/f-011.txt +13 -0
- alysis_code/assets/owl/ascii/f-012.txt +13 -0
- alysis_code/assets/owl/ascii/f-013.txt +13 -0
- alysis_code/assets/owl/ascii/f-014.txt +13 -0
- alysis_code/assets/owl/ascii/f-015.txt +13 -0
- alysis_code/assets/owl/ascii/f-016.txt +13 -0
- alysis_code/assets/owl/ascii/f-017.txt +13 -0
- alysis_code/assets/owl/ascii/f-018.txt +13 -0
- alysis_code/assets/owl/ascii/f-019.txt +13 -0
- alysis_code/assets/owl/ascii/f-020.txt +13 -0
- alysis_code/assets/owl/index.html +98 -0
- alysis_code/assets/owl/show-owl.sh +761 -0
- alysis_code/assets/paths.py +49 -0
- alysis_code/assets/plan_binding.py +326 -0
- alysis_code/assets/planner_context.py +466 -0
- alysis_code/assets/planner_tools.py +184 -0
- alysis_code/assets/prompts.py +101 -0
- alysis_code/assets/replanner_context.py +239 -0
- alysis_code/assets/surface.py +521 -0
- alysis_code/assets/untrusted_content.py +48 -0
- alysis_code/assets/usage_logger.py +94 -0
- alysis_code/assets/worker_mirror.py +428 -0
- alysis_code/assets/worker_section.py +303 -0
- alysis_code/assets/worker_tools.py +468 -0
- alysis_code/atomic_io.py +83 -0
- alysis_code/auth_diagnostics.py +272 -0
- alysis_code/background_runner.py +366 -0
- alysis_code/branding.py +270 -0
- alysis_code/budget_policy.py +390 -0
- alysis_code/build_identity.py +465 -0
- alysis_code/builtin_hooks/__init__.py +7 -0
- alysis_code/builtin_hooks/notify_done_windows.py +65 -0
- alysis_code/bwrap_etc.py +76 -0
- alysis_code/cancellation.py +41 -0
- alysis_code/capabilities.py +137 -0
- alysis_code/chatgpt_codex_static_provider.py +133 -0
- alysis_code/cli.py +51 -0
- alysis_code/cli_impl/__init__.py +1 -0
- alysis_code/cli_impl/assets_cli.py +537 -0
- alysis_code/cli_impl/assets_modal.py +412 -0
- alysis_code/cli_impl/chat/__init__.py +156 -0
- alysis_code/cli_impl/chat/commands.py +2616 -0
- alysis_code/cli_impl/chat/loop.py +4508 -0
- alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
- alysis_code/cli_impl/chat/rendering.py +444 -0
- alysis_code/cli_impl/chat/state.py +124 -0
- alysis_code/cli_impl/chat_resume.py +830 -0
- alysis_code/cli_impl/chat_slash_completer.py +258 -0
- alysis_code/cli_impl/commands/__init__.py +11 -0
- alysis_code/cli_impl/commands/_shared.py +89 -0
- alysis_code/cli_impl/commands/auth.py +623 -0
- alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
- alysis_code/cli_impl/commands/chat_state.py +158 -0
- alysis_code/cli_impl/commands/chat_status.py +1248 -0
- alysis_code/cli_impl/commands/chat_terminal.py +942 -0
- alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
- alysis_code/cli_impl/commands/cli_common.py +1223 -0
- alysis_code/cli_impl/commands/cli_surface.py +77 -0
- alysis_code/cli_impl/commands/config.py +131 -0
- alysis_code/cli_impl/commands/conventions.py +85 -0
- alysis_code/cli_impl/commands/execution_helpers.py +350 -0
- alysis_code/cli_impl/commands/extensions.py +401 -0
- alysis_code/cli_impl/commands/forge.py +1282 -0
- alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
- alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
- alysis_code/cli_impl/commands/hooks.py +737 -0
- alysis_code/cli_impl/commands/ide_bridge.py +31 -0
- alysis_code/cli_impl/commands/mcp.py +700 -0
- alysis_code/cli_impl/commands/profile.py +453 -0
- alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
- alysis_code/cli_impl/commands/report.py +88 -0
- alysis_code/cli_impl/commands/root.py +1118 -0
- alysis_code/cli_impl/commands/sandbox.py +184 -0
- alysis_code/cli_impl/commands/server.py +54 -0
- alysis_code/cli_impl/commands/sessions.py +252 -0
- alysis_code/cli_impl/commands/skills.py +404 -0
- alysis_code/cli_impl/commands/startup.py +946 -0
- alysis_code/cli_impl/commands/tools.py +335 -0
- alysis_code/cli_impl/commands/update.py +364 -0
- alysis_code/cli_impl/commands/welcome.py +972 -0
- alysis_code/cli_impl/config_menu.py +3882 -0
- alysis_code/cli_impl/forge.py +4509 -0
- alysis_code/cli_impl/forge_recovery.py +485 -0
- alysis_code/cli_impl/setup_wizard.py +2409 -0
- alysis_code/cli_impl/tui/__init__.py +58 -0
- alysis_code/cli_impl/tui/app.py +4551 -0
- alysis_code/cli_impl/tui/config.py +32 -0
- alysis_code/cli_impl/tui/config_flow.py +2754 -0
- alysis_code/cli_impl/tui/config_overlay.py +566 -0
- alysis_code/cli_impl/tui/content.py +78 -0
- alysis_code/cli_impl/tui/footer.py +218 -0
- alysis_code/cli_impl/tui/forge_status.py +136 -0
- alysis_code/cli_impl/tui/markdown.py +244 -0
- alysis_code/cli_impl/tui/owl.py +109 -0
- alysis_code/cli_impl/tui/plan_meta.py +477 -0
- alysis_code/cli_impl/tui/setup_app.py +519 -0
- alysis_code/cli_impl/tui/setup_flow.py +1622 -0
- alysis_code/cli_impl/tui/state.py +101 -0
- alysis_code/cli_impl/tui/subagent_identity.py +66 -0
- alysis_code/cli_impl/tui/subagent_panel.py +186 -0
- alysis_code/cli_impl/tui/surface.py +796 -0
- alysis_code/cli_impl/tui/transcript.py +514 -0
- alysis_code/cli_impl/tui/update_prompt.py +79 -0
- alysis_code/cli_impl/tui/workspace_guard.py +384 -0
- alysis_code/clipboard.py +172 -0
- alysis_code/code_review.py +1211 -0
- alysis_code/compaction/__init__.py +28 -0
- alysis_code/compaction/conversation_compactor.py +2932 -0
- alysis_code/compaction/importance.py +177 -0
- alysis_code/compaction/settings.py +297 -0
- alysis_code/compaction/tool_output_offload.py +447 -0
- alysis_code/config.py +3509 -0
- alysis_code/conflict_auto_resolver.py +895 -0
- alysis_code/context/__init__.py +1 -0
- alysis_code/context/tool_schema_budgeter.py +220 -0
- alysis_code/crash_diagnostics.py +282 -0
- alysis_code/custom_tools/README.md +34 -0
- alysis_code/custom_tools/__init__.py +43 -0
- alysis_code/custom_tools/discovery.py +903 -0
- alysis_code/custom_tools/runtime.py +1516 -0
- alysis_code/custom_tools/session.py +227 -0
- alysis_code/custom_tools/trust.py +232 -0
- alysis_code/diff_paths.py +113 -0
- alysis_code/direction_change.py +293 -0
- alysis_code/dispatch_timing.py +306 -0
- alysis_code/durable_service_manager.py +1236 -0
- alysis_code/edit_discipline.py +659 -0
- alysis_code/error_text.py +73 -0
- alysis_code/execution_budget.py +411 -0
- alysis_code/execution_context.py +915 -0
- alysis_code/execution_deadline.py +1065 -0
- alysis_code/execution_shared.py +1904 -0
- alysis_code/extensions/README.md +30 -0
- alysis_code/extensions/__init__.py +93 -0
- alysis_code/extensions/activation.py +138 -0
- alysis_code/extensions/install.py +1436 -0
- alysis_code/extensions/manifest.py +487 -0
- alysis_code/extensions/models.py +74 -0
- alysis_code/extensions/paths.py +56 -0
- alysis_code/extensions/registry.json +4 -0
- alysis_code/extensions/registry.py +52 -0
- alysis_code/extensions/state.py +83 -0
- alysis_code/extensions/workspace_trust.py +101 -0
- alysis_code/failed_task_evidence.py +369 -0
- alysis_code/failure_category.py +315 -0
- alysis_code/feedback_report.py +1647 -0
- alysis_code/file_classification.py +485 -0
- alysis_code/forge.py +2064 -0
- alysis_code/forge_completion.py +362 -0
- alysis_code/forge_events.py +475 -0
- alysis_code/frontmatter_utils.py +95 -0
- alysis_code/git_evidence.py +1181 -0
- alysis_code/git_ops.py +560 -0
- alysis_code/git_safe.py +62 -0
- alysis_code/git_worktrees.py +190 -0
- alysis_code/hooks/README.md +33 -0
- alysis_code/hooks/__init__.py +67 -0
- alysis_code/hooks/audit.py +171 -0
- alysis_code/hooks/config.py +225 -0
- alysis_code/hooks/dispatcher.py +1110 -0
- alysis_code/hooks/models.py +447 -0
- alysis_code/hooks/trust.py +202 -0
- alysis_code/host_actions.py +543 -0
- alysis_code/host_browser.py +103 -0
- alysis_code/ide/__init__.py +5 -0
- alysis_code/ide/activity_events.py +399 -0
- alysis_code/ide/approvals.py +337 -0
- alysis_code/ide/artifacts.py +153 -0
- alysis_code/ide/browser_egress_proxy.py +1076 -0
- alysis_code/ide/cdp_websocket_transport.py +1192 -0
- alysis_code/ide/change_ledger.py +1721 -0
- alysis_code/ide/context_blocks.py +979 -0
- alysis_code/ide/event_stream.py +531 -0
- alysis_code/ide/forge_protocol.py +3112 -0
- alysis_code/ide/forge_request_ledger.py +737 -0
- alysis_code/ide/health.py +965 -0
- alysis_code/ide/managed_browser.py +2251 -0
- alysis_code/ide/management_protocol.py +3414 -0
- alysis_code/ide/mcp_oauth_coordinator.py +744 -0
- alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
- alysis_code/ide/prompt_queue.py +1070 -0
- alysis_code/ide/protocol.py +191 -0
- alysis_code/ide/resumable_swarm.py +1543 -0
- alysis_code/ide/session_search.py +295 -0
- alysis_code/ide/stdio_bridge.py +9935 -0
- alysis_code/ide/structured_state.py +1579 -0
- alysis_code/ide/swarm_protocol.py +816 -0
- alysis_code/integration_gate.py +506 -0
- alysis_code/interactive_input_guard.py +39 -0
- alysis_code/interactive_plan_mode.py +26 -0
- alysis_code/internal_artifacts.py +179 -0
- alysis_code/knowledge_base.py +1409 -0
- alysis_code/knowledge_capture.py +1190 -0
- alysis_code/knowledge_librarian.py +605 -0
- alysis_code/language_policy.py +34 -0
- alysis_code/litellm_static_provider.py +535 -0
- alysis_code/llm/__init__.py +1 -0
- alysis_code/llm/anthropic_messages.py +2288 -0
- alysis_code/llm/base.py +71 -0
- alysis_code/llm/cache_capabilities.py +985 -0
- alysis_code/llm/cache_control_blocks.py +244 -0
- alysis_code/llm/cache_policy.py +388 -0
- alysis_code/llm/factory.py +373 -0
- alysis_code/llm/gemini_generate_content.py +2652 -0
- alysis_code/llm/gemini_interactions.py +739 -0
- alysis_code/llm/metadata.py +450 -0
- alysis_code/llm/openai_compat.py +2947 -0
- alysis_code/llm/openai_responses.py +2604 -0
- alysis_code/llm/protocols.py +609 -0
- alysis_code/llm/provider_limits.py +525 -0
- alysis_code/llm/request_plan.py +389 -0
- alysis_code/llm/request_shape.py +238 -0
- alysis_code/llm/streaming.py +108 -0
- alysis_code/llm/temperature_compat.py +78 -0
- alysis_code/llm/types.py +195 -0
- alysis_code/llm/usage_normalization.py +222 -0
- alysis_code/llm_error_display.py +315 -0
- alysis_code/logging_redaction.py +326 -0
- alysis_code/managed_host_deadline.py +196 -0
- alysis_code/mcp/README.md +33 -0
- alysis_code/mcp/__init__.py +24 -0
- alysis_code/mcp/client.py +1137 -0
- alysis_code/mcp/config.py +597 -0
- alysis_code/mcp/errors.py +113 -0
- alysis_code/mcp/forge_scope.py +154 -0
- alysis_code/mcp/jsonrpc.py +214 -0
- alysis_code/mcp/manager.py +2308 -0
- alysis_code/mcp/models.py +666 -0
- alysis_code/mcp/oauth.py +972 -0
- alysis_code/mcp/oauth_runtime.py +310 -0
- alysis_code/mcp/oauth_store.py +276 -0
- alysis_code/mcp/prompts.py +329 -0
- alysis_code/mcp/resources.py +295 -0
- alysis_code/mcp/roots.py +106 -0
- alysis_code/mcp/server_requests.py +75 -0
- alysis_code/mcp/token_store.py +859 -0
- alysis_code/mcp/transport_http.py +1338 -0
- alysis_code/mcp/transport_stdio.py +1267 -0
- alysis_code/mcp/untrusted_content.py +119 -0
- alysis_code/merge_conflict_reviewer.py +729 -0
- alysis_code/model_catalog/__init__.py +1 -0
- alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
- alysis_code/model_metadata_policy.py +223 -0
- alysis_code/model_metadata_utils.py +103 -0
- alysis_code/model_registry.py +1420 -0
- alysis_code/model_router.py +147 -0
- alysis_code/permission_policy.py +1016 -0
- alysis_code/personas.py +451 -0
- alysis_code/pipeline_facts.py +233 -0
- alysis_code/plan_assistant.py +4763 -0
- alysis_code/plan_mode.py +393 -0
- alysis_code/plan_reconciliation.py +1228 -0
- alysis_code/plan_repair.py +652 -0
- alysis_code/plan_validation.py +1099 -0
- alysis_code/planning_constraints.py +904 -0
- alysis_code/policy.py +95 -0
- alysis_code/preview_server.py +457 -0
- alysis_code/process_reaping.py +566 -0
- alysis_code/profile_presets.py +1834 -0
- alysis_code/profiles.py +666 -0
- alysis_code/provider_auth/__init__.py +29 -0
- alysis_code/provider_auth/base.py +99 -0
- alysis_code/provider_auth/openai_codex.py +951 -0
- alysis_code/provider_auth/registry.py +76 -0
- alysis_code/provider_auth/store.py +125 -0
- alysis_code/provider_diagnostics.py +1209 -0
- alysis_code/provider_model_catalog.py +685 -0
- alysis_code/provider_telemetry.py +1699 -0
- alysis_code/provider_url.py +75 -0
- alysis_code/reasoning_contracts.py +911 -0
- alysis_code/remote_sync.py +350 -0
- alysis_code/replanning.py +1195 -0
- alysis_code/repo_scan.py +1152 -0
- alysis_code/request_estimation.py +296 -0
- alysis_code/review_gate.py +617 -0
- alysis_code/run_lock.py +1141 -0
- alysis_code/run_outcome.py +58 -0
- alysis_code/run_provenance.py +774 -0
- alysis_code/run_state.py +445 -0
- alysis_code/runtime_artifacts.py +116 -0
- alysis_code/runtime_context_features.py +78 -0
- alysis_code/runtime_kind.py +52 -0
- alysis_code/safety/__init__.py +11 -0
- alysis_code/safety/mcp_sanitize.py +29 -0
- alysis_code/safety/safe_http.py +297 -0
- alysis_code/safety/subagent_report.py +184 -0
- alysis_code/sandbox_doctor.py +682 -0
- alysis_code/sandbox_runner.py +1025 -0
- alysis_code/sandbox_settings.py +423 -0
- alysis_code/serialized_paths.py +355 -0
- alysis_code/server/__init__.py +3 -0
- alysis_code/server/app.py +367 -0
- alysis_code/server/auth.py +34 -0
- alysis_code/server/job_config.py +30 -0
- alysis_code/server/settings.py +215 -0
- alysis_code/server/store.py +193 -0
- alysis_code/server/worker_runner.py +657 -0
- alysis_code/service_persistence.py +355 -0
- alysis_code/session_artifacts.py +108 -0
- alysis_code/session_metrics.py +331 -0
- alysis_code/session_store.py +624 -0
- alysis_code/skills/README.md +34 -0
- alysis_code/skills/__init__.py +104 -0
- alysis_code/skills/conventions.py +84 -0
- alysis_code/skills/discovery.py +176 -0
- alysis_code/skills/eval_models.py +232 -0
- alysis_code/skills/eval_runner.py +372 -0
- alysis_code/skills/evals.py +1344 -0
- alysis_code/skills/install.py +293 -0
- alysis_code/skills/loader.py +118 -0
- alysis_code/skills/matching.py +103 -0
- alysis_code/skills/models.py +71 -0
- alysis_code/skills/paths.py +56 -0
- alysis_code/skills/prompting.py +500 -0
- alysis_code/skills/scaffold.py +142 -0
- alysis_code/skills/state.py +441 -0
- alysis_code/skills/transactions.py +125 -0
- alysis_code/skills/validation.py +304 -0
- alysis_code/step_budget.py +238 -0
- alysis_code/subagent_labels.py +49 -0
- alysis_code/subagents.py +1072 -0
- alysis_code/surface/__init__.py +80 -0
- alysis_code/surface/base.py +305 -0
- alysis_code/surface/console.py +387 -0
- alysis_code/surface/events.py +372 -0
- alysis_code/surface/hidden_surface.py +529 -0
- alysis_code/surface/noop_surface.py +219 -0
- alysis_code/surface/rich_surface.py +1555 -0
- alysis_code/surface/styles.py +67 -0
- alysis_code/surface/theme.py +455 -0
- alysis_code/surface/types.py +100 -0
- alysis_code/swarm_backend.py +926 -0
- alysis_code/swarm_orchestrator.py +4020 -0
- alysis_code/swarm_scheduler.py +441 -0
- alysis_code/swarm_trace.py +429 -0
- alysis_code/swarm_worker.py +2119 -0
- alysis_code/swarm_write_guard.py +348 -0
- alysis_code/task_dependencies.py +170 -0
- alysis_code/task_readiness.py +992 -0
- alysis_code/task_scope.py +2148 -0
- alysis_code/terminal_manager.py +762 -0
- alysis_code/terminal_ownership.py +460 -0
- alysis_code/text_normalization.py +30 -0
- alysis_code/token_budget.py +97 -0
- alysis_code/tools/README.md +34 -0
- alysis_code/tools/__init__.py +1 -0
- alysis_code/tools/artifacts.py +127 -0
- alysis_code/tools/availability.py +188 -0
- alysis_code/tools/fs.py +1456 -0
- alysis_code/tools/git.py +461 -0
- alysis_code/tools/history.py +229 -0
- alysis_code/tools/http_timeout.py +78 -0
- alysis_code/tools/image_generation.py +552 -0
- alysis_code/tools/registry.py +2936 -0
- alysis_code/tools/repo_map.py +476 -0
- alysis_code/tools/search.py +563 -0
- alysis_code/tools/shell.py +135 -0
- alysis_code/tools/symbols.py +1350 -0
- alysis_code/tools/test_discovery.py +643 -0
- alysis_code/tools/web.py +482 -0
- alysis_code/tools/web_search.py +2012 -0
- alysis_code/tools/web_search_dashscope.py +557 -0
- alysis_code/tools/web_search_ddgs.py +221 -0
- alysis_code/tools/web_search_provider_adapters.py +1429 -0
- alysis_code/tools/web_search_tavily.py +194 -0
- alysis_code/updates.py +933 -0
- alysis_code/usage_tracker.py +1990 -0
- alysis_code/verification_command_analysis.py +1004 -0
- alysis_code/verification_contract.py +574 -0
- alysis_code/verification_failure_summary.py +273 -0
- alysis_code/verification_repair.py +385 -0
- alysis_code/verify_gate.py +3129 -0
- alysis_code/web_research.py +1872 -0
- alysis_code/web_search_adapters.py +66 -0
- alysis_code/web_search_policy.py +27 -0
- alysis_code/workspace_binding.py +389 -0
- alysis_code/workspace_binding_ui.py +408 -0
- alysis_code/workspace_context.py +273 -0
- alysis_code/workspace_isolation.py +138 -0
- alysis_code/workspace_provisioning.py +455 -0
- alysis_code-0.13.0.dist-info/METADATA +507 -0
- alysis_code-0.13.0.dist-info/RECORD +458 -0
- alysis_code-0.13.0.dist-info/WHEEL +4 -0
- alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
- alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
- alysis_code-0.13.0.dist-info/licenses/NOTICE +4 -0
|
@@ -0,0 +1,1420 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
from urllib.parse import urlsplit
|
|
6
|
+
|
|
7
|
+
from .branding import env_get
|
|
8
|
+
from .chatgpt_codex_static_provider import (
|
|
9
|
+
CHATGPT_CODEX_SUBSCRIPTION_CATALOG_SOURCE,
|
|
10
|
+
resolve_chatgpt_codex_static_model,
|
|
11
|
+
)
|
|
12
|
+
from .config import AppConfig
|
|
13
|
+
from .litellm_static_provider import (
|
|
14
|
+
BUNDLED_MODEL_CATALOG_SOURCE,
|
|
15
|
+
resolve_litellm_static_metadata,
|
|
16
|
+
)
|
|
17
|
+
from .model_metadata_utils import (
|
|
18
|
+
model_name_variants,
|
|
19
|
+
normalize_base_url,
|
|
20
|
+
parse_bool,
|
|
21
|
+
parse_non_negative_float,
|
|
22
|
+
parse_positive_int,
|
|
23
|
+
)
|
|
24
|
+
from .provider_url import known_provider_key_from_base_url
|
|
25
|
+
|
|
26
|
+
_INT_FIELDS = ("context_window_tokens", "max_output_tokens")
|
|
27
|
+
_FLOAT_FIELDS = (
|
|
28
|
+
"input_cost_per_token",
|
|
29
|
+
"output_cost_per_token",
|
|
30
|
+
"cache_read_input_cost_per_token",
|
|
31
|
+
"cache_creation_input_cost_per_token",
|
|
32
|
+
"cache_creation_5m_input_cost_per_token",
|
|
33
|
+
"cache_creation_1h_input_cost_per_token",
|
|
34
|
+
"reasoning_output_cost_per_token",
|
|
35
|
+
)
|
|
36
|
+
_BOOL_FIELDS = ("supports_vision", "supports_reasoning")
|
|
37
|
+
_TRACKED_FIELDS = (
|
|
38
|
+
"context_window_tokens",
|
|
39
|
+
"max_output_tokens",
|
|
40
|
+
"supports_vision",
|
|
41
|
+
"supports_reasoning",
|
|
42
|
+
"input_cost_per_token",
|
|
43
|
+
"output_cost_per_token",
|
|
44
|
+
"cache_read_input_cost_per_token",
|
|
45
|
+
"cache_creation_input_cost_per_token",
|
|
46
|
+
"cache_creation_5m_input_cost_per_token",
|
|
47
|
+
"cache_creation_1h_input_cost_per_token",
|
|
48
|
+
"reasoning_output_cost_per_token",
|
|
49
|
+
)
|
|
50
|
+
DEFAULT_UNKNOWN_MODEL_CONTEXT_WINDOW_TOKENS = 128_000
|
|
51
|
+
DEFAULT_UNKNOWN_MODEL_MAX_OUTPUT_TOKENS = 8_192
|
|
52
|
+
|
|
53
|
+
_FALLBACKS: dict[str, Any] = {
|
|
54
|
+
"context_window_tokens": DEFAULT_UNKNOWN_MODEL_CONTEXT_WINDOW_TOKENS,
|
|
55
|
+
"max_output_tokens": DEFAULT_UNKNOWN_MODEL_MAX_OUTPUT_TOKENS,
|
|
56
|
+
"supports_vision": False,
|
|
57
|
+
"supports_reasoning": None,
|
|
58
|
+
"input_cost_per_token": None,
|
|
59
|
+
"output_cost_per_token": None,
|
|
60
|
+
"cache_read_input_cost_per_token": None,
|
|
61
|
+
"cache_creation_input_cost_per_token": None,
|
|
62
|
+
"cache_creation_5m_input_cost_per_token": None,
|
|
63
|
+
"cache_creation_1h_input_cost_per_token": None,
|
|
64
|
+
"reasoning_output_cost_per_token": None,
|
|
65
|
+
}
|
|
66
|
+
_ENV_FIELD_MAP: dict[str, str] = {
|
|
67
|
+
"context_window_tokens": "ALYSIS_CONTEXT_WINDOW",
|
|
68
|
+
"max_output_tokens": "ALYSIS_MAX_OUTPUT_TOKENS",
|
|
69
|
+
"supports_vision": "ALYSIS_SUPPORTS_VISION",
|
|
70
|
+
"supports_reasoning": "ALYSIS_SUPPORTS_REASONING",
|
|
71
|
+
"input_cost_per_token": "ALYSIS_INPUT_COST_PER_TOKEN",
|
|
72
|
+
"output_cost_per_token": "ALYSIS_OUTPUT_COST_PER_TOKEN",
|
|
73
|
+
"cache_read_input_cost_per_token": "ALYSIS_CACHE_READ_INPUT_COST_PER_TOKEN",
|
|
74
|
+
"cache_creation_input_cost_per_token": "ALYSIS_CACHE_CREATION_INPUT_COST_PER_TOKEN",
|
|
75
|
+
"cache_creation_5m_input_cost_per_token": ("ALYSIS_CACHE_CREATION_5M_INPUT_COST_PER_TOKEN"),
|
|
76
|
+
"cache_creation_1h_input_cost_per_token": ("ALYSIS_CACHE_CREATION_1H_INPUT_COST_PER_TOKEN"),
|
|
77
|
+
"reasoning_output_cost_per_token": "ALYSIS_REASONING_OUTPUT_COST_PER_TOKEN",
|
|
78
|
+
}
|
|
79
|
+
_DEPRECATED_MODEL_CAPABILITIES_WARNING = (
|
|
80
|
+
"Config key `model_capabilities` is deprecated and ignored; use `model_metadata_overrides`."
|
|
81
|
+
)
|
|
82
|
+
_FALLBACK_WARNING = (
|
|
83
|
+
"Using fallback context/max_output; set model_metadata_overrides for best performance."
|
|
84
|
+
)
|
|
85
|
+
OFFICIAL_PROVIDER_MODEL_CATALOG_SOURCE = "official_provider_model_catalog"
|
|
86
|
+
CANONICAL_MODEL_CATALOG_SOURCE = "canonical_model_catalog"
|
|
87
|
+
_CANONICAL_MODEL_SOURCES: dict[str, tuple[str, ...]] = {
|
|
88
|
+
"qwen3.8-max": ("https://help.aliyun.com/en/model-studio/qwen3-8-max",),
|
|
89
|
+
"deepseek-v4-pro": ("https://api-docs.deepseek.com/quick_start/pricing/",),
|
|
90
|
+
"deepseek-v4-flash": ("https://api-docs.deepseek.com/quick_start/pricing/",),
|
|
91
|
+
"deepseek-v4-flash-vision-exp": ("https://api-docs.deepseek.com/quick_start/pricing/",),
|
|
92
|
+
}
|
|
93
|
+
_CANONICAL_MODEL_METADATA: dict[str, dict[str, Any]] = {
|
|
94
|
+
"qwen3.8-max": {
|
|
95
|
+
"context_window_tokens": 1_000_000,
|
|
96
|
+
"max_output_tokens": 131_072,
|
|
97
|
+
"supports_vision": True,
|
|
98
|
+
"supports_reasoning": True,
|
|
99
|
+
},
|
|
100
|
+
"deepseek-v4-pro": {
|
|
101
|
+
"context_window_tokens": 1_000_000,
|
|
102
|
+
"max_output_tokens": 384_000,
|
|
103
|
+
"supports_vision": False,
|
|
104
|
+
"supports_reasoning": True,
|
|
105
|
+
},
|
|
106
|
+
"deepseek-v4-flash": {
|
|
107
|
+
"context_window_tokens": 1_000_000,
|
|
108
|
+
"max_output_tokens": 384_000,
|
|
109
|
+
"supports_vision": False,
|
|
110
|
+
"supports_reasoning": True,
|
|
111
|
+
},
|
|
112
|
+
"deepseek-v4-flash-vision-exp": {
|
|
113
|
+
"context_window_tokens": 1_000_000,
|
|
114
|
+
"max_output_tokens": 384_000,
|
|
115
|
+
"supports_vision": True,
|
|
116
|
+
"supports_reasoning": True,
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
# Provider model ids are routing details, not separate capability records. This
|
|
120
|
+
# declarative alias table lets each route inherit the canonical model's limits
|
|
121
|
+
# while retaining provider-specific pricing, availability, and stricter caps.
|
|
122
|
+
_PROVIDER_CANONICAL_MODEL_IDS: dict[str, dict[str, str]] = {
|
|
123
|
+
"qwen": {
|
|
124
|
+
"qwen3.8-max": "qwen3.8-max",
|
|
125
|
+
},
|
|
126
|
+
"deepseek": {
|
|
127
|
+
"deepseek-v4-pro": "deepseek-v4-pro",
|
|
128
|
+
"deepseek-v4-flash": "deepseek-v4-flash",
|
|
129
|
+
"deepseek-v4-flash-vision-exp": "deepseek-v4-flash-vision-exp",
|
|
130
|
+
},
|
|
131
|
+
"openrouter": {
|
|
132
|
+
"qwen/qwen3.8-max": "qwen3.8-max",
|
|
133
|
+
"deepseek/deepseek-v4-pro-0813": "deepseek-v4-pro",
|
|
134
|
+
"deepseek/deepseek-v4-flash-0731": "deepseek-v4-flash",
|
|
135
|
+
"deepseek/deepseek-v4-flash-vision-exp": "deepseek-v4-flash-vision-exp",
|
|
136
|
+
},
|
|
137
|
+
"together": {
|
|
138
|
+
"deepseek-ai/DeepSeek-V4-Pro-0813": "deepseek-v4-pro",
|
|
139
|
+
"deepseek-ai/DeepSeek-V4-Flash-0731": "deepseek-v4-flash",
|
|
140
|
+
},
|
|
141
|
+
"fireworks": {
|
|
142
|
+
"accounts/fireworks/models/deepseek-v4-pro-0813": "deepseek-v4-pro",
|
|
143
|
+
"accounts/fireworks/models/deepseek-v4-flash-0731": "deepseek-v4-flash",
|
|
144
|
+
},
|
|
145
|
+
"nvidia": {
|
|
146
|
+
"deepseek-ai/deepseek-v4-pro": "deepseek-v4-pro",
|
|
147
|
+
"deepseek-ai/deepseek-v4-flash": "deepseek-v4-flash",
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
_OFFICIAL_PROVIDER_MODEL_SOURCES: dict[str, dict[str, tuple[str, ...]]] = {
|
|
151
|
+
"zai_coding_plan": {
|
|
152
|
+
"glm-5.3": ("https://docs.z.ai/guides/llm/glm-5.3",),
|
|
153
|
+
"glm-5-turbo": ("https://docs.z.ai/guides/llm/glm-5-turbo",),
|
|
154
|
+
"glm-4.7": ("https://docs.z.ai/guides/llm/glm-4.7",),
|
|
155
|
+
},
|
|
156
|
+
"xai": {
|
|
157
|
+
"grok-4.6": (
|
|
158
|
+
"https://docs.x.ai/developers/grok-4-6",
|
|
159
|
+
"https://docs.x.ai/developers/pricing",
|
|
160
|
+
),
|
|
161
|
+
},
|
|
162
|
+
"gemini": {
|
|
163
|
+
model: (model_url, "https://ai.google.dev/gemini-api/docs/pricing")
|
|
164
|
+
for model, model_url in {
|
|
165
|
+
"gemini-3.7-flash": ("https://ai.google.dev/gemini-api/docs/models/gemini-3.7-flash"),
|
|
166
|
+
"gemini-3.6-flash": ("https://ai.google.dev/gemini-api/docs/models/gemini-3.6-flash"),
|
|
167
|
+
"gemini-3.5-flash-lite": (
|
|
168
|
+
"https://ai.google.dev/gemini-api/docs/models/gemini-3.5-flash-lite"
|
|
169
|
+
),
|
|
170
|
+
}.items()
|
|
171
|
+
},
|
|
172
|
+
"qwen": {
|
|
173
|
+
"qwen3.8-max": (
|
|
174
|
+
"https://help.aliyun.com/en/model-studio/qwen3-8-max",
|
|
175
|
+
"https://help.aliyun.com/en/model-studio/qwen-api-via-openai-chat-completions",
|
|
176
|
+
),
|
|
177
|
+
},
|
|
178
|
+
"deepseek": {
|
|
179
|
+
model: (
|
|
180
|
+
"https://api-docs.deepseek.com/quick_start/pricing/",
|
|
181
|
+
"https://api-docs.deepseek.com/guides/thinking_mode/",
|
|
182
|
+
)
|
|
183
|
+
for model in (
|
|
184
|
+
"deepseek-v4-pro",
|
|
185
|
+
"deepseek-v4-flash",
|
|
186
|
+
"deepseek-v4-flash-vision-exp",
|
|
187
|
+
)
|
|
188
|
+
},
|
|
189
|
+
"openrouter": {
|
|
190
|
+
model: ("https://openrouter.ai/api/v1/models",)
|
|
191
|
+
for model in (
|
|
192
|
+
"qwen/qwen3.8-max",
|
|
193
|
+
"deepseek/deepseek-v4-pro-0813",
|
|
194
|
+
"deepseek/deepseek-v4-flash-0731",
|
|
195
|
+
"deepseek/deepseek-v4-flash-vision-exp",
|
|
196
|
+
)
|
|
197
|
+
},
|
|
198
|
+
"together": {
|
|
199
|
+
model: ("https://docs.together.ai/docs/serverless/models",)
|
|
200
|
+
for model in (
|
|
201
|
+
"deepseek-ai/DeepSeek-V4-Pro-0813",
|
|
202
|
+
"deepseek-ai/DeepSeek-V4-Flash-0731",
|
|
203
|
+
)
|
|
204
|
+
},
|
|
205
|
+
"fireworks": {
|
|
206
|
+
"accounts/fireworks/models/deepseek-v4-pro-0813": (
|
|
207
|
+
"https://fireworks.ai/models/deepseek-ai/deepseek-v4-pro-0813",
|
|
208
|
+
),
|
|
209
|
+
"accounts/fireworks/models/deepseek-v4-flash-0731": (
|
|
210
|
+
"https://fireworks.ai/models/deepseek-ai/deepseek-v4-flash-0731",
|
|
211
|
+
),
|
|
212
|
+
},
|
|
213
|
+
"nvidia": {
|
|
214
|
+
"nvidia/nemotron-3-super-120b-a12b": (
|
|
215
|
+
"https://build.nvidia.com/nvidia/nemotron-3-super-120b-a12b/build",
|
|
216
|
+
),
|
|
217
|
+
"nvidia/nemotron-3-ultra-550b-a55b": (
|
|
218
|
+
"https://build.nvidia.com/nvidia/nemotron-3-ultra-550b-a55b",
|
|
219
|
+
),
|
|
220
|
+
"nvidia/nemotron-3-nano-30b-a3b": (
|
|
221
|
+
"https://build.nvidia.com/nvidia/nemotron-3-nano-30b-a3b",
|
|
222
|
+
),
|
|
223
|
+
"deepseek-ai/deepseek-v4-pro": ("https://build.nvidia.com/deepseek-ai/deepseek-v4-pro",),
|
|
224
|
+
"deepseek-ai/deepseek-v4-flash": (
|
|
225
|
+
"https://build.nvidia.com/deepseek-ai/deepseek-v4-flash",
|
|
226
|
+
),
|
|
227
|
+
},
|
|
228
|
+
"moonshot": {
|
|
229
|
+
model: ("https://platform.moonshot.ai/docs/pricing/chat",)
|
|
230
|
+
for model in (
|
|
231
|
+
"kimi-k3",
|
|
232
|
+
"kimi-k2.7-code",
|
|
233
|
+
"kimi-k2.7-code-highspeed",
|
|
234
|
+
"kimi-k2.6",
|
|
235
|
+
)
|
|
236
|
+
},
|
|
237
|
+
"anthropic": {
|
|
238
|
+
"claude-opus-5": (
|
|
239
|
+
"https://docs.anthropic.com/en/docs/about-claude/models/overview",
|
|
240
|
+
"https://docs.anthropic.com/en/docs/about-claude/pricing",
|
|
241
|
+
),
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
_OFFICIAL_PROVIDER_MODEL_METADATA: dict[str, dict[str, dict[str, Any]]] = {
|
|
245
|
+
"zai_coding_plan": {
|
|
246
|
+
# GLM-5.3 is currently exclusive to the subscription Coding Plan API;
|
|
247
|
+
# the general pay-per-token API is still marked "coming soon". Plan
|
|
248
|
+
# credits are not token prices, so this route intentionally carries no
|
|
249
|
+
# monetary cost fields.
|
|
250
|
+
"glm-5.3": {
|
|
251
|
+
"context_window_tokens": 1_000_000,
|
|
252
|
+
"max_output_tokens": 131_072,
|
|
253
|
+
"supports_vision": False,
|
|
254
|
+
"supports_reasoning": True,
|
|
255
|
+
},
|
|
256
|
+
"glm-5-turbo": {
|
|
257
|
+
"context_window_tokens": 200_000,
|
|
258
|
+
"max_output_tokens": 131_072,
|
|
259
|
+
"supports_vision": False,
|
|
260
|
+
"supports_reasoning": True,
|
|
261
|
+
},
|
|
262
|
+
"glm-4.7": {
|
|
263
|
+
"context_window_tokens": 200_000,
|
|
264
|
+
"max_output_tokens": 131_072,
|
|
265
|
+
"supports_vision": False,
|
|
266
|
+
"supports_reasoning": True,
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
"xai": {
|
|
270
|
+
# Grok 4.6 postdates the provenance-pinned LiteLLM snapshot. xAI
|
|
271
|
+
# publishes a 500K shared context with no separate text-output limit,
|
|
272
|
+
# so use the full window here and let the registry's generic
|
|
273
|
+
# shared-window policy reserve a practical output allowance. These are
|
|
274
|
+
# the base rates for prompts up to 200K tokens; xAI doubles all three
|
|
275
|
+
# rates above that threshold, which this flat-price schema cannot
|
|
276
|
+
# represent yet.
|
|
277
|
+
"grok-4.6": {
|
|
278
|
+
"context_window_tokens": 500_000,
|
|
279
|
+
"max_output_tokens": 500_000,
|
|
280
|
+
"supports_vision": True,
|
|
281
|
+
"supports_reasoning": True,
|
|
282
|
+
"input_cost_per_token": 0.000002,
|
|
283
|
+
"output_cost_per_token": 0.000006,
|
|
284
|
+
"cache_read_input_cost_per_token": 0.0000005,
|
|
285
|
+
"reasoning_output_cost_per_token": 0.000006,
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
"gemini": {
|
|
289
|
+
# These GA models postdate the provenance-pinned LiteLLM snapshot.
|
|
290
|
+
# Capacity and current standard-tier pricing are from Google's model
|
|
291
|
+
# and pricing pages. The 3.7/3.6 introductory rates expire 2026-12-31.
|
|
292
|
+
"gemini-3.7-flash": {
|
|
293
|
+
"context_window_tokens": 1_048_576,
|
|
294
|
+
"max_output_tokens": 65_536,
|
|
295
|
+
"supports_vision": True,
|
|
296
|
+
"supports_reasoning": True,
|
|
297
|
+
"input_cost_per_token": 0.00000075,
|
|
298
|
+
"output_cost_per_token": 0.00000375,
|
|
299
|
+
"cache_read_input_cost_per_token": 0.000000075,
|
|
300
|
+
"reasoning_output_cost_per_token": 0.00000375,
|
|
301
|
+
},
|
|
302
|
+
"gemini-3.6-flash": {
|
|
303
|
+
"context_window_tokens": 1_048_576,
|
|
304
|
+
"max_output_tokens": 65_536,
|
|
305
|
+
"supports_vision": True,
|
|
306
|
+
"supports_reasoning": True,
|
|
307
|
+
"input_cost_per_token": 0.00000075,
|
|
308
|
+
"output_cost_per_token": 0.00000375,
|
|
309
|
+
"cache_read_input_cost_per_token": 0.000000075,
|
|
310
|
+
"reasoning_output_cost_per_token": 0.00000375,
|
|
311
|
+
},
|
|
312
|
+
"gemini-3.5-flash-lite": {
|
|
313
|
+
"context_window_tokens": 1_048_576,
|
|
314
|
+
"max_output_tokens": 65_536,
|
|
315
|
+
"supports_vision": True,
|
|
316
|
+
"supports_reasoning": True,
|
|
317
|
+
"input_cost_per_token": 0.0000003,
|
|
318
|
+
"output_cost_per_token": 0.0000025,
|
|
319
|
+
"cache_read_input_cost_per_token": 0.00000003,
|
|
320
|
+
"reasoning_output_cost_per_token": 0.0000025,
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
"qwen": {
|
|
324
|
+
# Qwen3.8-Max is available from the China, Singapore, Frankfurt, US,
|
|
325
|
+
# and Tokyo surfaces under the same bare model id. Prices vary by
|
|
326
|
+
# region/currency, which this provider-level flat schema cannot encode,
|
|
327
|
+
# so costs remain unknown instead of applying one region globally.
|
|
328
|
+
"qwen3.8-max": {},
|
|
329
|
+
},
|
|
330
|
+
"deepseek": {
|
|
331
|
+
# DeepSeek charges half price outside two daily peak windows. The
|
|
332
|
+
# registry cannot express time-dependent rates, so use peak pricing as
|
|
333
|
+
# a conservative upper-bound estimate rather than under-reporting cost.
|
|
334
|
+
"deepseek-v4-pro": {
|
|
335
|
+
"input_cost_per_token": 0.00000132,
|
|
336
|
+
"output_cost_per_token": 0.00000396,
|
|
337
|
+
"cache_read_input_cost_per_token": 0.000000044,
|
|
338
|
+
"reasoning_output_cost_per_token": 0.00000396,
|
|
339
|
+
},
|
|
340
|
+
"deepseek-v4-flash": {
|
|
341
|
+
"input_cost_per_token": 0.00000044,
|
|
342
|
+
"output_cost_per_token": 0.00000132,
|
|
343
|
+
"cache_read_input_cost_per_token": 0.000000014,
|
|
344
|
+
"reasoning_output_cost_per_token": 0.00000132,
|
|
345
|
+
},
|
|
346
|
+
"deepseek-v4-flash-vision-exp": {
|
|
347
|
+
"input_cost_per_token": 0.00000044,
|
|
348
|
+
"output_cost_per_token": 0.00000132,
|
|
349
|
+
"cache_read_input_cost_per_token": 0.000000014,
|
|
350
|
+
"reasoning_output_cost_per_token": 0.00000132,
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
"openrouter": {
|
|
354
|
+
"qwen/qwen3.8-max": {
|
|
355
|
+
"input_cost_per_token": 0.000002,
|
|
356
|
+
"output_cost_per_token": 0.000006,
|
|
357
|
+
"cache_read_input_cost_per_token": 0.00000025,
|
|
358
|
+
"cache_creation_input_cost_per_token": 0.0000025,
|
|
359
|
+
"reasoning_output_cost_per_token": 0.000006,
|
|
360
|
+
},
|
|
361
|
+
"deepseek/deepseek-v4-pro-0813": {
|
|
362
|
+
"context_window_tokens": 1_048_576,
|
|
363
|
+
"input_cost_per_token": 0.000001188,
|
|
364
|
+
"output_cost_per_token": 0.000003564,
|
|
365
|
+
"cache_read_input_cost_per_token": 0.0000000396,
|
|
366
|
+
"reasoning_output_cost_per_token": 0.000003564,
|
|
367
|
+
},
|
|
368
|
+
"deepseek/deepseek-v4-flash-0731": {
|
|
369
|
+
# OpenRouter's public model catalog advertises a 1.31M routing
|
|
370
|
+
# window for this snapshot even though its current top provider is
|
|
371
|
+
# capped at 1,048,576 tokens. Preserve the gateway-level contract;
|
|
372
|
+
# route-specific limits remain OpenRouter's responsibility.
|
|
373
|
+
"context_window_tokens": 1_310_720,
|
|
374
|
+
"input_cost_per_token": 0.00000008,
|
|
375
|
+
"output_cost_per_token": 0.00000018,
|
|
376
|
+
"cache_read_input_cost_per_token": 0.000000016,
|
|
377
|
+
"reasoning_output_cost_per_token": 0.00000018,
|
|
378
|
+
},
|
|
379
|
+
"deepseek/deepseek-v4-flash-vision-exp": {
|
|
380
|
+
"context_window_tokens": 1_048_576,
|
|
381
|
+
# OpenRouter exposes DeepSeek's time-window overrides. Use the
|
|
382
|
+
# highest published rate so a flat estimate does not undercount.
|
|
383
|
+
"input_cost_per_token": 0.00000044,
|
|
384
|
+
"output_cost_per_token": 0.00000132,
|
|
385
|
+
"cache_read_input_cost_per_token": 0.000000014,
|
|
386
|
+
"reasoning_output_cost_per_token": 0.00000132,
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
"together": {
|
|
390
|
+
"deepseek-ai/DeepSeek-V4-Pro-0813": {
|
|
391
|
+
"context_window_tokens": 1_048_576,
|
|
392
|
+
"input_cost_per_token": 0.00000132,
|
|
393
|
+
"output_cost_per_token": 0.00000396,
|
|
394
|
+
"cache_read_input_cost_per_token": 0.00000013,
|
|
395
|
+
"reasoning_output_cost_per_token": 0.00000396,
|
|
396
|
+
},
|
|
397
|
+
"deepseek-ai/DeepSeek-V4-Flash-0731": {
|
|
398
|
+
"input_cost_per_token": 0.00000014,
|
|
399
|
+
"output_cost_per_token": 0.00000028,
|
|
400
|
+
"cache_read_input_cost_per_token": 0.00000003,
|
|
401
|
+
"reasoning_output_cost_per_token": 0.00000028,
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
"fireworks": {
|
|
405
|
+
"accounts/fireworks/models/deepseek-v4-pro-0813": {
|
|
406
|
+
"context_window_tokens": 1_048_576,
|
|
407
|
+
"input_cost_per_token": 0.00000132,
|
|
408
|
+
"output_cost_per_token": 0.00000396,
|
|
409
|
+
"cache_read_input_cost_per_token": 0.000000044,
|
|
410
|
+
"reasoning_output_cost_per_token": 0.00000396,
|
|
411
|
+
},
|
|
412
|
+
"accounts/fireworks/models/deepseek-v4-flash-0731": {
|
|
413
|
+
"context_window_tokens": 1_048_576,
|
|
414
|
+
"input_cost_per_token": 0.00000014,
|
|
415
|
+
"output_cost_per_token": 0.00000028,
|
|
416
|
+
"cache_read_input_cost_per_token": 0.000000028,
|
|
417
|
+
"reasoning_output_cost_per_token": 0.00000028,
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
"nvidia": {
|
|
421
|
+
# NVIDIA's hosted NIM catalog publishes the Nemotron 3 Super and Ultra
|
|
422
|
+
# models with 1M context. Nano's hosted endpoint currently advertises
|
|
423
|
+
# 262K. The hosted Nemotron endpoints accept up to 32,768 output tokens;
|
|
424
|
+
# 16,384 is their default, not their ceiling. NVIDIA-hosted DeepSeek V4
|
|
425
|
+
# uses a separate 16,384-token ceiling. Free Endpoint access is
|
|
426
|
+
# rate-limited prototyping, so no durable token price is encoded here.
|
|
427
|
+
"nvidia/nemotron-3-super-120b-a12b": {
|
|
428
|
+
"context_window_tokens": 1_048_576,
|
|
429
|
+
"max_output_tokens": 32_768,
|
|
430
|
+
"supports_vision": False,
|
|
431
|
+
"supports_reasoning": True,
|
|
432
|
+
},
|
|
433
|
+
"nvidia/nemotron-3-ultra-550b-a55b": {
|
|
434
|
+
"context_window_tokens": 1_048_576,
|
|
435
|
+
"max_output_tokens": 32_768,
|
|
436
|
+
"supports_vision": False,
|
|
437
|
+
"supports_reasoning": True,
|
|
438
|
+
},
|
|
439
|
+
"nvidia/nemotron-3-nano-30b-a3b": {
|
|
440
|
+
"context_window_tokens": 262_144,
|
|
441
|
+
"max_output_tokens": 32_768,
|
|
442
|
+
"supports_vision": False,
|
|
443
|
+
"supports_reasoning": True,
|
|
444
|
+
},
|
|
445
|
+
"deepseek-ai/deepseek-v4-pro": {
|
|
446
|
+
"context_window_tokens": 1_048_576,
|
|
447
|
+
"max_output_tokens": 16_384,
|
|
448
|
+
},
|
|
449
|
+
"deepseek-ai/deepseek-v4-flash": {
|
|
450
|
+
"context_window_tokens": 1_048_576,
|
|
451
|
+
"max_output_tokens": 16_384,
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
"moonshot": {
|
|
455
|
+
"kimi-k3": {
|
|
456
|
+
"context_window_tokens": 1_048_576,
|
|
457
|
+
# K3 can use the full context for output, but 131,072 is the API's
|
|
458
|
+
# normal completion allowance and therefore the useful local reserve.
|
|
459
|
+
"max_output_tokens": 131_072,
|
|
460
|
+
"supports_vision": True,
|
|
461
|
+
"supports_reasoning": True,
|
|
462
|
+
"input_cost_per_token": 0.000003,
|
|
463
|
+
"output_cost_per_token": 0.000015,
|
|
464
|
+
"cache_read_input_cost_per_token": 0.0000003,
|
|
465
|
+
},
|
|
466
|
+
"kimi-k2.7-code": {
|
|
467
|
+
"context_window_tokens": 262_144,
|
|
468
|
+
"max_output_tokens": 32_768,
|
|
469
|
+
"supports_vision": True,
|
|
470
|
+
"supports_reasoning": True,
|
|
471
|
+
"input_cost_per_token": 0.00000095,
|
|
472
|
+
"output_cost_per_token": 0.000004,
|
|
473
|
+
"cache_read_input_cost_per_token": 0.00000019,
|
|
474
|
+
},
|
|
475
|
+
"kimi-k2.7-code-highspeed": {
|
|
476
|
+
"context_window_tokens": 262_144,
|
|
477
|
+
"max_output_tokens": 32_768,
|
|
478
|
+
"supports_vision": True,
|
|
479
|
+
"supports_reasoning": True,
|
|
480
|
+
"input_cost_per_token": 0.0000019,
|
|
481
|
+
"output_cost_per_token": 0.000008,
|
|
482
|
+
"cache_read_input_cost_per_token": 0.00000038,
|
|
483
|
+
},
|
|
484
|
+
"kimi-k2.6": {
|
|
485
|
+
"context_window_tokens": 262_144,
|
|
486
|
+
"max_output_tokens": 32_768,
|
|
487
|
+
"supports_vision": True,
|
|
488
|
+
"supports_reasoning": True,
|
|
489
|
+
"input_cost_per_token": 0.00000095,
|
|
490
|
+
"output_cost_per_token": 0.000004,
|
|
491
|
+
"cache_read_input_cost_per_token": 0.00000016,
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
"anthropic": {
|
|
495
|
+
# Opus 5 ships ahead of the vendored litellm mirror, and that mirror is
|
|
496
|
+
# refresh-policy pinned to an upstream commit — so its capacity lives
|
|
497
|
+
# here rather than as a hand-edit that would falsify the snapshot's
|
|
498
|
+
# provenance. Drop this entry once a catalog refresh carries the model.
|
|
499
|
+
# Same shape as its siblings: 1M input + 128K output, Opus 4.8 pricing.
|
|
500
|
+
"claude-opus-5": {
|
|
501
|
+
"context_window_tokens": 1_128_000,
|
|
502
|
+
"max_output_tokens": 128_000,
|
|
503
|
+
"supports_vision": True,
|
|
504
|
+
"supports_reasoning": True,
|
|
505
|
+
"input_cost_per_token": 0.000005,
|
|
506
|
+
"output_cost_per_token": 0.000025,
|
|
507
|
+
"cache_read_input_cost_per_token": 0.0000005,
|
|
508
|
+
"cache_creation_input_cost_per_token": 0.00000625,
|
|
509
|
+
"cache_creation_5m_input_cost_per_token": 0.00000625,
|
|
510
|
+
"cache_creation_1h_input_cost_per_token": 0.00001,
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
}
|
|
514
|
+
_BUILT_IN_MODEL_METADATA: dict[str, dict[str, Any]] = {
|
|
515
|
+
"deepseek-chat": {
|
|
516
|
+
"context_window_tokens": 1_000_000,
|
|
517
|
+
"max_output_tokens": 384_000,
|
|
518
|
+
"supports_vision": False,
|
|
519
|
+
"input_cost_per_token": 0.000000435,
|
|
520
|
+
"output_cost_per_token": 0.00000087,
|
|
521
|
+
},
|
|
522
|
+
"deepseek-reasoner": {
|
|
523
|
+
"context_window_tokens": 1_000_000,
|
|
524
|
+
"max_output_tokens": 384_000,
|
|
525
|
+
"supports_vision": False,
|
|
526
|
+
"input_cost_per_token": 0.000000435,
|
|
527
|
+
"output_cost_per_token": 0.00000087,
|
|
528
|
+
},
|
|
529
|
+
# Hosted Xiaomi MiMo trial models. The proxy serves these ids when allowlisted,
|
|
530
|
+
# and falls back server-side to its canonical MiMo model otherwise.
|
|
531
|
+
"mimo-v2.5-pro": {
|
|
532
|
+
"context_window_tokens": 1_000_000,
|
|
533
|
+
"max_output_tokens": 131_072,
|
|
534
|
+
"supports_vision": False,
|
|
535
|
+
"input_cost_per_token": 0.000001,
|
|
536
|
+
"output_cost_per_token": 0.000003,
|
|
537
|
+
},
|
|
538
|
+
"mimo-v2-flash": {
|
|
539
|
+
"context_window_tokens": 262_144,
|
|
540
|
+
"max_output_tokens": 65_536,
|
|
541
|
+
"supports_vision": False,
|
|
542
|
+
"input_cost_per_token": 0.0000001,
|
|
543
|
+
"output_cost_per_token": 0.0000003,
|
|
544
|
+
},
|
|
545
|
+
"mimo-v2.5": {
|
|
546
|
+
"context_window_tokens": 1_000_000,
|
|
547
|
+
"max_output_tokens": 131_072,
|
|
548
|
+
"supports_vision": True,
|
|
549
|
+
"input_cost_per_token": 0.0000004,
|
|
550
|
+
"output_cost_per_token": 0.000002,
|
|
551
|
+
},
|
|
552
|
+
# Legacy friendly id kept for sessions that logged in before model choice.
|
|
553
|
+
# Hosted Xiaomi MiMo trial: the CLI sends the friendly id "mimo" (the proxy
|
|
554
|
+
# pins it to the real upstream id server-side). Without an entry here the id
|
|
555
|
+
# resolves nowhere and falls back to generic unknown-model metadata, emitting
|
|
556
|
+
# a metadata note on every run and silently shrinking the usable context.
|
|
557
|
+
# Values mirror the bundled `openrouter/xiaomi/mimo-v2-flash` catalog entry
|
|
558
|
+
# (262144 input / 16384 output); costs use the mimo-v2.5-pro list price.
|
|
559
|
+
"mimo": {
|
|
560
|
+
"context_window_tokens": 262_144,
|
|
561
|
+
"max_output_tokens": 16_384,
|
|
562
|
+
"supports_vision": False,
|
|
563
|
+
"input_cost_per_token": 0.000000435,
|
|
564
|
+
"output_cost_per_token": 0.00000087,
|
|
565
|
+
},
|
|
566
|
+
# Kimi Code membership ids served by api.kimi.com/coding/v1 (subscription
|
|
567
|
+
# billing, so no per-token costs). Context windows from
|
|
568
|
+
# kimi.com/code/docs, July 2026.
|
|
569
|
+
"k3": {
|
|
570
|
+
"context_window_tokens": 1_048_576,
|
|
571
|
+
"max_output_tokens": 1_048_576,
|
|
572
|
+
"supports_vision": True,
|
|
573
|
+
"supports_reasoning": True,
|
|
574
|
+
},
|
|
575
|
+
"kimi-for-coding": {
|
|
576
|
+
"context_window_tokens": 262_144,
|
|
577
|
+
"max_output_tokens": 262_144,
|
|
578
|
+
"supports_vision": True,
|
|
579
|
+
"supports_reasoning": True,
|
|
580
|
+
},
|
|
581
|
+
"kimi-for-coding-highspeed": {
|
|
582
|
+
"context_window_tokens": 262_144,
|
|
583
|
+
"max_output_tokens": 262_144,
|
|
584
|
+
"supports_vision": True,
|
|
585
|
+
"supports_reasoning": True,
|
|
586
|
+
},
|
|
587
|
+
# Moonshot Kimi models newer than the bundled LiteLLM snapshot (which tops
|
|
588
|
+
# out at moonshot/kimi-k2.6). Values from platform.kimi.ai/docs/pricing,
|
|
589
|
+
# July 2026; remove once a snapshot refresh covers these ids.
|
|
590
|
+
"kimi-k3": {
|
|
591
|
+
"context_window_tokens": 1_048_576,
|
|
592
|
+
"max_output_tokens": 1_048_576,
|
|
593
|
+
"supports_vision": True,
|
|
594
|
+
"supports_reasoning": True,
|
|
595
|
+
"input_cost_per_token": 0.000003,
|
|
596
|
+
"output_cost_per_token": 0.000015,
|
|
597
|
+
"cache_read_input_cost_per_token": 0.0000003,
|
|
598
|
+
},
|
|
599
|
+
"kimi-k2.7-code": {
|
|
600
|
+
"context_window_tokens": 262_144,
|
|
601
|
+
"max_output_tokens": 262_144,
|
|
602
|
+
"supports_vision": True,
|
|
603
|
+
"supports_reasoning": True,
|
|
604
|
+
"input_cost_per_token": 0.00000095,
|
|
605
|
+
"output_cost_per_token": 0.000004,
|
|
606
|
+
"cache_read_input_cost_per_token": 0.00000019,
|
|
607
|
+
},
|
|
608
|
+
"kimi-k2.7-code-highspeed": {
|
|
609
|
+
"context_window_tokens": 262_144,
|
|
610
|
+
"max_output_tokens": 262_144,
|
|
611
|
+
"supports_vision": True,
|
|
612
|
+
"supports_reasoning": True,
|
|
613
|
+
"input_cost_per_token": 0.0000019,
|
|
614
|
+
"output_cost_per_token": 0.000008,
|
|
615
|
+
"cache_read_input_cost_per_token": 0.00000038,
|
|
616
|
+
},
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
@dataclass(frozen=True)
|
|
621
|
+
class ModelMeta:
|
|
622
|
+
model_name: str
|
|
623
|
+
context_window_tokens: int
|
|
624
|
+
max_output_tokens: int
|
|
625
|
+
input_cost_per_token: float | None = None
|
|
626
|
+
output_cost_per_token: float | None = None
|
|
627
|
+
cache_read_input_cost_per_token: float | None = None
|
|
628
|
+
cache_creation_input_cost_per_token: float | None = None
|
|
629
|
+
cache_creation_5m_input_cost_per_token: float | None = None
|
|
630
|
+
cache_creation_1h_input_cost_per_token: float | None = None
|
|
631
|
+
reasoning_output_cost_per_token: float | None = None
|
|
632
|
+
raw_metadata: dict[str, Any] = field(default_factory=dict)
|
|
633
|
+
source: str = "fallback"
|
|
634
|
+
supports_vision: bool = False
|
|
635
|
+
supports_reasoning: bool | None = None
|
|
636
|
+
field_sources: dict[str, str] = field(default_factory=dict)
|
|
637
|
+
warnings: tuple[str, ...] = ()
|
|
638
|
+
provider_key: str | None = None
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
@dataclass
|
|
642
|
+
class _LayerData:
|
|
643
|
+
name: str
|
|
644
|
+
values: dict[str, Any] = field(default_factory=dict)
|
|
645
|
+
field_sources: dict[str, str] = field(default_factory=dict)
|
|
646
|
+
warnings: list[str] = field(default_factory=list)
|
|
647
|
+
error: str | None = None
|
|
648
|
+
raw_metadata: dict[str, Any] = field(default_factory=dict)
|
|
649
|
+
model_name: str | None = None
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _parse_field(field: str, raw_value: Any) -> Any | None:
|
|
653
|
+
if field in _INT_FIELDS:
|
|
654
|
+
return parse_positive_int(raw_value)
|
|
655
|
+
if field in _FLOAT_FIELDS:
|
|
656
|
+
return parse_non_negative_float(raw_value)
|
|
657
|
+
if field in _BOOL_FIELDS:
|
|
658
|
+
return parse_bool(raw_value)
|
|
659
|
+
return None
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def _dedupe_warnings(values: list[str]) -> list[str]:
|
|
663
|
+
deduped: list[str] = []
|
|
664
|
+
seen: set[str] = set()
|
|
665
|
+
for value in values:
|
|
666
|
+
clean = value.strip()
|
|
667
|
+
if not clean:
|
|
668
|
+
continue
|
|
669
|
+
if clean in seen:
|
|
670
|
+
continue
|
|
671
|
+
seen.add(clean)
|
|
672
|
+
deduped.append(clean)
|
|
673
|
+
return deduped
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def _build_model_alias_index(mapping: dict[str, Any]) -> dict[str, str]:
|
|
677
|
+
index: dict[str, str] = {}
|
|
678
|
+
for key in mapping:
|
|
679
|
+
if not isinstance(key, str):
|
|
680
|
+
continue
|
|
681
|
+
for variant in model_name_variants(key):
|
|
682
|
+
index.setdefault(variant.casefold(), key)
|
|
683
|
+
return index
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def _lookup_model_override(
|
|
687
|
+
*,
|
|
688
|
+
mapping: Any,
|
|
689
|
+
requested_model: str,
|
|
690
|
+
label: str,
|
|
691
|
+
warnings: list[str],
|
|
692
|
+
) -> tuple[str, dict[str, Any]] | None:
|
|
693
|
+
if mapping is None:
|
|
694
|
+
return None
|
|
695
|
+
if not isinstance(mapping, dict):
|
|
696
|
+
warnings.append(f"Ignoring invalid {label}: expected object.")
|
|
697
|
+
return None
|
|
698
|
+
|
|
699
|
+
alias_index = _build_model_alias_index(mapping)
|
|
700
|
+
for variant in model_name_variants(requested_model):
|
|
701
|
+
key = alias_index.get(variant.casefold())
|
|
702
|
+
if key is None:
|
|
703
|
+
continue
|
|
704
|
+
raw = mapping.get(key)
|
|
705
|
+
if isinstance(raw, dict):
|
|
706
|
+
return key, raw
|
|
707
|
+
warnings.append(f"Ignoring invalid {label}[{key!r}]: expected object.")
|
|
708
|
+
return None
|
|
709
|
+
return None
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _lookup_endpoint_override(
|
|
713
|
+
*,
|
|
714
|
+
endpoints: Any,
|
|
715
|
+
base_url: str,
|
|
716
|
+
warnings: list[str],
|
|
717
|
+
) -> tuple[str, dict[str, Any]] | None:
|
|
718
|
+
if endpoints is None:
|
|
719
|
+
return None
|
|
720
|
+
if not isinstance(endpoints, dict):
|
|
721
|
+
warnings.append("Ignoring invalid model_metadata_overrides.endpoints: expected object.")
|
|
722
|
+
return None
|
|
723
|
+
|
|
724
|
+
if base_url in endpoints:
|
|
725
|
+
exact = endpoints.get(base_url)
|
|
726
|
+
if isinstance(exact, dict):
|
|
727
|
+
return base_url, exact
|
|
728
|
+
warnings.append(
|
|
729
|
+
f"Ignoring invalid model_metadata_overrides.endpoints[{base_url!r}]: expected object."
|
|
730
|
+
)
|
|
731
|
+
return None
|
|
732
|
+
|
|
733
|
+
normalized_base_url = normalize_base_url(base_url)
|
|
734
|
+
if not normalized_base_url:
|
|
735
|
+
return None
|
|
736
|
+
for endpoint_key, endpoint_value in endpoints.items():
|
|
737
|
+
if not isinstance(endpoint_key, str):
|
|
738
|
+
continue
|
|
739
|
+
if normalize_base_url(endpoint_key) != normalized_base_url:
|
|
740
|
+
continue
|
|
741
|
+
if isinstance(endpoint_value, dict):
|
|
742
|
+
return endpoint_key, endpoint_value
|
|
743
|
+
warnings.append(
|
|
744
|
+
"Ignoring invalid model_metadata_overrides.endpoints"
|
|
745
|
+
f"[{endpoint_key!r}]: expected object."
|
|
746
|
+
)
|
|
747
|
+
return None
|
|
748
|
+
return None
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
class ModelRegistry:
|
|
752
|
+
def __init__(self, *, cfg: AppConfig, api_key: str | None = None) -> None:
|
|
753
|
+
_ = api_key
|
|
754
|
+
self._cfg = cfg
|
|
755
|
+
self.last_error: str | None = None
|
|
756
|
+
self.last_warnings: list[str] = []
|
|
757
|
+
self.last_source: str | None = None
|
|
758
|
+
|
|
759
|
+
def _provider_route_identity(self) -> tuple[str | None, str]:
|
|
760
|
+
"""Resolve explicit preset route identity without trusting model aliases."""
|
|
761
|
+
|
|
762
|
+
try:
|
|
763
|
+
from .profile_presets import find_preset_for_profile
|
|
764
|
+
from .profiles import get_active_profile
|
|
765
|
+
|
|
766
|
+
profile = get_active_profile(self._cfg)
|
|
767
|
+
preset = find_preset_for_profile(profile)
|
|
768
|
+
except Exception: # noqa: BLE001 - metadata fallback must remain offline-safe
|
|
769
|
+
return None, str(self._cfg.base_url or "").strip()
|
|
770
|
+
provider_key = str(preset.provider_key or "").strip() if preset is not None else ""
|
|
771
|
+
base_url = str(profile.base_url or self._cfg.base_url or "").strip()
|
|
772
|
+
return provider_key or None, base_url
|
|
773
|
+
|
|
774
|
+
def _provider_hint(self) -> str | None:
|
|
775
|
+
provider_key, _base_url = self._provider_route_identity()
|
|
776
|
+
return provider_key
|
|
777
|
+
|
|
778
|
+
def _resolve_env_layer(self) -> _LayerData:
|
|
779
|
+
layer = _LayerData(name="env")
|
|
780
|
+
for field_name, env_name in _ENV_FIELD_MAP.items():
|
|
781
|
+
raw_value = env_get(env_name)
|
|
782
|
+
if raw_value is None:
|
|
783
|
+
continue
|
|
784
|
+
parsed = _parse_field(field_name, raw_value)
|
|
785
|
+
if parsed is None:
|
|
786
|
+
layer.warnings.append(f"Ignoring invalid {env_name}: {raw_value!r}")
|
|
787
|
+
continue
|
|
788
|
+
layer.values[field_name] = parsed
|
|
789
|
+
layer.field_sources[field_name] = f"env:{env_name}"
|
|
790
|
+
return layer
|
|
791
|
+
|
|
792
|
+
def _apply_user_scope(
|
|
793
|
+
self,
|
|
794
|
+
*,
|
|
795
|
+
layer: _LayerData,
|
|
796
|
+
source: str,
|
|
797
|
+
payload: dict[str, Any],
|
|
798
|
+
) -> None:
|
|
799
|
+
for field_name in _TRACKED_FIELDS:
|
|
800
|
+
if field_name in layer.values:
|
|
801
|
+
continue
|
|
802
|
+
if field_name not in payload:
|
|
803
|
+
continue
|
|
804
|
+
raw_value = payload.get(field_name)
|
|
805
|
+
if raw_value is None:
|
|
806
|
+
continue
|
|
807
|
+
parsed = _parse_field(field_name, raw_value)
|
|
808
|
+
if parsed is None:
|
|
809
|
+
layer.warnings.append(f"Ignoring invalid {field_name} in {source}: {raw_value!r}")
|
|
810
|
+
continue
|
|
811
|
+
layer.values[field_name] = parsed
|
|
812
|
+
layer.field_sources[field_name] = source
|
|
813
|
+
|
|
814
|
+
def _resolve_user_layer(self, requested_model: str) -> _LayerData:
|
|
815
|
+
layer = _LayerData(name="user")
|
|
816
|
+
raw_overrides = self._cfg.extra_fields.get("model_metadata_overrides")
|
|
817
|
+
if raw_overrides is None:
|
|
818
|
+
return layer
|
|
819
|
+
if not isinstance(raw_overrides, dict):
|
|
820
|
+
layer.warnings.append("Ignoring invalid model_metadata_overrides: expected object.")
|
|
821
|
+
return layer
|
|
822
|
+
|
|
823
|
+
scoped_payloads: list[tuple[str, dict[str, Any]]] = []
|
|
824
|
+
endpoint_match = _lookup_endpoint_override(
|
|
825
|
+
endpoints=raw_overrides.get("endpoints"),
|
|
826
|
+
base_url=self._cfg.base_url,
|
|
827
|
+
warnings=layer.warnings,
|
|
828
|
+
)
|
|
829
|
+
if endpoint_match is not None:
|
|
830
|
+
endpoint_key, endpoint_payload = endpoint_match
|
|
831
|
+
endpoint_model_match = _lookup_model_override(
|
|
832
|
+
mapping=endpoint_payload.get("models"),
|
|
833
|
+
requested_model=requested_model,
|
|
834
|
+
label=f"model_metadata_overrides.endpoints[{endpoint_key!r}].models",
|
|
835
|
+
warnings=layer.warnings,
|
|
836
|
+
)
|
|
837
|
+
if endpoint_model_match is not None:
|
|
838
|
+
model_key, model_payload = endpoint_model_match
|
|
839
|
+
scoped_payloads.append(
|
|
840
|
+
(
|
|
841
|
+
f"user:endpoints[{endpoint_key!r}].models[{model_key!r}]",
|
|
842
|
+
model_payload,
|
|
843
|
+
)
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
endpoint_default = endpoint_payload.get("default")
|
|
847
|
+
if endpoint_default is not None:
|
|
848
|
+
if isinstance(endpoint_default, dict):
|
|
849
|
+
scoped_payloads.append(
|
|
850
|
+
(f"user:endpoints[{endpoint_key!r}].default", endpoint_default)
|
|
851
|
+
)
|
|
852
|
+
else:
|
|
853
|
+
layer.warnings.append(
|
|
854
|
+
"Ignoring invalid model_metadata_overrides.endpoints"
|
|
855
|
+
f"[{endpoint_key!r}].default: expected object."
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
model_match = _lookup_model_override(
|
|
859
|
+
mapping=raw_overrides.get("models"),
|
|
860
|
+
requested_model=requested_model,
|
|
861
|
+
label="model_metadata_overrides.models",
|
|
862
|
+
warnings=layer.warnings,
|
|
863
|
+
)
|
|
864
|
+
if model_match is not None:
|
|
865
|
+
model_key, model_payload = model_match
|
|
866
|
+
scoped_payloads.append((f"user:models[{model_key!r}]", model_payload))
|
|
867
|
+
|
|
868
|
+
default_payload = raw_overrides.get("default")
|
|
869
|
+
if default_payload is not None:
|
|
870
|
+
if isinstance(default_payload, dict):
|
|
871
|
+
scoped_payloads.append(("user:default", default_payload))
|
|
872
|
+
else:
|
|
873
|
+
layer.warnings.append(
|
|
874
|
+
"Ignoring invalid model_metadata_overrides.default: expected object."
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
for source, payload in scoped_payloads:
|
|
878
|
+
self._apply_user_scope(layer=layer, source=source, payload=payload)
|
|
879
|
+
|
|
880
|
+
return layer
|
|
881
|
+
|
|
882
|
+
def _resolve_bundled_model_catalog_layer(self, requested_model: str) -> _LayerData:
|
|
883
|
+
layer = _LayerData(name=BUNDLED_MODEL_CATALOG_SOURCE)
|
|
884
|
+
provider_hint, route_base_url = self._provider_route_identity()
|
|
885
|
+
try:
|
|
886
|
+
static_meta = resolve_litellm_static_metadata(
|
|
887
|
+
requested_model,
|
|
888
|
+
base_url=route_base_url,
|
|
889
|
+
provider_hint=provider_hint,
|
|
890
|
+
)
|
|
891
|
+
except TypeError as exc:
|
|
892
|
+
# Preserve compatibility with lightweight test/plugin resolvers that
|
|
893
|
+
# implement the older ``(model, *, base_url)`` extension surface.
|
|
894
|
+
if "provider_hint" not in str(exc):
|
|
895
|
+
raise
|
|
896
|
+
static_meta = resolve_litellm_static_metadata(
|
|
897
|
+
requested_model,
|
|
898
|
+
base_url=route_base_url,
|
|
899
|
+
)
|
|
900
|
+
layer.error = static_meta.error
|
|
901
|
+
layer.raw_metadata = static_meta.raw_metadata
|
|
902
|
+
layer.model_name = static_meta.model_key
|
|
903
|
+
|
|
904
|
+
values = {
|
|
905
|
+
"context_window_tokens": static_meta.context_window_tokens,
|
|
906
|
+
"max_output_tokens": static_meta.max_output_tokens,
|
|
907
|
+
"supports_vision": static_meta.supports_vision,
|
|
908
|
+
"supports_reasoning": parse_bool(static_meta.raw_metadata.get("supports_reasoning")),
|
|
909
|
+
"input_cost_per_token": static_meta.input_cost_per_token,
|
|
910
|
+
"output_cost_per_token": static_meta.output_cost_per_token,
|
|
911
|
+
"cache_read_input_cost_per_token": static_meta.cache_read_input_cost_per_token,
|
|
912
|
+
"cache_creation_input_cost_per_token": (
|
|
913
|
+
static_meta.cache_creation_input_cost_per_token
|
|
914
|
+
),
|
|
915
|
+
"cache_creation_5m_input_cost_per_token": (
|
|
916
|
+
static_meta.cache_creation_5m_input_cost_per_token
|
|
917
|
+
),
|
|
918
|
+
"cache_creation_1h_input_cost_per_token": (
|
|
919
|
+
static_meta.cache_creation_1h_input_cost_per_token
|
|
920
|
+
),
|
|
921
|
+
"reasoning_output_cost_per_token": static_meta.reasoning_output_cost_per_token,
|
|
922
|
+
}
|
|
923
|
+
for field_name, value in values.items():
|
|
924
|
+
if value is None:
|
|
925
|
+
continue
|
|
926
|
+
layer.values[field_name] = value
|
|
927
|
+
layer.field_sources[field_name] = BUNDLED_MODEL_CATALOG_SOURCE
|
|
928
|
+
return layer
|
|
929
|
+
|
|
930
|
+
def _resolve_provider_auth_layer(self, requested_model: str) -> _LayerData:
|
|
931
|
+
"""Project the active account's live model catalog into context metadata."""
|
|
932
|
+
|
|
933
|
+
layer = _LayerData(name="provider_auth")
|
|
934
|
+
try:
|
|
935
|
+
from .profiles import get_active_profile
|
|
936
|
+
|
|
937
|
+
profile = get_active_profile(self._cfg)
|
|
938
|
+
except Exception: # noqa: BLE001 - malformed profiles fall through to static metadata
|
|
939
|
+
return layer
|
|
940
|
+
provider_id = str(profile.auth_provider or "").strip()
|
|
941
|
+
if not provider_id:
|
|
942
|
+
return layer
|
|
943
|
+
try:
|
|
944
|
+
from .provider_auth import create_provider_auth
|
|
945
|
+
|
|
946
|
+
models = create_provider_auth(provider_id).list_models(refresh=False)
|
|
947
|
+
except Exception as exc: # noqa: BLE001 - offline startup must remain possible
|
|
948
|
+
layer.warnings.append(f"Subscription model metadata unavailable: {exc}")
|
|
949
|
+
models = ()
|
|
950
|
+
|
|
951
|
+
requested_variants = {
|
|
952
|
+
value.casefold() for value in model_name_variants(requested_model) if value
|
|
953
|
+
}
|
|
954
|
+
selected = next(
|
|
955
|
+
(
|
|
956
|
+
model
|
|
957
|
+
for model in models
|
|
958
|
+
if requested_variants.intersection(
|
|
959
|
+
value.casefold() for value in model_name_variants(model.id) if value
|
|
960
|
+
)
|
|
961
|
+
),
|
|
962
|
+
None,
|
|
963
|
+
)
|
|
964
|
+
if selected is None and provider_id != "openai-codex":
|
|
965
|
+
return layer
|
|
966
|
+
|
|
967
|
+
source = f"provider_auth:{provider_id}"
|
|
968
|
+
static_subscription_model = (
|
|
969
|
+
resolve_chatgpt_codex_static_model(requested_model)
|
|
970
|
+
if provider_id == "openai-codex"
|
|
971
|
+
else None
|
|
972
|
+
)
|
|
973
|
+
if selected is not None:
|
|
974
|
+
layer.model_name = selected.id
|
|
975
|
+
elif static_subscription_model is not None:
|
|
976
|
+
layer.model_name = static_subscription_model.id
|
|
977
|
+
source = f"provider_auth:{provider_id}:{CHATGPT_CODEX_SUBSCRIPTION_CATALOG_SOURCE}"
|
|
978
|
+
layer.warnings.append(
|
|
979
|
+
"Using the bundled ChatGPT subscription capacity snapshot because live "
|
|
980
|
+
"metadata for the selected model is unavailable."
|
|
981
|
+
)
|
|
982
|
+
else:
|
|
983
|
+
layer.model_name = requested_model
|
|
984
|
+
source = f"provider_auth:{provider_id}:conservative-default"
|
|
985
|
+
layer.warnings.append(
|
|
986
|
+
"Using conservative ChatGPT subscription capacity because the selected "
|
|
987
|
+
"model is absent from both live and bundled subscription metadata."
|
|
988
|
+
)
|
|
989
|
+
|
|
990
|
+
context_window_tokens = (
|
|
991
|
+
selected.context_window_tokens if selected is not None else None
|
|
992
|
+
) or (
|
|
993
|
+
static_subscription_model.context_window_tokens
|
|
994
|
+
if static_subscription_model is not None
|
|
995
|
+
else None
|
|
996
|
+
)
|
|
997
|
+
max_output_tokens = (selected.max_output_tokens if selected is not None else None) or (
|
|
998
|
+
static_subscription_model.max_output_tokens
|
|
999
|
+
if static_subscription_model is not None
|
|
1000
|
+
else None
|
|
1001
|
+
)
|
|
1002
|
+
input_modalities = (
|
|
1003
|
+
selected.input_modalities
|
|
1004
|
+
if selected is not None
|
|
1005
|
+
else (
|
|
1006
|
+
static_subscription_model.input_modalities
|
|
1007
|
+
if static_subscription_model is not None
|
|
1008
|
+
else ("text",)
|
|
1009
|
+
)
|
|
1010
|
+
)
|
|
1011
|
+
layer.raw_metadata = {
|
|
1012
|
+
"provider_auth": provider_id,
|
|
1013
|
+
"subscription_backed": True,
|
|
1014
|
+
"input_modalities": list(input_modalities),
|
|
1015
|
+
}
|
|
1016
|
+
selected_reasoning_efforts = (
|
|
1017
|
+
selected.reasoning_efforts
|
|
1018
|
+
if selected is not None
|
|
1019
|
+
else (
|
|
1020
|
+
static_subscription_model.reasoning_efforts
|
|
1021
|
+
if static_subscription_model is not None
|
|
1022
|
+
else ()
|
|
1023
|
+
)
|
|
1024
|
+
)
|
|
1025
|
+
selected_default_effort = (
|
|
1026
|
+
selected.default_reasoning_effort
|
|
1027
|
+
if selected is not None
|
|
1028
|
+
else (
|
|
1029
|
+
static_subscription_model.default_reasoning_effort
|
|
1030
|
+
if static_subscription_model is not None
|
|
1031
|
+
else None
|
|
1032
|
+
)
|
|
1033
|
+
)
|
|
1034
|
+
if selected is not None or static_subscription_model is not None:
|
|
1035
|
+
supports_reasoning = bool(selected_reasoning_efforts or selected_default_effort)
|
|
1036
|
+
layer.values["supports_reasoning"] = supports_reasoning
|
|
1037
|
+
layer.field_sources["supports_reasoning"] = source
|
|
1038
|
+
layer.raw_metadata["supports_reasoning"] = supports_reasoning
|
|
1039
|
+
if context_window_tokens is not None:
|
|
1040
|
+
layer.values["context_window_tokens"] = context_window_tokens
|
|
1041
|
+
layer.field_sources["context_window_tokens"] = source
|
|
1042
|
+
layer.raw_metadata["context_window_tokens"] = context_window_tokens
|
|
1043
|
+
# The Codex catalog currently omits a response-output ceiling. Keep
|
|
1044
|
+
# Alysis Code's existing local reserve without adding a wire-level cap.
|
|
1045
|
+
layer.values["max_output_tokens"] = (
|
|
1046
|
+
max_output_tokens or DEFAULT_UNKNOWN_MODEL_MAX_OUTPUT_TOKENS
|
|
1047
|
+
)
|
|
1048
|
+
layer.field_sources["max_output_tokens"] = (
|
|
1049
|
+
source if max_output_tokens is not None else f"{source}:local-default"
|
|
1050
|
+
)
|
|
1051
|
+
layer.raw_metadata["max_output_tokens"] = layer.values["max_output_tokens"]
|
|
1052
|
+
if max_output_tokens is not None and "max_output_tokens" not in layer.values:
|
|
1053
|
+
layer.values["max_output_tokens"] = max_output_tokens
|
|
1054
|
+
layer.field_sources["max_output_tokens"] = source
|
|
1055
|
+
layer.raw_metadata["max_output_tokens"] = max_output_tokens
|
|
1056
|
+
if provider_id == "openai-codex" and "context_window_tokens" not in layer.values:
|
|
1057
|
+
layer.values["context_window_tokens"] = DEFAULT_UNKNOWN_MODEL_CONTEXT_WINDOW_TOKENS
|
|
1058
|
+
layer.field_sources["context_window_tokens"] = source
|
|
1059
|
+
layer.values["max_output_tokens"] = DEFAULT_UNKNOWN_MODEL_MAX_OUTPUT_TOKENS
|
|
1060
|
+
layer.field_sources["max_output_tokens"] = f"{source}:local-default"
|
|
1061
|
+
layer.raw_metadata["context_window_tokens"] = layer.values["context_window_tokens"]
|
|
1062
|
+
layer.raw_metadata["max_output_tokens"] = layer.values["max_output_tokens"]
|
|
1063
|
+
layer.values["supports_vision"] = "image" in input_modalities
|
|
1064
|
+
layer.field_sources["supports_vision"] = source
|
|
1065
|
+
for field_name in _FLOAT_FIELDS:
|
|
1066
|
+
layer.values[field_name] = 0.0
|
|
1067
|
+
layer.field_sources[field_name] = f"{source}:included"
|
|
1068
|
+
return layer
|
|
1069
|
+
|
|
1070
|
+
def _resolve_official_provider_layer(self, requested_model: str) -> _LayerData:
|
|
1071
|
+
layer = _LayerData(name=OFFICIAL_PROVIDER_MODEL_CATALOG_SOURCE)
|
|
1072
|
+
provider_key, route_base_url = self._provider_route_identity()
|
|
1073
|
+
normalized_provider = str(provider_key or "").strip().lower()
|
|
1074
|
+
provider_models = _OFFICIAL_PROVIDER_MODEL_METADATA.get(normalized_provider)
|
|
1075
|
+
if provider_models is None:
|
|
1076
|
+
return layer
|
|
1077
|
+
model_match = _lookup_model_override(
|
|
1078
|
+
mapping=provider_models,
|
|
1079
|
+
requested_model=requested_model,
|
|
1080
|
+
label=f"{OFFICIAL_PROVIDER_MODEL_CATALOG_SOURCE}.{normalized_provider}",
|
|
1081
|
+
warnings=layer.warnings,
|
|
1082
|
+
)
|
|
1083
|
+
if model_match is None:
|
|
1084
|
+
return layer
|
|
1085
|
+
|
|
1086
|
+
model_key, payload = model_match
|
|
1087
|
+
payload = dict(payload)
|
|
1088
|
+
if normalized_provider == "moonshot":
|
|
1089
|
+
try:
|
|
1090
|
+
route_host = (urlsplit(route_base_url).hostname or "").rstrip(".").lower()
|
|
1091
|
+
except ValueError:
|
|
1092
|
+
route_host = ""
|
|
1093
|
+
if route_host != "api.moonshot.ai":
|
|
1094
|
+
# The verified prices are Moonshot's global USD rates. Capacity
|
|
1095
|
+
# and capabilities also apply to China, but those prices do not.
|
|
1096
|
+
for field_name in _FLOAT_FIELDS:
|
|
1097
|
+
payload.pop(field_name, None)
|
|
1098
|
+
source = f"{OFFICIAL_PROVIDER_MODEL_CATALOG_SOURCE}:{normalized_provider}"
|
|
1099
|
+
layer.model_name = model_key
|
|
1100
|
+
layer.raw_metadata = {
|
|
1101
|
+
**payload,
|
|
1102
|
+
"provider": normalized_provider,
|
|
1103
|
+
"catalog_source": OFFICIAL_PROVIDER_MODEL_CATALOG_SOURCE,
|
|
1104
|
+
"catalog_sources": list(
|
|
1105
|
+
_OFFICIAL_PROVIDER_MODEL_SOURCES.get(normalized_provider, {}).get(model_key, ())
|
|
1106
|
+
),
|
|
1107
|
+
}
|
|
1108
|
+
self._apply_user_scope(layer=layer, source=source, payload=payload)
|
|
1109
|
+
return layer
|
|
1110
|
+
|
|
1111
|
+
def _resolve_canonical_model_layer(self, requested_model: str) -> _LayerData:
|
|
1112
|
+
layer = _LayerData(name=CANONICAL_MODEL_CATALOG_SOURCE)
|
|
1113
|
+
provider_key, _route_base_url = self._provider_route_identity()
|
|
1114
|
+
normalized_provider = str(provider_key or "").strip().lower()
|
|
1115
|
+
|
|
1116
|
+
canonical_id: str | None = None
|
|
1117
|
+
provider_aliases = _PROVIDER_CANONICAL_MODEL_IDS.get(normalized_provider, {})
|
|
1118
|
+
provider_alias_index = _build_model_alias_index(provider_aliases)
|
|
1119
|
+
for variant in model_name_variants(requested_model):
|
|
1120
|
+
route_id = provider_alias_index.get(variant.casefold())
|
|
1121
|
+
if route_id is not None:
|
|
1122
|
+
canonical_id = provider_aliases.get(route_id)
|
|
1123
|
+
break
|
|
1124
|
+
|
|
1125
|
+
if canonical_id is None:
|
|
1126
|
+
canonical_alias_index = _build_model_alias_index(_CANONICAL_MODEL_METADATA)
|
|
1127
|
+
for variant in model_name_variants(requested_model):
|
|
1128
|
+
canonical_id = canonical_alias_index.get(variant.casefold())
|
|
1129
|
+
if canonical_id is not None:
|
|
1130
|
+
break
|
|
1131
|
+
if canonical_id is None:
|
|
1132
|
+
return layer
|
|
1133
|
+
|
|
1134
|
+
payload = _CANONICAL_MODEL_METADATA.get(canonical_id)
|
|
1135
|
+
if payload is None:
|
|
1136
|
+
return layer
|
|
1137
|
+
source = f"{CANONICAL_MODEL_CATALOG_SOURCE}:{canonical_id}"
|
|
1138
|
+
layer.raw_metadata = {
|
|
1139
|
+
**payload,
|
|
1140
|
+
"canonical_model": canonical_id,
|
|
1141
|
+
"canonical_catalog_sources": list(_CANONICAL_MODEL_SOURCES.get(canonical_id, ())),
|
|
1142
|
+
}
|
|
1143
|
+
self._apply_user_scope(layer=layer, source=source, payload=payload)
|
|
1144
|
+
return layer
|
|
1145
|
+
|
|
1146
|
+
def _resolve_builtin_layer(self, requested_model: str) -> _LayerData:
|
|
1147
|
+
layer = _LayerData(name="built_in")
|
|
1148
|
+
model_match = _lookup_model_override(
|
|
1149
|
+
mapping=_BUILT_IN_MODEL_METADATA,
|
|
1150
|
+
requested_model=requested_model,
|
|
1151
|
+
label="built_in_model_catalog",
|
|
1152
|
+
warnings=layer.warnings,
|
|
1153
|
+
)
|
|
1154
|
+
if model_match is None:
|
|
1155
|
+
return layer
|
|
1156
|
+
|
|
1157
|
+
model_key, payload = model_match
|
|
1158
|
+
layer.model_name = model_key
|
|
1159
|
+
layer.raw_metadata = dict(payload)
|
|
1160
|
+
self._apply_user_scope(layer=layer, source="built_in", payload=payload)
|
|
1161
|
+
return layer
|
|
1162
|
+
|
|
1163
|
+
def _resolve_learned_layer(self, requested_model: str) -> _LayerData:
|
|
1164
|
+
_ = requested_model
|
|
1165
|
+
# Placeholder layer for deterministic precedence; learned cache is not implemented yet.
|
|
1166
|
+
return _LayerData(name="learned")
|
|
1167
|
+
|
|
1168
|
+
def _resolve_field_value(self, field_name: str, layers: list[_LayerData]) -> tuple[Any, str]:
|
|
1169
|
+
for layer in layers:
|
|
1170
|
+
if field_name not in layer.values:
|
|
1171
|
+
continue
|
|
1172
|
+
value = layer.values[field_name]
|
|
1173
|
+
if value is None:
|
|
1174
|
+
continue
|
|
1175
|
+
return value, layer.field_sources.get(field_name, layer.name)
|
|
1176
|
+
return _FALLBACKS[field_name], "fallback"
|
|
1177
|
+
|
|
1178
|
+
def get(self, model_name: str, *, include_provider_auth: bool = True) -> ModelMeta:
|
|
1179
|
+
requested = model_name.strip() or self._cfg.model.strip() or "unknown-model"
|
|
1180
|
+
warnings: list[str] = []
|
|
1181
|
+
if "model_capabilities" in self._cfg.extra_fields:
|
|
1182
|
+
warnings.append(_DEPRECATED_MODEL_CAPABILITIES_WARNING)
|
|
1183
|
+
|
|
1184
|
+
env_layer = self._resolve_env_layer()
|
|
1185
|
+
user_layer = self._resolve_user_layer(requested)
|
|
1186
|
+
provider_auth_layer = (
|
|
1187
|
+
self._resolve_provider_auth_layer(requested)
|
|
1188
|
+
if include_provider_auth
|
|
1189
|
+
else _LayerData(name="provider_auth")
|
|
1190
|
+
)
|
|
1191
|
+
official_provider_layer = self._resolve_official_provider_layer(requested)
|
|
1192
|
+
bundled_catalog_layer = self._resolve_bundled_model_catalog_layer(requested)
|
|
1193
|
+
canonical_model_layer = self._resolve_canonical_model_layer(requested)
|
|
1194
|
+
route_provider, route_base_url = self._provider_route_identity()
|
|
1195
|
+
active_provider = (
|
|
1196
|
+
str(route_provider or known_provider_key_from_base_url(route_base_url) or "")
|
|
1197
|
+
.strip()
|
|
1198
|
+
.lower()
|
|
1199
|
+
)
|
|
1200
|
+
bundled_provider = (
|
|
1201
|
+
str(bundled_catalog_layer.raw_metadata.get("litellm_provider") or "").strip().lower()
|
|
1202
|
+
)
|
|
1203
|
+
catalog_route_provider = (
|
|
1204
|
+
str(bundled_catalog_layer.raw_metadata.get("catalog_provider_hint") or "")
|
|
1205
|
+
.strip()
|
|
1206
|
+
.lower()
|
|
1207
|
+
or bundled_provider
|
|
1208
|
+
)
|
|
1209
|
+
if catalog_route_provider and catalog_route_provider != active_provider:
|
|
1210
|
+
# Catalog prices belong to the route selected by the catalog lookup.
|
|
1211
|
+
# A model-name match may still supply portable capability data, but
|
|
1212
|
+
# it cannot establish prices for an unrelated compatible endpoint.
|
|
1213
|
+
for field_name in _FLOAT_FIELDS:
|
|
1214
|
+
bundled_catalog_layer.values.pop(field_name, None)
|
|
1215
|
+
bundled_catalog_layer.field_sources.pop(field_name, None)
|
|
1216
|
+
bundled_catalog_layer.raw_metadata = {}
|
|
1217
|
+
built_in_layer = self._resolve_builtin_layer(requested)
|
|
1218
|
+
learned_layer = self._resolve_learned_layer(requested)
|
|
1219
|
+
layers = [
|
|
1220
|
+
env_layer,
|
|
1221
|
+
user_layer,
|
|
1222
|
+
provider_auth_layer,
|
|
1223
|
+
official_provider_layer,
|
|
1224
|
+
canonical_model_layer,
|
|
1225
|
+
bundled_catalog_layer,
|
|
1226
|
+
built_in_layer,
|
|
1227
|
+
learned_layer,
|
|
1228
|
+
]
|
|
1229
|
+
|
|
1230
|
+
for layer in layers:
|
|
1231
|
+
warnings.extend(layer.warnings)
|
|
1232
|
+
|
|
1233
|
+
field_sources: dict[str, str] = {}
|
|
1234
|
+
resolved_fields: dict[str, Any] = {}
|
|
1235
|
+
for field_name in _TRACKED_FIELDS:
|
|
1236
|
+
value, source = self._resolve_field_value(field_name, layers)
|
|
1237
|
+
resolved_fields[field_name] = value
|
|
1238
|
+
field_sources[field_name] = source
|
|
1239
|
+
|
|
1240
|
+
context_window_tokens = (
|
|
1241
|
+
parse_positive_int(resolved_fields["context_window_tokens"])
|
|
1242
|
+
or _FALLBACKS["context_window_tokens"]
|
|
1243
|
+
)
|
|
1244
|
+
max_output_tokens = (
|
|
1245
|
+
parse_positive_int(resolved_fields["max_output_tokens"])
|
|
1246
|
+
or _FALLBACKS["max_output_tokens"]
|
|
1247
|
+
)
|
|
1248
|
+
supports_vision = (
|
|
1249
|
+
parse_bool(resolved_fields["supports_vision"])
|
|
1250
|
+
if resolved_fields["supports_vision"] is not None
|
|
1251
|
+
else _FALLBACKS["supports_vision"]
|
|
1252
|
+
)
|
|
1253
|
+
if supports_vision is None:
|
|
1254
|
+
supports_vision = _FALLBACKS["supports_vision"]
|
|
1255
|
+
supports_reasoning = (
|
|
1256
|
+
parse_bool(resolved_fields["supports_reasoning"])
|
|
1257
|
+
if resolved_fields["supports_reasoning"] is not None
|
|
1258
|
+
else None
|
|
1259
|
+
)
|
|
1260
|
+
input_cost_per_token = (
|
|
1261
|
+
parse_non_negative_float(resolved_fields["input_cost_per_token"])
|
|
1262
|
+
if resolved_fields["input_cost_per_token"] is not None
|
|
1263
|
+
else None
|
|
1264
|
+
)
|
|
1265
|
+
output_cost_per_token = (
|
|
1266
|
+
parse_non_negative_float(resolved_fields["output_cost_per_token"])
|
|
1267
|
+
if resolved_fields["output_cost_per_token"] is not None
|
|
1268
|
+
else None
|
|
1269
|
+
)
|
|
1270
|
+
cache_read_input_cost_per_token = (
|
|
1271
|
+
parse_non_negative_float(resolved_fields["cache_read_input_cost_per_token"])
|
|
1272
|
+
if resolved_fields["cache_read_input_cost_per_token"] is not None
|
|
1273
|
+
else None
|
|
1274
|
+
)
|
|
1275
|
+
cache_creation_input_cost_per_token = (
|
|
1276
|
+
parse_non_negative_float(resolved_fields["cache_creation_input_cost_per_token"])
|
|
1277
|
+
if resolved_fields["cache_creation_input_cost_per_token"] is not None
|
|
1278
|
+
else None
|
|
1279
|
+
)
|
|
1280
|
+
cache_creation_5m_input_cost_per_token = (
|
|
1281
|
+
parse_non_negative_float(resolved_fields["cache_creation_5m_input_cost_per_token"])
|
|
1282
|
+
if resolved_fields["cache_creation_5m_input_cost_per_token"] is not None
|
|
1283
|
+
else None
|
|
1284
|
+
)
|
|
1285
|
+
cache_creation_1h_input_cost_per_token = (
|
|
1286
|
+
parse_non_negative_float(resolved_fields["cache_creation_1h_input_cost_per_token"])
|
|
1287
|
+
if resolved_fields["cache_creation_1h_input_cost_per_token"] is not None
|
|
1288
|
+
else None
|
|
1289
|
+
)
|
|
1290
|
+
reasoning_output_cost_per_token = (
|
|
1291
|
+
parse_non_negative_float(resolved_fields["reasoning_output_cost_per_token"])
|
|
1292
|
+
if resolved_fields["reasoning_output_cost_per_token"] is not None
|
|
1293
|
+
else None
|
|
1294
|
+
)
|
|
1295
|
+
|
|
1296
|
+
if max_output_tokens >= context_window_tokens:
|
|
1297
|
+
# Shared-window metadata (e.g. the Kimi Code ids publish max_tokens
|
|
1298
|
+
# up to the full context). Clamping to window-1 — the old behaviour —
|
|
1299
|
+
# left a 1-token input budget, which surfaced as "context: 0% left"
|
|
1300
|
+
# on a fresh 1M-context session. Reserve a conservative response
|
|
1301
|
+
# allowance instead so the input budget keeps most of the window.
|
|
1302
|
+
clamped = max(1, min(max_output_tokens, max(4096, context_window_tokens // 8)))
|
|
1303
|
+
warnings.append(
|
|
1304
|
+
"max_output_tokens >= context_window_tokens (shared window); "
|
|
1305
|
+
f"reserving {clamped} tokens for output."
|
|
1306
|
+
)
|
|
1307
|
+
max_output_tokens = clamped
|
|
1308
|
+
|
|
1309
|
+
key_fallback = (
|
|
1310
|
+
field_sources.get("context_window_tokens") == "fallback"
|
|
1311
|
+
or field_sources.get("max_output_tokens") == "fallback"
|
|
1312
|
+
)
|
|
1313
|
+
if key_fallback:
|
|
1314
|
+
warnings.append(_FALLBACK_WARNING)
|
|
1315
|
+
|
|
1316
|
+
all_sources = {field_sources.get(field_name, "fallback") for field_name in _TRACKED_FIELDS}
|
|
1317
|
+
overall_source = next(iter(all_sources)) if len(all_sources) == 1 else "mixed"
|
|
1318
|
+
|
|
1319
|
+
resolved_model_name = next(
|
|
1320
|
+
(
|
|
1321
|
+
layer.model_name
|
|
1322
|
+
for layer in layers
|
|
1323
|
+
if layer.values and isinstance(layer.model_name, str) and layer.model_name.strip()
|
|
1324
|
+
),
|
|
1325
|
+
requested,
|
|
1326
|
+
)
|
|
1327
|
+
|
|
1328
|
+
final_warnings = tuple(_dedupe_warnings(warnings))
|
|
1329
|
+
self.last_error = bundled_catalog_layer.error if key_fallback else None
|
|
1330
|
+
self.last_warnings = list(final_warnings)
|
|
1331
|
+
self.last_source = overall_source
|
|
1332
|
+
|
|
1333
|
+
raw_metadata = dict(bundled_catalog_layer.raw_metadata)
|
|
1334
|
+
raw_metadata.update(canonical_model_layer.raw_metadata)
|
|
1335
|
+
raw_metadata.update(official_provider_layer.raw_metadata)
|
|
1336
|
+
raw_metadata.update(provider_auth_layer.raw_metadata)
|
|
1337
|
+
return ModelMeta(
|
|
1338
|
+
model_name=resolved_model_name,
|
|
1339
|
+
context_window_tokens=context_window_tokens,
|
|
1340
|
+
max_output_tokens=max_output_tokens,
|
|
1341
|
+
input_cost_per_token=input_cost_per_token,
|
|
1342
|
+
output_cost_per_token=output_cost_per_token,
|
|
1343
|
+
cache_read_input_cost_per_token=cache_read_input_cost_per_token,
|
|
1344
|
+
cache_creation_input_cost_per_token=cache_creation_input_cost_per_token,
|
|
1345
|
+
cache_creation_5m_input_cost_per_token=cache_creation_5m_input_cost_per_token,
|
|
1346
|
+
cache_creation_1h_input_cost_per_token=cache_creation_1h_input_cost_per_token,
|
|
1347
|
+
reasoning_output_cost_per_token=reasoning_output_cost_per_token,
|
|
1348
|
+
raw_metadata=raw_metadata,
|
|
1349
|
+
source=overall_source,
|
|
1350
|
+
supports_vision=bool(supports_vision),
|
|
1351
|
+
supports_reasoning=supports_reasoning,
|
|
1352
|
+
field_sources=field_sources,
|
|
1353
|
+
warnings=final_warnings,
|
|
1354
|
+
provider_key=self._provider_hint(),
|
|
1355
|
+
)
|
|
1356
|
+
|
|
1357
|
+
|
|
1358
|
+
def resolve_model_provider_key(
|
|
1359
|
+
*,
|
|
1360
|
+
cfg: AppConfig,
|
|
1361
|
+
model_name: str,
|
|
1362
|
+
base_url: str | None = None,
|
|
1363
|
+
profile_name: str | None = None,
|
|
1364
|
+
) -> str | None:
|
|
1365
|
+
requested_model = str(model_name or "").strip()
|
|
1366
|
+
resolved_base_url = base_url or getattr(cfg, "base_url", None)
|
|
1367
|
+
try:
|
|
1368
|
+
from .profile_presets import find_preset_for_base_url
|
|
1369
|
+
|
|
1370
|
+
preset = find_preset_for_base_url(str(resolved_base_url or ""))
|
|
1371
|
+
except Exception: # noqa: BLE001 - provider inference remains best effort
|
|
1372
|
+
preset = None
|
|
1373
|
+
preset_provider = str(preset.provider_key or "").strip() if preset is not None else ""
|
|
1374
|
+
if preset_provider:
|
|
1375
|
+
return preset_provider
|
|
1376
|
+
|
|
1377
|
+
url_provider = _provider_key_from_base_url(resolved_base_url)
|
|
1378
|
+
if url_provider == "openrouter":
|
|
1379
|
+
return url_provider
|
|
1380
|
+
|
|
1381
|
+
if requested_model:
|
|
1382
|
+
meta = ModelRegistry(cfg=cfg).get(requested_model)
|
|
1383
|
+
provider = str(meta.raw_metadata.get("litellm_provider") or "").strip()
|
|
1384
|
+
if provider:
|
|
1385
|
+
return provider
|
|
1386
|
+
resolved_model = str(meta.model_name or "").strip()
|
|
1387
|
+
if "/" in resolved_model:
|
|
1388
|
+
return resolved_model.split("/", 1)[0].strip() or None
|
|
1389
|
+
|
|
1390
|
+
if url_provider:
|
|
1391
|
+
return url_provider
|
|
1392
|
+
|
|
1393
|
+
profile_provider = str(profile_name or "").strip()
|
|
1394
|
+
if profile_provider and profile_provider.lower() != "default":
|
|
1395
|
+
return profile_provider
|
|
1396
|
+
|
|
1397
|
+
if "/" in requested_model:
|
|
1398
|
+
return requested_model.split("/", 1)[0].strip() or None
|
|
1399
|
+
return requested_model or None
|
|
1400
|
+
|
|
1401
|
+
|
|
1402
|
+
def _provider_key_from_base_url(base_url: str | None) -> str | None:
|
|
1403
|
+
raw = str(base_url or "").strip()
|
|
1404
|
+
if not raw:
|
|
1405
|
+
return None
|
|
1406
|
+
try:
|
|
1407
|
+
hostname = (urlsplit(raw).hostname or "").rstrip(".").lower()
|
|
1408
|
+
except ValueError:
|
|
1409
|
+
return None
|
|
1410
|
+
if not hostname:
|
|
1411
|
+
return None
|
|
1412
|
+
known_provider = known_provider_key_from_base_url(raw)
|
|
1413
|
+
if known_provider:
|
|
1414
|
+
return known_provider
|
|
1415
|
+
parts = [
|
|
1416
|
+
part
|
|
1417
|
+
for part in hostname.split(".")
|
|
1418
|
+
if part and part not in {"api", "ai", "www", "com", "v1", "v1beta"}
|
|
1419
|
+
]
|
|
1420
|
+
return parts[-1] if parts else hostname
|