pyinferencemanager 1.2.0__tar.gz → 1.3.0__tar.gz

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.
Files changed (82) hide show
  1. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/Cargo.lock +2 -2
  2. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/Cargo.toml +1 -1
  3. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/PKG-INFO +4 -3
  4. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/README.md +3 -2
  5. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/backends/mod.rs +3 -1
  6. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/backends/stub_backend.rs +9 -19
  7. pyinferencemanager-1.3.0/crates/pyinferencemanager-core/src/backends/vllm_backend.rs +155 -0
  8. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/engines/mod.rs +2 -0
  9. pyinferencemanager-1.3.0/crates/pyinferencemanager-core/src/engines/provider_health.rs +483 -0
  10. pyinferencemanager-1.3.0/crates/pyinferencemanager-core/src/engines/vllm_client.rs +347 -0
  11. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/error.rs +3 -0
  12. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/optimizer/retry_strategy.rs +26 -0
  13. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/orchestrator/mod.rs +180 -2
  14. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/types/config.rs +5 -0
  15. pyinferencemanager-1.2.0/crates/pyinferencemanager-core/src/engines/provider_health.rs +0 -303
  16. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/LICENSE +0 -0
  17. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/Cargo.toml +0 -0
  18. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/analyzer/classifier.rs +0 -0
  19. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/analyzer/complexity.rs +0 -0
  20. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/analyzer/embedding_complexity.rs +0 -0
  21. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/analyzer/mod.rs +0 -0
  22. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/backends/anthropic_backend.rs +0 -0
  23. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/backends/colibri_backend.rs +0 -0
  24. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/backends/gemini_backend.rs +0 -0
  25. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/backends/ollama_backend.rs +0 -0
  26. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/backends/openai_backend.rs +0 -0
  27. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/cache/embedding_key.rs +0 -0
  28. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/cache/mod.rs +0 -0
  29. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/cache/semantic_cache.rs +0 -0
  30. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/cache/sqlite_store.rs +0 -0
  31. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/engines/cloud_client.rs +0 -0
  32. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/engines/gemini_client.rs +0 -0
  33. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/engines/ollama_client.rs +0 -0
  34. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/engines/openai_client.rs +0 -0
  35. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/error_classifier.rs +0 -0
  36. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/hardware/memory_map.rs +0 -0
  37. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/hardware/mod.rs +0 -0
  38. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/hardware/ollama_probe.rs +0 -0
  39. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/hardware/profiler.rs +0 -0
  40. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/lib.rs +0 -0
  41. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/memory/mod.rs +0 -0
  42. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/memory/resource_monitor.rs +0 -0
  43. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/observability/exporters/jaeger_exporter.rs +0 -0
  44. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/observability/exporters/logging_exporter.rs +0 -0
  45. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/observability/exporters/mod.rs +0 -0
  46. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/observability/exporters/prometheus_exporter.rs +0 -0
  47. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/observability/logging.rs +0 -0
  48. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/observability/metrics.rs +0 -0
  49. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/observability/mod.rs +0 -0
  50. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/observability/tracer.rs +0 -0
  51. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/optimizer/budget_enforcer.rs +0 -0
  52. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/optimizer/cost_estimator.rs +0 -0
  53. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/optimizer/cost_tracker.rs +0 -0
  54. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/optimizer/dynamic_router.rs +0 -0
  55. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/optimizer/mod.rs +0 -0
  56. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/orchestrator/api_executor.rs +0 -0
  57. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/orchestrator/executor.rs +0 -0
  58. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/orchestrator/load_tester.rs +0 -0
  59. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/orchestrator/provider_executor.rs +0 -0
  60. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/orchestrator/provider_load_test.rs +0 -0
  61. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/orchestrator/real_load_tester.rs +0 -0
  62. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/orchestrator/scenarios.rs +0 -0
  63. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/planner/dag_builder.rs +0 -0
  64. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/planner/mod.rs +0 -0
  65. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/planner/parallel.rs +0 -0
  66. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/planner/templates.rs +0 -0
  67. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/router/execution_router.rs +0 -0
  68. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/router/mod.rs +0 -0
  69. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/router/multi_provider.rs +0 -0
  70. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/types/cache.rs +0 -0
  71. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/types/dag.rs +0 -0
  72. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/types/hardware.rs +0 -0
  73. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/types/mod.rs +0 -0
  74. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/types/plan.rs +0 -0
  75. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-core/src/types/task.rs +0 -0
  76. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-py/Cargo.toml +0 -0
  77. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/crates/pyinferencemanager-py/src/lib.rs +0 -0
  78. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/pyproject.toml +0 -0
  79. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/src/pyinferencemanager/__init__.py +0 -0
  80. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/src/pyinferencemanager/_mcp_connector.py +0 -0
  81. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/src/pyinferencemanager/_mcp_tools.py +0 -0
  82. {pyinferencemanager-1.2.0 → pyinferencemanager-1.3.0}/src/pyinferencemanager/py.typed +0 -0
@@ -1339,7 +1339,7 @@ checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94"
1339
1339
 
1340
1340
  [[package]]
1341
1341
  name = "pyinferencemanager"
1342
- version = "1.2.0"
1342
+ version = "1.3.0"
1343
1343
  dependencies = [
1344
1344
  "chrono",
1345
1345
  "pyinferencemanager-core",
@@ -1350,7 +1350,7 @@ dependencies = [
1350
1350
 
1351
1351
  [[package]]
1352
1352
  name = "pyinferencemanager-core"
1353
- version = "1.2.0"
1353
+ version = "1.3.0"
1354
1354
  dependencies = [
1355
1355
  "anyhow",
1356
1356
  "async-trait",
@@ -3,7 +3,7 @@ members = ["crates/pyinferencemanager-core", "crates/pyinferencemanager-py"]
3
3
  resolver = "2"
4
4
 
5
5
  [workspace.package]
6
- version = "1.2.0"
6
+ version = "1.3.0"
7
7
  edition = "2021"
8
8
  rust-version = "1.82"
9
9
  authors = ["Georgi Mammen Mullassery <mullassery@gmail.com>"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyinferencemanager
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -134,8 +134,9 @@ Real HTTP calls, with retry/backoff and cost tracking:
134
134
  | OpenAI | `OPENAI_API_KEY` | `gpt-4o-mini` |
135
135
  | Google Gemini | `GEMINI_API_KEY` / `GOOGLE_API_KEY` | `gemini-1.5-flash` |
136
136
  | Ollama (local) | — | whatever models you've pulled locally |
137
+ | vLLM (local) | — | OpenAI-compatible endpoint, default `localhost:8000` |
137
138
 
138
- Additionally, `vllm`, `tensorrt_llm`, `mlc_llm`, and `colibri` exist as **cost-estimator-only stubs** (`BackendKind`/`RuntimeBackend` trait implementations with real cost/latency estimation logic, but no live inference) — they're placeholders for self-hosted inference servers, not currently wired to make real calls. `orchestrator.available_backends()` lists all of these honestly, including the stubs.
139
+ Additionally, `tensorrt_llm`, `mlc_llm`, and `colibri` exist as **cost-estimator-only stubs** (`BackendKind`/`RuntimeBackend` trait implementations with real cost/latency estimation logic, but no live inference) — they're placeholders for self-hosted inference servers, not currently wired to make real calls. `orchestrator.available_backends()` lists all of these honestly, including the stubs.
139
140
 
140
141
  Cloud execution dispatches through `BackendRegistry`/`RuntimeBackend` (not a hand-matched provider enum): `ProviderExecutor::execute` maps a `CloudProvider` to its `BackendKind`, registers the one backend it needs (reading the API key from the env vars above), and calls it through the same `RuntimeBackend::infer` trait object every backend implements — so adding a new cloud provider is a new `BackendKind`/backend + one dispatch arm, not edits scattered across every call site that used to match the old enum.
141
142
 
@@ -152,7 +153,7 @@ The network connector (`_mcp_connector.InferenceManager.start_mcp_connector()`)
152
153
 
153
154
  ## Known issues
154
155
 
155
- - `vllm`, `tensorrt_llm`, `mlc_llm`, and `colibri` backends are cost-estimator-only stubs (see [Providers](#providers)) — they do not make live inference calls yet.
156
+ - `tensorrt_llm`, `mlc_llm`, and `colibri` backends are cost-estimator-only stubs (see [Providers](#providers)) — they do not make live inference calls yet. `vllm` was a stub too as of 1.2.0 but is now a real backend.
156
157
  - No open GitHub issues and no `TODO`/`FIXME` markers in the codebase at the time of this writing.
157
158
 
158
159
  ## License
@@ -100,8 +100,9 @@ Real HTTP calls, with retry/backoff and cost tracking:
100
100
  | OpenAI | `OPENAI_API_KEY` | `gpt-4o-mini` |
101
101
  | Google Gemini | `GEMINI_API_KEY` / `GOOGLE_API_KEY` | `gemini-1.5-flash` |
102
102
  | Ollama (local) | — | whatever models you've pulled locally |
103
+ | vLLM (local) | — | OpenAI-compatible endpoint, default `localhost:8000` |
103
104
 
104
- Additionally, `vllm`, `tensorrt_llm`, `mlc_llm`, and `colibri` exist as **cost-estimator-only stubs** (`BackendKind`/`RuntimeBackend` trait implementations with real cost/latency estimation logic, but no live inference) — they're placeholders for self-hosted inference servers, not currently wired to make real calls. `orchestrator.available_backends()` lists all of these honestly, including the stubs.
105
+ Additionally, `tensorrt_llm`, `mlc_llm`, and `colibri` exist as **cost-estimator-only stubs** (`BackendKind`/`RuntimeBackend` trait implementations with real cost/latency estimation logic, but no live inference) — they're placeholders for self-hosted inference servers, not currently wired to make real calls. `orchestrator.available_backends()` lists all of these honestly, including the stubs.
105
106
 
106
107
  Cloud execution dispatches through `BackendRegistry`/`RuntimeBackend` (not a hand-matched provider enum): `ProviderExecutor::execute` maps a `CloudProvider` to its `BackendKind`, registers the one backend it needs (reading the API key from the env vars above), and calls it through the same `RuntimeBackend::infer` trait object every backend implements — so adding a new cloud provider is a new `BackendKind`/backend + one dispatch arm, not edits scattered across every call site that used to match the old enum.
107
108
 
@@ -118,7 +119,7 @@ The network connector (`_mcp_connector.InferenceManager.start_mcp_connector()`)
118
119
 
119
120
  ## Known issues
120
121
 
121
- - `vllm`, `tensorrt_llm`, `mlc_llm`, and `colibri` backends are cost-estimator-only stubs (see [Providers](#providers)) — they do not make live inference calls yet.
122
+ - `tensorrt_llm`, `mlc_llm`, and `colibri` backends are cost-estimator-only stubs (see [Providers](#providers)) — they do not make live inference calls yet. `vllm` was a stub too as of 1.2.0 but is now a real backend.
122
123
  - No open GitHub issues and no `TODO`/`FIXME` markers in the codebase at the time of this writing.
123
124
 
124
125
  ## License
@@ -7,6 +7,7 @@ pub mod gemini_backend;
7
7
  pub mod ollama_backend;
8
8
  pub mod openai_backend;
9
9
  pub mod stub_backend;
10
+ pub mod vllm_backend;
10
11
 
11
12
  pub use anthropic_backend::AnthropicBackend;
12
13
  pub use colibri_backend::ColibriBackend;
@@ -14,6 +15,7 @@ pub use gemini_backend::GeminiBackend;
14
15
  pub use ollama_backend::OllamaBackend;
15
16
  pub use openai_backend::OpenAiBackend;
16
17
  pub use stub_backend::StubBackend;
18
+ pub use vllm_backend::VLlmBackend;
17
19
 
18
20
  #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
19
21
  pub enum BackendKind {
@@ -105,7 +107,7 @@ pub enum AnyBackend {
105
107
  OpenAi(OpenAiBackend),
106
108
  Gemini(GeminiBackend),
107
109
  Ollama(OllamaBackend),
108
- VLlm(StubBackend),
110
+ VLlm(VLlmBackend),
109
111
  TensorRtLlm(StubBackend),
110
112
  MlcLlm(StubBackend),
111
113
  Colibri(ColibriBackend),
@@ -1,5 +1,10 @@
1
1
  use super::{BackendKind, InferenceRequest, InferenceResult, ModelProfile, RuntimeBackend};
2
2
 
3
+ /// Placeholder `RuntimeBackend` for runtimes that don't have a real client
4
+ /// implementation yet. `BackendKind::VLlm` used to be one of these -- it now
5
+ /// has a real implementation in `vllm_backend::VLlmBackend` (see
6
+ /// `AnyBackend::VLlm` in `mod.rs`), so it's no longer constructed with this
7
+ /// stub. Only `TensorRtLlm` and `MlcLlm` remain genuinely unimplemented.
3
8
  pub struct StubBackend {
4
9
  kind: BackendKind,
5
10
  }
@@ -22,15 +27,6 @@ impl RuntimeBackend for StubBackend {
22
27
  fn estimate_cost(&self, profile: &ModelProfile) -> f32 {
23
28
  let size_gb = profile.size_gb();
24
29
  match self.kind {
25
- BackendKind::VLlm => {
26
- if size_gb < 20.0 {
27
- 0.0
28
- } else if size_gb < 50.0 {
29
- 0.00001
30
- } else {
31
- 0.00002
32
- }
33
- }
34
30
  BackendKind::TensorRtLlm => {
35
31
  if size_gb < 20.0 {
36
32
  0.0
@@ -52,10 +48,6 @@ impl RuntimeBackend for StubBackend {
52
48
  fn estimate_latency(&self, profile: &ModelProfile) -> u64 {
53
49
  let size_gb = profile.size_gb();
54
50
  match self.kind {
55
- BackendKind::VLlm => {
56
- let base = (size_gb * 30.0) as u64;
57
- base.max(200).min(30000)
58
- }
59
51
  BackendKind::TensorRtLlm => {
60
52
  let base = (size_gb * 20.0) as u64;
61
53
  base.max(150).min(20000)
@@ -79,7 +71,7 @@ mod tests {
79
71
 
80
72
  #[test]
81
73
  fn test_stub_backend_infer_returns_error() {
82
- let backend = StubBackend::new(BackendKind::VLlm);
74
+ let backend = StubBackend::new(BackendKind::TensorRtLlm);
83
75
  let request = InferenceRequest {
84
76
  model: "model".to_string(),
85
77
  prompt: "test".to_string(),
@@ -93,23 +85,21 @@ mod tests {
93
85
 
94
86
  #[test]
95
87
  fn test_stub_backend_kind() {
96
- let vllm = StubBackend::new(BackendKind::VLlm);
97
88
  let trt = StubBackend::new(BackendKind::TensorRtLlm);
98
89
  let mlc = StubBackend::new(BackendKind::MlcLlm);
99
90
 
100
- assert_eq!(vllm.kind(), BackendKind::VLlm);
101
91
  assert_eq!(trt.kind(), BackendKind::TensorRtLlm);
102
92
  assert_eq!(mlc.kind(), BackendKind::MlcLlm);
103
93
  }
104
94
 
105
95
  #[test]
106
96
  fn test_stub_backend_estimate_cost() {
107
- let vllm = StubBackend::new(BackendKind::VLlm);
97
+ let trt = StubBackend::new(BackendKind::TensorRtLlm);
108
98
  let small = ModelProfile::new(10 * 1_073_741_824, "model".to_string());
109
99
  let large = ModelProfile::new(60 * 1_073_741_824, "model".to_string());
110
100
 
111
- let small_cost = vllm.estimate_cost(&small);
112
- let large_cost = vllm.estimate_cost(&large);
101
+ let small_cost = trt.estimate_cost(&small);
102
+ let large_cost = trt.estimate_cost(&large);
113
103
 
114
104
  assert!(small_cost <= large_cost);
115
105
  }
@@ -0,0 +1,155 @@
1
+ use super::{BackendKind, InferenceRequest, InferenceResult, ModelProfile, RuntimeBackend};
2
+ use crate::engines::VLlmClient;
3
+
4
+ /// Real `RuntimeBackend` for a self-hosted vLLM server, talking to its
5
+ /// OpenAI-compatible HTTP API (`POST /v1/completions`). Follows the same
6
+ /// shape as `OllamaBackend`: a thin adapter that owns just enough config
7
+ /// (here, the base URL and optional API key) to construct a fresh client
8
+ /// per call and translate `InferenceRequest`/`InferenceResult` to and from
9
+ /// the wire format.
10
+ pub struct VLlmBackend {
11
+ base_url: String,
12
+ api_key: Option<String>,
13
+ }
14
+
15
+ impl VLlmBackend {
16
+ pub fn new(base_url: String) -> Self {
17
+ VLlmBackend {
18
+ base_url,
19
+ api_key: None,
20
+ }
21
+ }
22
+
23
+ pub fn with_api_key(mut self, api_key: Option<String>) -> Self {
24
+ self.api_key = api_key;
25
+ self
26
+ }
27
+ }
28
+
29
+ #[async_trait::async_trait]
30
+ impl RuntimeBackend for VLlmBackend {
31
+ async fn infer(&self, request: InferenceRequest) -> crate::Result<InferenceResult> {
32
+ let start = std::time::Instant::now();
33
+ let client = VLlmClient::new(&self.base_url).with_api_key(self.api_key.clone());
34
+
35
+ let response = client
36
+ .complete(&request.model, &request.prompt, request.max_tokens)
37
+ .await?;
38
+
39
+ let latency_ms = start.elapsed().as_millis() as u64;
40
+
41
+ Ok(InferenceResult {
42
+ output: response.text,
43
+ tokens_used: response.tokens_used,
44
+ latency_ms,
45
+ })
46
+ }
47
+
48
+ // vLLM is a self-hosted runtime like Ollama, not a metered cloud API --
49
+ // there's no per-token bill from vLLM itself. What scales with model
50
+ // size is the GPU/compute cost of running it, so this mirrors the
51
+ // original stub's tiered estimate (kept identical to avoid silently
52
+ // changing cost-comparison behavior elsewhere, e.g. CostEstimator)
53
+ // rather than collapsing to a flat 0.0 like OllamaBackend, since vLLM is
54
+ // typically deployed for larger models where that compute cost is
55
+ // material.
56
+ fn estimate_cost(&self, profile: &ModelProfile) -> f32 {
57
+ let size_gb = profile.size_gb();
58
+ if size_gb < 20.0 {
59
+ 0.0
60
+ } else if size_gb < 50.0 {
61
+ 0.00001
62
+ } else {
63
+ 0.00002
64
+ }
65
+ }
66
+
67
+ fn estimate_latency(&self, profile: &ModelProfile) -> u64 {
68
+ let size_gb = profile.size_gb();
69
+ let base = (size_gb * 30.0) as u64;
70
+ base.max(200).min(30000)
71
+ }
72
+
73
+ fn kind(&self) -> BackendKind {
74
+ BackendKind::VLlm
75
+ }
76
+ }
77
+
78
+ #[cfg(test)]
79
+ mod tests {
80
+ use super::*;
81
+
82
+ #[test]
83
+ fn test_vllm_backend_kind() {
84
+ let backend = VLlmBackend::new("http://localhost:8000".to_string());
85
+ assert_eq!(backend.kind(), BackendKind::VLlm);
86
+ }
87
+
88
+ #[test]
89
+ fn test_vllm_estimate_cost_tiers_by_model_size() {
90
+ let backend = VLlmBackend::new("http://localhost:8000".to_string());
91
+ let small = ModelProfile::new(10 * 1_073_741_824, "model".to_string());
92
+ let medium = ModelProfile::new(30 * 1_073_741_824, "model".to_string());
93
+ let large = ModelProfile::new(60 * 1_073_741_824, "model".to_string());
94
+
95
+ assert_eq!(backend.estimate_cost(&small), 0.0);
96
+ assert!(backend.estimate_cost(&medium) < backend.estimate_cost(&large));
97
+ }
98
+
99
+ #[test]
100
+ fn test_vllm_estimate_latency_bounds() {
101
+ let backend = VLlmBackend::new("http://localhost:8000".to_string());
102
+ let small_profile = ModelProfile::new(7 * 1_073_741_824, "llama".to_string());
103
+ let large_profile = ModelProfile::new(70 * 1_073_741_824, "llama".to_string());
104
+
105
+ let small_latency = backend.estimate_latency(&small_profile);
106
+ let large_latency = backend.estimate_latency(&large_profile);
107
+
108
+ assert!(small_latency < large_latency);
109
+ assert!(small_latency >= 200);
110
+ }
111
+
112
+ use wiremock::matchers::{method, path};
113
+ use wiremock::{Mock, MockServer, ResponseTemplate};
114
+
115
+ #[tokio::test]
116
+ async fn test_infer_real_http_dispatch_through_backend() {
117
+ let mock_server = MockServer::start().await;
118
+
119
+ Mock::given(method("POST"))
120
+ .and(path("/v1/completions"))
121
+ .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
122
+ "choices": [{"text": "Hello from vLLM", "finish_reason": "stop"}],
123
+ "usage": {"prompt_tokens": 3, "completion_tokens": 4, "total_tokens": 7}
124
+ })))
125
+ .mount(&mock_server)
126
+ .await;
127
+
128
+ let backend = VLlmBackend::new(mock_server.uri());
129
+ let request = InferenceRequest {
130
+ model: "meta-llama/Llama-3-8b".to_string(),
131
+ prompt: "Hi".to_string(),
132
+ max_tokens: 50,
133
+ };
134
+
135
+ let result = backend.infer(request).await.expect("mocked infer should succeed");
136
+ assert_eq!(result.output, "Hello from vLLM");
137
+ assert_eq!(result.tokens_used, 4);
138
+ }
139
+
140
+ #[tokio::test]
141
+ async fn test_infer_propagates_backend_unreachable_error() {
142
+ // Nothing listening -- must return a real error, not silently
143
+ // succeed or hang, since this is meant to replace the old stub that
144
+ // always errored with "not configured".
145
+ let backend = VLlmBackend::new("http://127.0.0.1:1".to_string());
146
+ let request = InferenceRequest {
147
+ model: "model".to_string(),
148
+ prompt: "Hi".to_string(),
149
+ max_tokens: 10,
150
+ };
151
+
152
+ let result = backend.infer(request).await;
153
+ assert!(result.is_err());
154
+ }
155
+ }
@@ -3,9 +3,11 @@ pub mod gemini_client;
3
3
  pub mod ollama_client;
4
4
  pub mod openai_client;
5
5
  pub mod provider_health;
6
+ pub mod vllm_client;
6
7
 
7
8
  pub use cloud_client::CloudClient;
8
9
  pub use gemini_client::GeminiClient;
9
10
  pub use ollama_client::OllamaClient;
10
11
  pub use openai_client::OpenAIClient;
11
12
  pub use provider_health::{ProviderHealth, ProviderHealthMetrics, ProviderStatus};
13
+ pub use vllm_client::VLlmClient;