devsper 2.1.6__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.
- devsper/__init__.py +14 -0
- devsper/agents/a2a/__init__.py +27 -0
- devsper/agents/a2a/client.py +126 -0
- devsper/agents/a2a/discovery.py +24 -0
- devsper/agents/a2a/server.py +128 -0
- devsper/agents/a2a/tool_adapter.py +68 -0
- devsper/agents/a2a/types.py +49 -0
- devsper/agents/agent.py +602 -0
- devsper/agents/critic.py +80 -0
- devsper/agents/message_bus.py +124 -0
- devsper/agents/roles.py +181 -0
- devsper/agents/run_agent.py +78 -0
- devsper/analytics/__init__.py +5 -0
- devsper/analytics/tool_analytics.py +78 -0
- devsper/audit/__init__.py +5 -0
- devsper/audit/logger.py +214 -0
- devsper/bus/__init__.py +29 -0
- devsper/bus/backends/__init__.py +5 -0
- devsper/bus/backends/base.py +38 -0
- devsper/bus/backends/memory.py +55 -0
- devsper/bus/backends/redis.py +146 -0
- devsper/bus/message.py +56 -0
- devsper/bus/schema_version.py +3 -0
- devsper/bus/topics.py +19 -0
- devsper/cache/__init__.py +6 -0
- devsper/cache/embedding_index.py +98 -0
- devsper/cache/hashing.py +24 -0
- devsper/cache/store.py +153 -0
- devsper/cache/task_cache.py +191 -0
- devsper/cli/__init__.py +6 -0
- devsper/cli/commands/reg.py +733 -0
- devsper/cli/github_oauth.py +157 -0
- devsper/cli/init.py +637 -0
- devsper/cli/main.py +2956 -0
- devsper/cli/run_progress.py +103 -0
- devsper/cli/ui/__init__.py +65 -0
- devsper/cli/ui/components.py +94 -0
- devsper/cli/ui/errors.py +104 -0
- devsper/cli/ui/logging.py +120 -0
- devsper/cli/ui/onboarding.py +102 -0
- devsper/cli/ui/progress.py +43 -0
- devsper/cli/ui/run_view.py +308 -0
- devsper/cli/ui/theme.py +40 -0
- devsper/cluster/__init__.py +29 -0
- devsper/cluster/election.py +84 -0
- devsper/cluster/local.py +97 -0
- devsper/cluster/node_info.py +77 -0
- devsper/cluster/registry.py +71 -0
- devsper/cluster/router.py +117 -0
- devsper/cluster/state_backend.py +105 -0
- devsper/compliance/__init__.py +5 -0
- devsper/compliance/pii.py +147 -0
- devsper/config/__init__.py +52 -0
- devsper/config/config_loader.py +121 -0
- devsper/config/defaults.py +77 -0
- devsper/config/resolver.py +342 -0
- devsper/config/schema.py +237 -0
- devsper/credentials/__init__.py +19 -0
- devsper/credentials/cli.py +197 -0
- devsper/credentials/migration.py +124 -0
- devsper/credentials/store.py +142 -0
- devsper/dashboard/__init__.py +9 -0
- devsper/dashboard/dashboard.py +87 -0
- devsper/dev/__init__.py +25 -0
- devsper/dev/builder.py +195 -0
- devsper/dev/debugger.py +95 -0
- devsper/dev/repo_index.py +138 -0
- devsper/dev/sandbox.py +203 -0
- devsper/dev/scaffold.py +122 -0
- devsper/embeddings/__init__.py +5 -0
- devsper/embeddings/service.py +36 -0
- devsper/explainability/__init__.py +14 -0
- devsper/explainability/decision_tree.py +104 -0
- devsper/explainability/rationale.py +38 -0
- devsper/explainability/simulation.py +56 -0
- devsper/hitl/__init__.py +13 -0
- devsper/hitl/approval.py +160 -0
- devsper/hitl/escalation.py +95 -0
- devsper/intelligence/__init__.py +9 -0
- devsper/intelligence/adaptation.py +88 -0
- devsper/intelligence/analysis/__init__.py +19 -0
- devsper/intelligence/analysis/analyzer.py +71 -0
- devsper/intelligence/analysis/cost_estimator.py +66 -0
- devsper/intelligence/analysis/formatter.py +103 -0
- devsper/intelligence/analysis/run_report.py +402 -0
- devsper/intelligence/learning_engine.py +92 -0
- devsper/intelligence/strategies/__init__.py +23 -0
- devsper/intelligence/strategies/base.py +14 -0
- devsper/intelligence/strategies/code_analysis_strategy.py +33 -0
- devsper/intelligence/strategies/data_science_strategy.py +33 -0
- devsper/intelligence/strategies/document_pipeline_strategy.py +33 -0
- devsper/intelligence/strategies/experiment_strategy.py +33 -0
- devsper/intelligence/strategies/research_strategy.py +34 -0
- devsper/intelligence/strategy_selector.py +84 -0
- devsper/intelligence/synthesis.py +132 -0
- devsper/intelligence/task_optimizer.py +92 -0
- devsper/knowledge/__init__.py +5 -0
- devsper/knowledge/extractor.py +204 -0
- devsper/knowledge/knowledge_graph.py +184 -0
- devsper/knowledge/query.py +285 -0
- devsper/memory/__init__.py +35 -0
- devsper/memory/consolidation.py +138 -0
- devsper/memory/embeddings.py +60 -0
- devsper/memory/memory_index.py +97 -0
- devsper/memory/memory_router.py +62 -0
- devsper/memory/memory_store.py +221 -0
- devsper/memory/memory_types.py +54 -0
- devsper/memory/namespaces.py +45 -0
- devsper/memory/scoring.py +77 -0
- devsper/memory/summarizer.py +52 -0
- devsper/nodes/__init__.py +5 -0
- devsper/nodes/controller.py +449 -0
- devsper/nodes/rpc.py +127 -0
- devsper/nodes/single.py +161 -0
- devsper/nodes/worker.py +506 -0
- devsper/orchestration/__init__.py +19 -0
- devsper/orchestration/meta_planner.py +239 -0
- devsper/orchestration/priority_queue.py +61 -0
- devsper/plugins/__init__.py +19 -0
- devsper/plugins/marketplace/__init__.py +0 -0
- devsper/plugins/plugin_loader.py +70 -0
- devsper/plugins/plugin_registry.py +34 -0
- devsper/plugins/registry.py +83 -0
- devsper/protocols/__init__.py +6 -0
- devsper/providers/__init__.py +17 -0
- devsper/providers/anthropic.py +84 -0
- devsper/providers/base.py +75 -0
- devsper/providers/complexity_router.py +94 -0
- devsper/providers/gemini.py +36 -0
- devsper/providers/github.py +180 -0
- devsper/providers/model_router.py +40 -0
- devsper/providers/openai.py +105 -0
- devsper/providers/router/__init__.py +21 -0
- devsper/providers/router/backends/__init__.py +19 -0
- devsper/providers/router/backends/anthropic_backend.py +111 -0
- devsper/providers/router/backends/custom_backend.py +138 -0
- devsper/providers/router/backends/gemini_backend.py +89 -0
- devsper/providers/router/backends/github_backend.py +165 -0
- devsper/providers/router/backends/ollama_backend.py +104 -0
- devsper/providers/router/backends/openai_backend.py +142 -0
- devsper/providers/router/backends/vllm_backend.py +35 -0
- devsper/providers/router/base.py +60 -0
- devsper/providers/router/factory.py +92 -0
- devsper/providers/router/legacy.py +101 -0
- devsper/providers/router/router.py +135 -0
- devsper/reasoning/__init__.py +12 -0
- devsper/reasoning/graph.py +59 -0
- devsper/reasoning/nodes.py +20 -0
- devsper/reasoning/store.py +67 -0
- devsper/runtime/__init__.py +12 -0
- devsper/runtime/health.py +88 -0
- devsper/runtime/replay.py +53 -0
- devsper/runtime/replay_engine.py +142 -0
- devsper/runtime/run_history.py +204 -0
- devsper/runtime/telemetry.py +116 -0
- devsper/runtime/visualize.py +58 -0
- devsper/sandbox/__init__.py +13 -0
- devsper/sandbox/sandbox.py +161 -0
- devsper/swarm/checkpointer.py +65 -0
- devsper/swarm/executor.py +558 -0
- devsper/swarm/map_reduce.py +44 -0
- devsper/swarm/planner.py +197 -0
- devsper/swarm/prefetcher.py +91 -0
- devsper/swarm/scheduler.py +153 -0
- devsper/swarm/speculation.py +47 -0
- devsper/swarm/swarm.py +562 -0
- devsper/tools/__init__.py +33 -0
- devsper/tools/base.py +29 -0
- devsper/tools/code_intelligence/__init__.py +13 -0
- devsper/tools/code_intelligence/api_surface_extractor.py +73 -0
- devsper/tools/code_intelligence/architecture_analyzer.py +65 -0
- devsper/tools/code_intelligence/codebase_indexer.py +71 -0
- devsper/tools/code_intelligence/dependency_graph_builder.py +67 -0
- devsper/tools/code_intelligence/design_pattern_detector.py +62 -0
- devsper/tools/code_intelligence/large_function_detector.py +68 -0
- devsper/tools/code_intelligence/module_responsibility_mapper.py +56 -0
- devsper/tools/code_intelligence/parallel_codebase_analysis.py +44 -0
- devsper/tools/code_intelligence/refactor_candidate_detector.py +81 -0
- devsper/tools/code_intelligence/repository_semantic_index.py +61 -0
- devsper/tools/code_intelligence/test_coverage_estimator.py +62 -0
- devsper/tools/coding/__init__.py +12 -0
- devsper/tools/coding/analyze_code_complexity.py +48 -0
- devsper/tools/coding/dependency_analyzer.py +42 -0
- devsper/tools/coding/extract_functions.py +38 -0
- devsper/tools/coding/format_python.py +50 -0
- devsper/tools/coding/generate_docstrings.py +40 -0
- devsper/tools/coding/generate_unit_tests.py +42 -0
- devsper/tools/coding/lint_python.py +51 -0
- devsper/tools/coding/refactor_function.py +41 -0
- devsper/tools/coding/repo_structure_map.py +54 -0
- devsper/tools/coding/run_python.py +53 -0
- devsper/tools/data/__init__.py +12 -0
- devsper/tools/data/column_type_detection.py +64 -0
- devsper/tools/data/csv_summary.py +52 -0
- devsper/tools/data/dataframe_filter.py +51 -0
- devsper/tools/data/dataframe_groupby.py +47 -0
- devsper/tools/data/dataframe_stats.py +38 -0
- devsper/tools/data/dataset_sampling.py +55 -0
- devsper/tools/data/dataset_schema.py +45 -0
- devsper/tools/data/json_pretty_print.py +37 -0
- devsper/tools/data/json_query.py +46 -0
- devsper/tools/data/missing_value_report.py +47 -0
- devsper/tools/data_science/__init__.py +13 -0
- devsper/tools/data_science/correlation_heatmap.py +72 -0
- devsper/tools/data_science/dataset_bias_detector.py +49 -0
- devsper/tools/data_science/dataset_distribution_report.py +64 -0
- devsper/tools/data_science/dataset_drift_detector.py +64 -0
- devsper/tools/data_science/dataset_outlier_detector.py +65 -0
- devsper/tools/data_science/dataset_profile.py +76 -0
- devsper/tools/data_science/distributed_dataset_processor.py +54 -0
- devsper/tools/data_science/feature_engineering_suggestions.py +69 -0
- devsper/tools/data_science/feature_importance_estimator.py +82 -0
- devsper/tools/data_science/model_input_validator.py +59 -0
- devsper/tools/data_science/time_series_analyzer.py +57 -0
- devsper/tools/documents/__init__.py +11 -0
- devsper/tools/documents/_docproc.py +56 -0
- devsper/tools/documents/document_to_markdown.py +29 -0
- devsper/tools/documents/extract_document_images.py +39 -0
- devsper/tools/documents/extract_document_text.py +29 -0
- devsper/tools/documents/extract_equations.py +36 -0
- devsper/tools/documents/extract_tables.py +47 -0
- devsper/tools/documents/summarize_document.py +42 -0
- devsper/tools/documents/write_latex_document.py +133 -0
- devsper/tools/documents/write_markdown_document.py +89 -0
- devsper/tools/documents/write_word_document.py +149 -0
- devsper/tools/experiments/__init__.py +13 -0
- devsper/tools/experiments/bootstrap_estimator.py +54 -0
- devsper/tools/experiments/experiment_report_generator.py +50 -0
- devsper/tools/experiments/experiment_tracker.py +36 -0
- devsper/tools/experiments/grid_search_runner.py +50 -0
- devsper/tools/experiments/model_benchmark_runner.py +45 -0
- devsper/tools/experiments/monte_carlo_experiment.py +38 -0
- devsper/tools/experiments/parameter_sweep_runner.py +51 -0
- devsper/tools/experiments/result_comparator.py +58 -0
- devsper/tools/experiments/simulation_runner.py +43 -0
- devsper/tools/experiments/statistical_significance_test.py +56 -0
- devsper/tools/experiments/swarm_map_reduce.py +42 -0
- devsper/tools/filesystem/__init__.py +12 -0
- devsper/tools/filesystem/append_file.py +42 -0
- devsper/tools/filesystem/file_hash.py +40 -0
- devsper/tools/filesystem/file_line_count.py +36 -0
- devsper/tools/filesystem/file_metadata.py +38 -0
- devsper/tools/filesystem/file_preview.py +55 -0
- devsper/tools/filesystem/find_large_files.py +50 -0
- devsper/tools/filesystem/list_directory.py +39 -0
- devsper/tools/filesystem/read_file.py +35 -0
- devsper/tools/filesystem/search_files.py +60 -0
- devsper/tools/filesystem/write_file.py +41 -0
- devsper/tools/flagship/__init__.py +15 -0
- devsper/tools/flagship/distributed_document_analysis.py +77 -0
- devsper/tools/flagship/docproc_corpus_pipeline.py +91 -0
- devsper/tools/flagship/repository_semantic_map.py +99 -0
- devsper/tools/flagship/research_graph_builder.py +111 -0
- devsper/tools/flagship/swarm_experiment_runner.py +86 -0
- devsper/tools/knowledge/__init__.py +10 -0
- devsper/tools/knowledge/citation_graph_builder.py +69 -0
- devsper/tools/knowledge/concept_frequency_analyzer.py +74 -0
- devsper/tools/knowledge/corpus_builder.py +66 -0
- devsper/tools/knowledge/cross_document_entity_linker.py +71 -0
- devsper/tools/knowledge/document_corpus_summary.py +68 -0
- devsper/tools/knowledge/document_topic_extractor.py +58 -0
- devsper/tools/knowledge/knowledge_graph_extractor.py +58 -0
- devsper/tools/knowledge/timeline_extractor.py +59 -0
- devsper/tools/math/__init__.py +12 -0
- devsper/tools/math/calculate_expression.py +52 -0
- devsper/tools/math/correlation.py +44 -0
- devsper/tools/math/distribution_summary.py +39 -0
- devsper/tools/math/histogram.py +53 -0
- devsper/tools/math/linear_regression.py +47 -0
- devsper/tools/math/matrix_multiply.py +38 -0
- devsper/tools/math/mean_std.py +35 -0
- devsper/tools/math/monte_carlo_simulation.py +43 -0
- devsper/tools/math/polynomial_fit.py +40 -0
- devsper/tools/math/random_sample.py +36 -0
- devsper/tools/mcp/__init__.py +23 -0
- devsper/tools/mcp/adapter.py +53 -0
- devsper/tools/mcp/client.py +235 -0
- devsper/tools/mcp/discovery.py +53 -0
- devsper/tools/memory/__init__.py +16 -0
- devsper/tools/memory/delete_memory.py +25 -0
- devsper/tools/memory/list_memory.py +34 -0
- devsper/tools/memory/search_memory.py +36 -0
- devsper/tools/memory/store_memory.py +47 -0
- devsper/tools/memory/summarize_memory.py +41 -0
- devsper/tools/memory/tag_memory.py +47 -0
- devsper/tools/pipelines.py +92 -0
- devsper/tools/registry.py +39 -0
- devsper/tools/research/__init__.py +12 -0
- devsper/tools/research/arxiv_download.py +55 -0
- devsper/tools/research/arxiv_search.py +58 -0
- devsper/tools/research/citation_extractor.py +35 -0
- devsper/tools/research/duckduckgo_search.py +42 -0
- devsper/tools/research/paper_metadata_extractor.py +45 -0
- devsper/tools/research/paper_summarizer.py +41 -0
- devsper/tools/research/research_question_generator.py +39 -0
- devsper/tools/research/topic_cluster.py +46 -0
- devsper/tools/research/web_search.py +47 -0
- devsper/tools/research/wikipedia_lookup.py +50 -0
- devsper/tools/research_advanced/__init__.py +14 -0
- devsper/tools/research_advanced/citation_context_extractor.py +60 -0
- devsper/tools/research_advanced/literature_review_generator.py +79 -0
- devsper/tools/research_advanced/methodology_extractor.py +58 -0
- devsper/tools/research_advanced/paper_contribution_extractor.py +50 -0
- devsper/tools/research_advanced/paper_dataset_identifier.py +49 -0
- devsper/tools/research_advanced/paper_method_comparator.py +62 -0
- devsper/tools/research_advanced/paper_similarity_search.py +69 -0
- devsper/tools/research_advanced/paper_trend_analyzer.py +69 -0
- devsper/tools/research_advanced/parallel_document_analyzer.py +56 -0
- devsper/tools/research_advanced/research_gap_finder.py +71 -0
- devsper/tools/research_advanced/research_topic_mapper.py +69 -0
- devsper/tools/research_advanced/swarm_literature_review.py +58 -0
- devsper/tools/scoring/__init__.py +52 -0
- devsper/tools/scoring/report.py +44 -0
- devsper/tools/scoring/scorer.py +39 -0
- devsper/tools/scoring/selector.py +61 -0
- devsper/tools/scoring/store.py +267 -0
- devsper/tools/selector.py +130 -0
- devsper/tools/system/__init__.py +12 -0
- devsper/tools/system/cpu_usage.py +22 -0
- devsper/tools/system/disk_usage.py +35 -0
- devsper/tools/system/environment_variables.py +29 -0
- devsper/tools/system/memory_usage.py +23 -0
- devsper/tools/system/pip_install.py +44 -0
- devsper/tools/system/pip_search.py +29 -0
- devsper/tools/system/process_list.py +34 -0
- devsper/tools/system/python_package_list.py +40 -0
- devsper/tools/system/run_shell_command.py +51 -0
- devsper/tools/system/system_info.py +26 -0
- devsper/tools/tool_runner.py +122 -0
- devsper/tui/__init__.py +5 -0
- devsper/tui/activity_feed_view.py +73 -0
- devsper/tui/adaptive_tasks_view.py +75 -0
- devsper/tui/agent_role_view.py +35 -0
- devsper/tui/app.py +395 -0
- devsper/tui/dashboard_screen.py +290 -0
- devsper/tui/dev_view.py +99 -0
- devsper/tui/inject_screen.py +73 -0
- devsper/tui/knowledge_graph_view.py +46 -0
- devsper/tui/layout.py +43 -0
- devsper/tui/logs_view.py +83 -0
- devsper/tui/memory_view.py +58 -0
- devsper/tui/performance_view.py +33 -0
- devsper/tui/reasoning_graph_view.py +39 -0
- devsper/tui/results_view.py +139 -0
- devsper/tui/swarm_view.py +37 -0
- devsper/tui/task_detail_screen.py +55 -0
- devsper/tui/task_view.py +103 -0
- devsper/types/event.py +97 -0
- devsper/types/exceptions.py +21 -0
- devsper/types/swarm.py +41 -0
- devsper/types/task.py +80 -0
- devsper/upgrade/__init__.py +21 -0
- devsper/upgrade/changelog.py +124 -0
- devsper/upgrade/cli.py +145 -0
- devsper/upgrade/installer.py +103 -0
- devsper/upgrade/notifier.py +52 -0
- devsper/upgrade/version_check.py +121 -0
- devsper/utils/event_logger.py +88 -0
- devsper/utils/http.py +43 -0
- devsper/utils/models.py +54 -0
- devsper/visualization/__init__.py +5 -0
- devsper/visualization/dag_export.py +67 -0
- devsper/workflow/__init__.py +18 -0
- devsper/workflow/conditions.py +157 -0
- devsper/workflow/context.py +108 -0
- devsper/workflow/loader.py +156 -0
- devsper/workflow/resolver.py +109 -0
- devsper/workflow/runner.py +562 -0
- devsper/workflow/schema.py +63 -0
- devsper/workflow/validator.py +128 -0
- devsper-2.1.6.dist-info/METADATA +346 -0
- devsper-2.1.6.dist-info/RECORD +375 -0
- devsper-2.1.6.dist-info/WHEEL +4 -0
- devsper-2.1.6.dist-info/entry_points.txt +3 -0
- devsper-2.1.6.dist-info/licenses/LICENSE +639 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Anthropic Claude LLM backend."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from devsper.providers.router.base import LLMBackend, LLMRequest, LLMResponse
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _messages_to_anthropic(messages: list[dict]) -> list:
|
|
9
|
+
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
|
|
10
|
+
out = []
|
|
11
|
+
for m in messages:
|
|
12
|
+
role = (m.get("role") or "user").lower()
|
|
13
|
+
content = m.get("content") or ""
|
|
14
|
+
if role == "user":
|
|
15
|
+
out.append(HumanMessage(content=content))
|
|
16
|
+
elif role == "assistant":
|
|
17
|
+
out.append(AIMessage(content=content))
|
|
18
|
+
elif role == "system":
|
|
19
|
+
out.append(SystemMessage(content=content))
|
|
20
|
+
else:
|
|
21
|
+
out.append(HumanMessage(content=content))
|
|
22
|
+
return out
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AnthropicBackend(LLMBackend):
|
|
26
|
+
"""Anthropic API (and Azure Foundry when env is set)."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
api_key: str | None = None,
|
|
31
|
+
*,
|
|
32
|
+
azure: bool = False,
|
|
33
|
+
azure_endpoint: str | None = None,
|
|
34
|
+
azure_api_key: str | None = None,
|
|
35
|
+
azure_deployment: str | None = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
self.azure = azure or bool(
|
|
38
|
+
os.environ.get("AZURE_ANTHROPIC_ENDPOINT") or os.environ.get("AZURE_ANTHROPIC_API_KEY")
|
|
39
|
+
)
|
|
40
|
+
self.azure_endpoint = (azure_endpoint or os.environ.get("AZURE_ANTHROPIC_ENDPOINT", "")).rstrip("/")
|
|
41
|
+
if self.azure_endpoint.endswith("/messages"):
|
|
42
|
+
self.azure_endpoint = self.azure_endpoint[: -len("/messages")]
|
|
43
|
+
self.api_key = azure_api_key or os.environ.get("AZURE_ANTHROPIC_API_KEY") if self.azure else (api_key or os.environ.get("ANTHROPIC_API_KEY"))
|
|
44
|
+
self.azure_deployment = (azure_deployment or os.environ.get("AZURE_ANTHROPIC_DEPLOYMENT_NAME") or "").strip()
|
|
45
|
+
self._llm = None
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def name(self) -> str:
|
|
49
|
+
return "anthropic"
|
|
50
|
+
|
|
51
|
+
def _get_llm(self, model: str):
|
|
52
|
+
if self._llm is not None:
|
|
53
|
+
return self._llm
|
|
54
|
+
from langchain_anthropic import ChatAnthropic
|
|
55
|
+
if self.azure:
|
|
56
|
+
self._llm = ChatAnthropic(
|
|
57
|
+
model=model or self.azure_deployment or "claude-3-5-sonnet-20241022",
|
|
58
|
+
anthropic_api_url=self.azure_endpoint,
|
|
59
|
+
anthropic_api_key=self.api_key,
|
|
60
|
+
temperature=0,
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
self._llm = ChatAnthropic(
|
|
64
|
+
model=model or "claude-3-5-sonnet-20241022",
|
|
65
|
+
api_key=self.api_key,
|
|
66
|
+
temperature=0,
|
|
67
|
+
)
|
|
68
|
+
return self._llm
|
|
69
|
+
|
|
70
|
+
def supports_model(self, model_name: str) -> bool:
|
|
71
|
+
return (model_name or "").strip().lower().startswith("claude")
|
|
72
|
+
|
|
73
|
+
async def complete(self, request: LLMRequest) -> LLMResponse:
|
|
74
|
+
from langchain_core.messages import HumanMessage
|
|
75
|
+
model = request.model or "claude-3-5-sonnet-20241022"
|
|
76
|
+
llm = self._get_llm(model)
|
|
77
|
+
lc_messages = _messages_to_anthropic(request.messages)
|
|
78
|
+
msg = await llm.ainvoke(lc_messages)
|
|
79
|
+
content = msg.content if isinstance(msg.content, str) else str(msg.content or "")
|
|
80
|
+
usage = {}
|
|
81
|
+
if hasattr(msg, "response_metadata") and msg.response_metadata:
|
|
82
|
+
meta = msg.response_metadata
|
|
83
|
+
usage = {
|
|
84
|
+
"prompt_tokens": meta.get("input_tokens", 0),
|
|
85
|
+
"completion_tokens": meta.get("output_tokens", 0),
|
|
86
|
+
"total_tokens": meta.get("input_tokens", 0) + meta.get("output_tokens", 0),
|
|
87
|
+
}
|
|
88
|
+
return LLMResponse(
|
|
89
|
+
content=content,
|
|
90
|
+
model=model,
|
|
91
|
+
usage=usage,
|
|
92
|
+
finish_reason="stop",
|
|
93
|
+
backend=self.name,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
async def stream(self, request: LLMRequest):
|
|
97
|
+
model = request.model or "claude-3-5-sonnet-20241022"
|
|
98
|
+
llm = self._get_llm(model)
|
|
99
|
+
lc_messages = _messages_to_anthropic(request.messages)
|
|
100
|
+
async for chunk in llm.astream(lc_messages):
|
|
101
|
+
if hasattr(chunk, "content") and chunk.content:
|
|
102
|
+
yield chunk.content
|
|
103
|
+
|
|
104
|
+
async def health(self) -> bool:
|
|
105
|
+
try:
|
|
106
|
+
await self.complete(
|
|
107
|
+
LLMRequest(model="claude-3-haiku-20240307", messages=[{"role": "user", "content": "Hi"}], max_tokens=2)
|
|
108
|
+
)
|
|
109
|
+
return True
|
|
110
|
+
except Exception:
|
|
111
|
+
return False
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Custom OpenAI-compatible endpoint backend."""
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from devsper.providers.router.base import LLMBackend, LLMRequest, LLMResponse
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _messages_to_openai(messages: list[dict]) -> list[dict]:
|
|
9
|
+
out = []
|
|
10
|
+
for m in messages:
|
|
11
|
+
role = (m.get("role") or "user").lower()
|
|
12
|
+
content = m.get("content") or ""
|
|
13
|
+
out.append({"role": role, "content": content})
|
|
14
|
+
return out
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CustomBackend(LLMBackend):
|
|
18
|
+
"""Any OpenAI-compatible endpoint (base_url, optional api_key, optional model_prefix_strip)."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
base_url: str,
|
|
23
|
+
api_key: str | None = None,
|
|
24
|
+
model_prefix_strip: str | None = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
self.base_url = base_url.rstrip("/")
|
|
27
|
+
self.api_key = api_key
|
|
28
|
+
self.model_prefix_strip = (model_prefix_strip or "").strip() or None
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def name(self) -> str:
|
|
32
|
+
return "custom"
|
|
33
|
+
|
|
34
|
+
def _model_for_request(self, model: str) -> str:
|
|
35
|
+
if self.model_prefix_strip and model.startswith(self.model_prefix_strip):
|
|
36
|
+
return model[len(self.model_prefix_strip):].lstrip("-")
|
|
37
|
+
return model
|
|
38
|
+
|
|
39
|
+
def supports_model(self, model_name: str) -> bool:
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
def _headers(self) -> dict[str, str]:
|
|
43
|
+
h = {"Content-Type": "application/json"}
|
|
44
|
+
if self.api_key:
|
|
45
|
+
h["Authorization"] = f"Bearer {self.api_key}"
|
|
46
|
+
return h
|
|
47
|
+
|
|
48
|
+
async def complete(self, request: LLMRequest) -> LLMResponse:
|
|
49
|
+
model = self._model_for_request(request.model or "gpt-4o")
|
|
50
|
+
messages = _messages_to_openai(request.messages)
|
|
51
|
+
payload = {
|
|
52
|
+
"model": model,
|
|
53
|
+
"messages": messages,
|
|
54
|
+
"max_tokens": request.max_tokens,
|
|
55
|
+
"temperature": request.temperature,
|
|
56
|
+
}
|
|
57
|
+
if request.tools:
|
|
58
|
+
payload["tools"] = request.tools
|
|
59
|
+
url = f"{self.base_url}/v1/chat/completions"
|
|
60
|
+
from devsper.utils.http import ssl_verify, format_retry_after
|
|
61
|
+
async with httpx.AsyncClient(timeout=120.0, verify=ssl_verify()) as client:
|
|
62
|
+
r = await client.post(url, headers=self._headers(), json=payload)
|
|
63
|
+
try:
|
|
64
|
+
r.raise_for_status()
|
|
65
|
+
except httpx.HTTPStatusError as e:
|
|
66
|
+
hint = format_retry_after(e.response)
|
|
67
|
+
if hint:
|
|
68
|
+
raise httpx.HTTPStatusError(
|
|
69
|
+
str(e) + hint, request=e.request, response=e.response
|
|
70
|
+
) from e
|
|
71
|
+
raise
|
|
72
|
+
data = r.json()
|
|
73
|
+
choices = data.get("choices") or []
|
|
74
|
+
content = ""
|
|
75
|
+
finish_reason = "stop"
|
|
76
|
+
if choices:
|
|
77
|
+
msg = choices[0].get("message") or {}
|
|
78
|
+
content = msg.get("content") or ""
|
|
79
|
+
finish_reason = choices[0].get("finish_reason") or "stop"
|
|
80
|
+
usage = data.get("usage") or {}
|
|
81
|
+
return LLMResponse(
|
|
82
|
+
content=content,
|
|
83
|
+
model=model,
|
|
84
|
+
usage={
|
|
85
|
+
"prompt_tokens": usage.get("prompt_tokens", 0),
|
|
86
|
+
"completion_tokens": usage.get("completion_tokens", 0),
|
|
87
|
+
"total_tokens": usage.get("total_tokens", 0),
|
|
88
|
+
},
|
|
89
|
+
finish_reason=finish_reason,
|
|
90
|
+
backend=self.name,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
async def stream(self, request: LLMRequest):
|
|
94
|
+
model = self._model_for_request(request.model or "gpt-4o")
|
|
95
|
+
messages = _messages_to_openai(request.messages)
|
|
96
|
+
payload = {
|
|
97
|
+
"model": model,
|
|
98
|
+
"messages": messages,
|
|
99
|
+
"max_tokens": request.max_tokens,
|
|
100
|
+
"temperature": request.temperature,
|
|
101
|
+
"stream": True,
|
|
102
|
+
}
|
|
103
|
+
url = f"{self.base_url}/v1/chat/completions"
|
|
104
|
+
from devsper.utils.http import ssl_verify, format_retry_after
|
|
105
|
+
async with httpx.AsyncClient(timeout=120.0, verify=ssl_verify()) as client:
|
|
106
|
+
async with client.stream("POST", url, headers=self._headers(), json=payload) as resp:
|
|
107
|
+
try:
|
|
108
|
+
resp.raise_for_status()
|
|
109
|
+
except httpx.HTTPStatusError as e:
|
|
110
|
+
hint = format_retry_after(e.response)
|
|
111
|
+
if hint:
|
|
112
|
+
raise httpx.HTTPStatusError(
|
|
113
|
+
str(e) + hint, request=e.request, response=e.response
|
|
114
|
+
) from e
|
|
115
|
+
raise
|
|
116
|
+
async for line in resp.aiter_lines():
|
|
117
|
+
if not line.strip():
|
|
118
|
+
continue
|
|
119
|
+
if line.strip() == "data: [DONE]":
|
|
120
|
+
return
|
|
121
|
+
if line.startswith("data: "):
|
|
122
|
+
import json
|
|
123
|
+
try:
|
|
124
|
+
chunk = json.loads(line[6:])
|
|
125
|
+
choices = chunk.get("choices") or []
|
|
126
|
+
if choices and choices[0].get("delta", {}).get("content"):
|
|
127
|
+
yield choices[0]["delta"]["content"]
|
|
128
|
+
except Exception:
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
async def health(self) -> bool:
|
|
132
|
+
try:
|
|
133
|
+
await self.complete(
|
|
134
|
+
LLMRequest(model="gpt-4o-mini", messages=[{"role": "user", "content": "Hi"}], max_tokens=2)
|
|
135
|
+
)
|
|
136
|
+
return True
|
|
137
|
+
except Exception:
|
|
138
|
+
return False
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Google Gemini LLM backend."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from devsper.providers.router.base import LLMBackend, LLMRequest, LLMResponse
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _messages_to_lc(messages: list[dict]) -> list:
|
|
9
|
+
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
|
|
10
|
+
out = []
|
|
11
|
+
for m in messages:
|
|
12
|
+
role = (m.get("role") or "user").lower()
|
|
13
|
+
content = m.get("content") or ""
|
|
14
|
+
if role == "user":
|
|
15
|
+
out.append(HumanMessage(content=content))
|
|
16
|
+
elif role == "assistant":
|
|
17
|
+
out.append(AIMessage(content=content))
|
|
18
|
+
elif role == "system":
|
|
19
|
+
out.append(SystemMessage(content=content))
|
|
20
|
+
else:
|
|
21
|
+
out.append(HumanMessage(content=content))
|
|
22
|
+
return out
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GeminiBackend(LLMBackend):
|
|
26
|
+
"""Google Gemini API."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, api_key: str | None = None) -> None:
|
|
29
|
+
self.api_key = api_key or os.environ.get("GOOGLE_API_KEY") or os.environ.get("GEMINI_API_KEY")
|
|
30
|
+
if not self.api_key:
|
|
31
|
+
raise ValueError("Gemini requires api_key or GOOGLE_API_KEY or GEMINI_API_KEY")
|
|
32
|
+
self._llm = None
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def name(self) -> str:
|
|
36
|
+
return "gemini"
|
|
37
|
+
|
|
38
|
+
def _get_llm(self, model: str):
|
|
39
|
+
if self._llm is not None:
|
|
40
|
+
return self._llm
|
|
41
|
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
|
42
|
+
self._llm = ChatGoogleGenerativeAI(
|
|
43
|
+
model=model or "gemini-1.5-flash",
|
|
44
|
+
google_api_key=self.api_key,
|
|
45
|
+
temperature=0,
|
|
46
|
+
)
|
|
47
|
+
return self._llm
|
|
48
|
+
|
|
49
|
+
def supports_model(self, model_name: str) -> bool:
|
|
50
|
+
return (model_name or "").strip().lower().startswith("gemini")
|
|
51
|
+
|
|
52
|
+
async def complete(self, request: LLMRequest) -> LLMResponse:
|
|
53
|
+
model = request.model or "gemini-1.5-flash"
|
|
54
|
+
llm = self._get_llm(model)
|
|
55
|
+
lc_messages = _messages_to_lc(request.messages)
|
|
56
|
+
msg = await llm.ainvoke(lc_messages)
|
|
57
|
+
content = msg.content if isinstance(msg.content, str) else str(msg.content or "")
|
|
58
|
+
usage = {}
|
|
59
|
+
if hasattr(msg, "response_metadata") and msg.response_metadata:
|
|
60
|
+
meta = msg.response_metadata
|
|
61
|
+
usage = {
|
|
62
|
+
"prompt_tokens": meta.get("input_tokens", 0),
|
|
63
|
+
"completion_tokens": meta.get("output_tokens", 0),
|
|
64
|
+
"total_tokens": meta.get("input_tokens", 0) + meta.get("output_tokens", 0),
|
|
65
|
+
}
|
|
66
|
+
return LLMResponse(
|
|
67
|
+
content=content,
|
|
68
|
+
model=model,
|
|
69
|
+
usage=usage,
|
|
70
|
+
finish_reason="stop",
|
|
71
|
+
backend=self.name,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
async def stream(self, request: LLMRequest):
|
|
75
|
+
model = request.model or "gemini-1.5-flash"
|
|
76
|
+
llm = self._get_llm(model)
|
|
77
|
+
lc_messages = _messages_to_lc(request.messages)
|
|
78
|
+
async for chunk in llm.astream(lc_messages):
|
|
79
|
+
if hasattr(chunk, "content") and chunk.content:
|
|
80
|
+
yield chunk.content
|
|
81
|
+
|
|
82
|
+
async def health(self) -> bool:
|
|
83
|
+
try:
|
|
84
|
+
await self.complete(
|
|
85
|
+
LLMRequest(model="gemini-1.5-flash", messages=[{"role": "user", "content": "Hi"}], max_tokens=2)
|
|
86
|
+
)
|
|
87
|
+
return True
|
|
88
|
+
except Exception:
|
|
89
|
+
return False
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""GitHub Models LLM backend (models.github.ai)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from devsper.providers.router.base import LLMBackend, LLMRequest, LLMResponse
|
|
11
|
+
|
|
12
|
+
log = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
GITHUB_MODELS_BASE = "https://models.github.ai"
|
|
15
|
+
GITHUB_CHAT_URL = f"{GITHUB_MODELS_BASE}/inference/chat/completions"
|
|
16
|
+
GITHUB_API_VERSION = "2022-11-28"
|
|
17
|
+
DEFAULT_GITHUB_MODEL = "openai/gpt-4.1"
|
|
18
|
+
GITHUB_429_MAX_RETRIES = 3
|
|
19
|
+
GITHUB_429_BASE_DELAY = 1.0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _normalize_model_id(model: str) -> str:
|
|
23
|
+
s = model.split(":", 1)[-1].strip() if ":" in model else model.strip()
|
|
24
|
+
if not s or s.lower() == "copilot":
|
|
25
|
+
return DEFAULT_GITHUB_MODEL
|
|
26
|
+
return s
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class GitHubBackend(LLMBackend):
|
|
30
|
+
"""GitHub Models API. Uses GITHUB_TOKEN."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, token: str | None = None) -> None:
|
|
33
|
+
self.token = token or os.environ.get("GITHUB_TOKEN")
|
|
34
|
+
if not self.token:
|
|
35
|
+
raise ValueError("GitHub backend requires GITHUB_TOKEN")
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def name(self) -> str:
|
|
39
|
+
return "github"
|
|
40
|
+
|
|
41
|
+
def _headers(self) -> dict[str, str]:
|
|
42
|
+
return {
|
|
43
|
+
"Accept": "application/vnd.github.v3+json",
|
|
44
|
+
"Content-Type": "application/json",
|
|
45
|
+
"Authorization": f"Bearer {self.token}",
|
|
46
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
def supports_model(self, model_name: str) -> bool:
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
async def complete(self, request: LLMRequest) -> LLMResponse:
|
|
53
|
+
api_model = _normalize_model_id(request.model)
|
|
54
|
+
messages = request.messages
|
|
55
|
+
last_content = ""
|
|
56
|
+
for m in reversed(messages):
|
|
57
|
+
if (m.get("role") or "user").lower() == "user":
|
|
58
|
+
last_content = m.get("content") or ""
|
|
59
|
+
break
|
|
60
|
+
payload = {
|
|
61
|
+
"model": api_model,
|
|
62
|
+
"messages": [{"role": "user", "content": last_content}] if last_content else [{"role": "user", "content": "Hi"}],
|
|
63
|
+
"max_tokens": request.max_tokens,
|
|
64
|
+
"temperature": request.temperature,
|
|
65
|
+
}
|
|
66
|
+
from devsper.utils.http import ssl_verify, format_retry_after
|
|
67
|
+
async with httpx.AsyncClient(timeout=60.0, verify=ssl_verify()) as client:
|
|
68
|
+
for attempt in range(GITHUB_429_MAX_RETRIES):
|
|
69
|
+
try:
|
|
70
|
+
resp = await client.post(
|
|
71
|
+
GITHUB_CHAT_URL,
|
|
72
|
+
headers=self._headers(),
|
|
73
|
+
json=payload,
|
|
74
|
+
)
|
|
75
|
+
resp.raise_for_status()
|
|
76
|
+
data = resp.json()
|
|
77
|
+
break
|
|
78
|
+
except httpx.HTTPStatusError as e:
|
|
79
|
+
retry_hint = format_retry_after(e.response)
|
|
80
|
+
if e.response.status_code == 429 and attempt < GITHUB_429_MAX_RETRIES - 1:
|
|
81
|
+
delay = min(GITHUB_429_BASE_DELAY * (2**attempt), 32.0)
|
|
82
|
+
log.warning(
|
|
83
|
+
"GitHub 429, retry %s/%s in %.1fs%s",
|
|
84
|
+
attempt + 1, GITHUB_429_MAX_RETRIES, delay, retry_hint,
|
|
85
|
+
)
|
|
86
|
+
time.sleep(delay)
|
|
87
|
+
else:
|
|
88
|
+
if retry_hint:
|
|
89
|
+
raise httpx.HTTPStatusError(
|
|
90
|
+
str(e) + retry_hint, request=e.request, response=e.response
|
|
91
|
+
) from e
|
|
92
|
+
raise
|
|
93
|
+
choices = data.get("choices") or []
|
|
94
|
+
content = ""
|
|
95
|
+
if choices:
|
|
96
|
+
msg = choices[0].get("message") or {}
|
|
97
|
+
c = msg.get("content")
|
|
98
|
+
if isinstance(c, str):
|
|
99
|
+
content = c
|
|
100
|
+
elif isinstance(c, list):
|
|
101
|
+
parts = [p.get("text", "") for p in c if isinstance(p, dict) and p.get("type") == "text"]
|
|
102
|
+
content = "\n".join(parts) if parts else ""
|
|
103
|
+
else:
|
|
104
|
+
content = str(c or "")
|
|
105
|
+
usage = data.get("usage") or {}
|
|
106
|
+
return LLMResponse(
|
|
107
|
+
content=content,
|
|
108
|
+
model=api_model,
|
|
109
|
+
usage={
|
|
110
|
+
"prompt_tokens": usage.get("prompt_tokens", 0),
|
|
111
|
+
"completion_tokens": usage.get("completion_tokens", 0),
|
|
112
|
+
"total_tokens": usage.get("total_tokens", 0),
|
|
113
|
+
},
|
|
114
|
+
finish_reason="stop",
|
|
115
|
+
backend=self.name,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
async def stream(self, request: LLMRequest):
|
|
119
|
+
api_model = _normalize_model_id(request.model)
|
|
120
|
+
last_content = ""
|
|
121
|
+
for m in reversed(request.messages):
|
|
122
|
+
if (m.get("role") or "user").lower() == "user":
|
|
123
|
+
last_content = m.get("content") or ""
|
|
124
|
+
break
|
|
125
|
+
payload = {
|
|
126
|
+
"model": api_model,
|
|
127
|
+
"messages": [{"role": "user", "content": last_content or "Hi"}],
|
|
128
|
+
"temperature": request.temperature,
|
|
129
|
+
"stream": True,
|
|
130
|
+
}
|
|
131
|
+
from devsper.utils.http import ssl_verify
|
|
132
|
+
async with httpx.AsyncClient(timeout=60.0, verify=ssl_verify()) as client:
|
|
133
|
+
async with client.stream(
|
|
134
|
+
"POST",
|
|
135
|
+
GITHUB_CHAT_URL,
|
|
136
|
+
headers=self._headers(),
|
|
137
|
+
json=payload,
|
|
138
|
+
) as resp:
|
|
139
|
+
resp.raise_for_status()
|
|
140
|
+
async for line in resp.aiter_lines():
|
|
141
|
+
if not line or not line.strip():
|
|
142
|
+
continue
|
|
143
|
+
if line.startswith("data: "):
|
|
144
|
+
chunk = line[6:].strip()
|
|
145
|
+
if chunk == "[DONE]":
|
|
146
|
+
return
|
|
147
|
+
try:
|
|
148
|
+
data = json.loads(chunk)
|
|
149
|
+
choices = data.get("choices") or []
|
|
150
|
+
if choices:
|
|
151
|
+
delta = choices[0].get("delta") or {}
|
|
152
|
+
part = delta.get("content")
|
|
153
|
+
if part:
|
|
154
|
+
yield part
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
async def health(self) -> bool:
|
|
159
|
+
try:
|
|
160
|
+
await self.complete(
|
|
161
|
+
LLMRequest(model="openai/gpt-4.1", messages=[{"role": "user", "content": "Hi"}], max_tokens=2)
|
|
162
|
+
)
|
|
163
|
+
return True
|
|
164
|
+
except Exception:
|
|
165
|
+
return False
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Ollama local LLM backend (http://localhost:11434). No API key required."""
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from devsper.providers.router.base import LLMBackend, LLMRequest, LLMResponse
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OllamaBackend(LLMBackend):
|
|
9
|
+
"""Ollama local. supports_model via GET /api/tags; complete via POST /api/chat."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, base_url: str = "http://localhost:11434") -> None:
|
|
12
|
+
self.base_url = base_url.rstrip("/")
|
|
13
|
+
self._models_cache: list[str] | None = None
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def name(self) -> str:
|
|
17
|
+
return "ollama"
|
|
18
|
+
|
|
19
|
+
async def _fetch_models(self) -> list[str]:
|
|
20
|
+
if self._models_cache is not None:
|
|
21
|
+
return self._models_cache
|
|
22
|
+
try:
|
|
23
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
24
|
+
r = await client.get(f"{self.base_url}/api/tags")
|
|
25
|
+
r.raise_for_status()
|
|
26
|
+
data = r.json()
|
|
27
|
+
models = data.get("models") or []
|
|
28
|
+
self._models_cache = [m.get("name", "").split(":")[0] for m in models if m.get("name")]
|
|
29
|
+
return self._models_cache
|
|
30
|
+
except Exception:
|
|
31
|
+
self._models_cache = []
|
|
32
|
+
return []
|
|
33
|
+
|
|
34
|
+
def supports_model(self, model_name: str) -> bool:
|
|
35
|
+
"""Ollama can serve any model that exists on the server; we rely on prefix 'ollama:' for routing."""
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
async def complete(self, request: LLMRequest) -> LLMResponse:
|
|
39
|
+
model = request.model or "llama3"
|
|
40
|
+
messages = request.messages
|
|
41
|
+
prompt = ""
|
|
42
|
+
for m in reversed(messages):
|
|
43
|
+
if (m.get("role") or "user").lower() == "user":
|
|
44
|
+
prompt = m.get("content") or ""
|
|
45
|
+
break
|
|
46
|
+
payload = {
|
|
47
|
+
"model": model,
|
|
48
|
+
"messages": [{"role": "user", "content": prompt}] if prompt else [{"role": "user", "content": "Hi"}],
|
|
49
|
+
"stream": False,
|
|
50
|
+
"options": {"num_predict": request.max_tokens, "temperature": request.temperature},
|
|
51
|
+
}
|
|
52
|
+
async with httpx.AsyncClient(timeout=120.0) as client:
|
|
53
|
+
r = await client.post(f"{self.base_url}/api/chat", json=payload)
|
|
54
|
+
r.raise_for_status()
|
|
55
|
+
data = r.json()
|
|
56
|
+
msg = data.get("message") or {}
|
|
57
|
+
content = msg.get("content") or ""
|
|
58
|
+
usage = data.get("eval_count") or 0
|
|
59
|
+
return LLMResponse(
|
|
60
|
+
content=content,
|
|
61
|
+
model=model,
|
|
62
|
+
usage={"prompt_tokens": 0, "completion_tokens": usage, "total_tokens": usage},
|
|
63
|
+
finish_reason="stop",
|
|
64
|
+
backend=self.name,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
async def stream(self, request: LLMRequest):
|
|
68
|
+
model = request.model or "llama3"
|
|
69
|
+
prompt = ""
|
|
70
|
+
for m in reversed(request.messages):
|
|
71
|
+
if (m.get("role") or "user").lower() == "user":
|
|
72
|
+
prompt = m.get("content") or ""
|
|
73
|
+
break
|
|
74
|
+
payload = {
|
|
75
|
+
"model": model,
|
|
76
|
+
"messages": [{"role": "user", "content": prompt or "Hi"}],
|
|
77
|
+
"stream": True,
|
|
78
|
+
"options": {"num_predict": request.max_tokens, "temperature": request.temperature},
|
|
79
|
+
}
|
|
80
|
+
async with httpx.AsyncClient(timeout=120.0) as client:
|
|
81
|
+
async with client.stream("POST", f"{self.base_url}/api/chat", json=payload) as resp:
|
|
82
|
+
resp.raise_for_status()
|
|
83
|
+
async for line in resp.aiter_lines():
|
|
84
|
+
if not line.strip():
|
|
85
|
+
continue
|
|
86
|
+
import json
|
|
87
|
+
try:
|
|
88
|
+
data = json.loads(line)
|
|
89
|
+
msg = data.get("message") or {}
|
|
90
|
+
part = msg.get("content")
|
|
91
|
+
if part:
|
|
92
|
+
yield part
|
|
93
|
+
if data.get("done"):
|
|
94
|
+
break
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
async def health(self) -> bool:
|
|
99
|
+
try:
|
|
100
|
+
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
101
|
+
r = await client.get(f"{self.base_url}/api/tags")
|
|
102
|
+
return r.status_code == 200
|
|
103
|
+
except Exception:
|
|
104
|
+
return False
|