bridgekit 0.3.10__tar.gz → 0.3.11__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 (31) hide show
  1. {bridgekit-0.3.10/bridgekit.egg-info → bridgekit-0.3.11}/PKG-INFO +32 -2
  2. bridgekit-0.3.10/PKG-INFO → bridgekit-0.3.11/README.md +27 -33
  3. {bridgekit-0.3.10 → bridgekit-0.3.11}/bridgekit/config.py +32 -3
  4. {bridgekit-0.3.10 → bridgekit-0.3.11}/bridgekit/providers.py +40 -3
  5. bridgekit-0.3.10/README.md → bridgekit-0.3.11/bridgekit.egg-info/PKG-INFO +63 -1
  6. {bridgekit-0.3.10 → bridgekit-0.3.11}/bridgekit.egg-info/requires.txt +6 -0
  7. {bridgekit-0.3.10 → bridgekit-0.3.11}/pyproject.toml +3 -1
  8. bridgekit-0.3.11/tests/test_providers.py +214 -0
  9. bridgekit-0.3.10/tests/test_providers.py +0 -71
  10. {bridgekit-0.3.10 → bridgekit-0.3.11}/LICENSE +0 -0
  11. {bridgekit-0.3.10 → bridgekit-0.3.11}/bridgekit/__init__.py +0 -0
  12. {bridgekit-0.3.10 → bridgekit-0.3.11}/bridgekit/cli.py +0 -0
  13. {bridgekit-0.3.10 → bridgekit-0.3.11}/bridgekit/compare.py +0 -0
  14. {bridgekit-0.3.10 → bridgekit-0.3.11}/bridgekit/planner.py +0 -0
  15. {bridgekit-0.3.10 → bridgekit-0.3.11}/bridgekit/redteam.py +0 -0
  16. {bridgekit-0.3.10 → bridgekit-0.3.11}/bridgekit/reviewer.py +0 -0
  17. {bridgekit-0.3.10 → bridgekit-0.3.11}/bridgekit/search.py +0 -0
  18. {bridgekit-0.3.10 → bridgekit-0.3.11}/bridgekit/summarize.py +0 -0
  19. {bridgekit-0.3.10 → bridgekit-0.3.11}/bridgekit.egg-info/SOURCES.txt +0 -0
  20. {bridgekit-0.3.10 → bridgekit-0.3.11}/bridgekit.egg-info/dependency_links.txt +0 -0
  21. {bridgekit-0.3.10 → bridgekit-0.3.11}/bridgekit.egg-info/entry_points.txt +0 -0
  22. {bridgekit-0.3.10 → bridgekit-0.3.11}/bridgekit.egg-info/top_level.txt +0 -0
  23. {bridgekit-0.3.10 → bridgekit-0.3.11}/setup.cfg +0 -0
  24. {bridgekit-0.3.10 → bridgekit-0.3.11}/tests/test_cli.py +0 -0
  25. {bridgekit-0.3.10 → bridgekit-0.3.11}/tests/test_compare.py +0 -0
  26. {bridgekit-0.3.10 → bridgekit-0.3.11}/tests/test_config.py +0 -0
  27. {bridgekit-0.3.10 → bridgekit-0.3.11}/tests/test_planner.py +0 -0
  28. {bridgekit-0.3.10 → bridgekit-0.3.11}/tests/test_redteam.py +0 -0
  29. {bridgekit-0.3.10 → bridgekit-0.3.11}/tests/test_reviewer.py +0 -0
  30. {bridgekit-0.3.10 → bridgekit-0.3.11}/tests/test_search.py +0 -0
  31. {bridgekit-0.3.10 → bridgekit-0.3.11}/tests/test_summarize.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bridgekit
3
- Version: 0.3.10
3
+ Version: 0.3.11
4
4
  Summary: AI tools that make you a better data scientist, not a redundant one.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://usebridgekit.com
@@ -25,6 +25,10 @@ Provides-Extra: openai
25
25
  Requires-Dist: openai>=1.0.0; extra == "openai"
26
26
  Provides-Extra: gemini
27
27
  Requires-Dist: google-generativeai>=0.3.0; extra == "gemini"
28
+ Provides-Extra: ollama
29
+ Requires-Dist: ollama>=0.3.0; extra == "ollama"
30
+ Provides-Extra: openrouter
31
+ Requires-Dist: openai>=1.0.0; extra == "openrouter"
28
32
  Provides-Extra: dev
29
33
  Requires-Dist: pytest>=7.0.0; extra == "dev"
30
34
  Requires-Dist: pytest-mock>=3.0.0; extra == "dev"
@@ -87,6 +91,18 @@ pip install bridgekit[gemini]
87
91
  export GOOGLE_API_KEY=your_key_here
88
92
  ```
89
93
 
94
+ **Ollama (local models, free, no API key):**
95
+ ```bash
96
+ pip install bridgekit[ollama]
97
+ # Make sure Ollama is running locally: https://ollama.com
98
+ ```
99
+
100
+ **OpenRouter (free and paid models via one API):**
101
+ ```bash
102
+ pip install bridgekit[openrouter]
103
+ export OPENROUTER_API_KEY=your_key_here
104
+ ```
105
+
90
106
  ---
91
107
 
92
108
  ## Getting Started
@@ -528,7 +544,7 @@ high-confidence growth lever worth testing immediately.
528
544
 
529
545
  ## Multi-Provider Support
530
546
 
531
- Bridgekit now supports multiple AI providers so you're not locked into one API. You can use Anthropic, OpenAI, or Google Gemini models with any tool.
547
+ Bridgekit now supports multiple AI providers so you're not locked into one API. You can use Anthropic, OpenAI, Google Gemini, local models via Ollama, or any model on OpenRouter with any tool.
532
548
 
533
549
  **Using different providers:**
534
550
 
@@ -541,21 +557,35 @@ print(evaluate("Your analysis here", provider="openai"))
541
557
  # Use Google Gemini (default model: gemini-1.5-pro)
542
558
  print(plan("Your question here", provider="gemini"))
543
559
 
560
+ # Use a local model via Ollama - free, private, no API key (default model: llama3.2)
561
+ print(evaluate("Your analysis here", provider="ollama"))
562
+
563
+ # Use OpenRouter to access a wide range of models, including free tiers
564
+ print(evaluate("Your analysis here", provider="openrouter", model="deepseek/deepseek-v4-flash-0731:free"))
565
+
544
566
  # Use specific model
545
567
  print(redteam("Your analysis here", model="gpt-4-turbo"))
546
568
  print(ask("Your question here", source="reports/", model="claude-3-opus-20240229"))
547
569
  ```
548
570
 
571
+ **Ollama setup:** install and start [Ollama](https://ollama.com) locally, then pull a model (e.g. `ollama pull llama3.2`). By default Bridgekit connects to `http://localhost:11434`; set `OLLAMA_HOST` to point at a different host.
572
+
549
573
  **Provider auto-detection:**
550
574
  Bridgekit automatically detects the provider from model names:
551
575
  - Models starting with "claude" → Anthropic
552
576
  - Models starting with "gpt" → OpenAI
553
577
  - Models starting with "gemini" → Google Gemini
578
+ - Model names containing "/" (e.g. `deepseek/deepseek-v4-flash-0731:free`) → OpenRouter
579
+ - Common local model families (e.g. "llama", "mistral", "mixtral", "gemma", "phi", "qwen") → Ollama
554
580
 
555
581
  **Default models by provider:**
556
582
  - Anthropic: `claude-opus-4-8`
557
583
  - OpenAI: `gpt-4o`
558
584
  - Gemini: `gemini-1.5-pro`
585
+ - Ollama: `llama3.2`
586
+ - OpenRouter: `deepseek/deepseek-v4-flash-0731:free`
587
+
588
+ > **Note:** OpenRouter's free-tier model lineup rotates over time. Check the current list at [openrouter.ai/models?max_price=0](https://openrouter.ai/models?max_price=0) if a `:free` model stops working, and pass `model=` explicitly with whichever slug is currently free.
559
589
 
560
590
  All tools support the same `provider` and `model` parameters:
561
591
  - `evaluate(text, provider=None, model=None, system_prompt=None)`
@@ -1,35 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: bridgekit
3
- Version: 0.3.10
4
- Summary: AI tools that make you a better data scientist, not a redundant one.
5
- License: MIT
6
- Project-URL: Homepage, https://usebridgekit.com
7
- Project-URL: Issues, https://github.com/getbridgekit/bridgekit/issues
8
- Keywords: data science,AI,analysis,evaluation,anthropic
9
- Classifier: Development Status :: 3 - Alpha
10
- Classifier: Intended Audience :: Science/Research
11
- Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
12
- Classifier: Programming Language :: Python :: 3
13
- Classifier: License :: OSI Approved :: MIT License
14
- Requires-Python: >=3.9
15
- Description-Content-Type: text/markdown
16
- License-File: LICENSE
17
- Requires-Dist: anthropic>=0.20.0
18
- Requires-Dist: chromadb>=0.4.0
19
- Requires-Dist: sentence-transformers>=2.0.0
20
- Requires-Dist: pypdf>=3.0.0
21
- Requires-Dist: python-docx>=1.0.0
22
- Requires-Dist: python-pptx>=0.6.0
23
- Requires-Dist: nbformat>=5.0.0
24
- Provides-Extra: openai
25
- Requires-Dist: openai>=1.0.0; extra == "openai"
26
- Provides-Extra: gemini
27
- Requires-Dist: google-generativeai>=0.3.0; extra == "gemini"
28
- Provides-Extra: dev
29
- Requires-Dist: pytest>=7.0.0; extra == "dev"
30
- Requires-Dist: pytest-mock>=3.0.0; extra == "dev"
31
- Dynamic: license-file
32
-
33
1
  <p align="center">
34
2
  <img src="https://raw.githubusercontent.com/getbridgekit/bridgekit/main/assets/logo.png" alt="Bridgekit" width="200"/>
35
3
  </p>
@@ -87,6 +55,18 @@ pip install bridgekit[gemini]
87
55
  export GOOGLE_API_KEY=your_key_here
88
56
  ```
89
57
 
58
+ **Ollama (local models, free, no API key):**
59
+ ```bash
60
+ pip install bridgekit[ollama]
61
+ # Make sure Ollama is running locally: https://ollama.com
62
+ ```
63
+
64
+ **OpenRouter (free and paid models via one API):**
65
+ ```bash
66
+ pip install bridgekit[openrouter]
67
+ export OPENROUTER_API_KEY=your_key_here
68
+ ```
69
+
90
70
  ---
91
71
 
92
72
  ## Getting Started
@@ -528,7 +508,7 @@ high-confidence growth lever worth testing immediately.
528
508
 
529
509
  ## Multi-Provider Support
530
510
 
531
- Bridgekit now supports multiple AI providers so you're not locked into one API. You can use Anthropic, OpenAI, or Google Gemini models with any tool.
511
+ Bridgekit now supports multiple AI providers so you're not locked into one API. You can use Anthropic, OpenAI, Google Gemini, local models via Ollama, or any model on OpenRouter with any tool.
532
512
 
533
513
  **Using different providers:**
534
514
 
@@ -541,21 +521,35 @@ print(evaluate("Your analysis here", provider="openai"))
541
521
  # Use Google Gemini (default model: gemini-1.5-pro)
542
522
  print(plan("Your question here", provider="gemini"))
543
523
 
524
+ # Use a local model via Ollama - free, private, no API key (default model: llama3.2)
525
+ print(evaluate("Your analysis here", provider="ollama"))
526
+
527
+ # Use OpenRouter to access a wide range of models, including free tiers
528
+ print(evaluate("Your analysis here", provider="openrouter", model="deepseek/deepseek-v4-flash-0731:free"))
529
+
544
530
  # Use specific model
545
531
  print(redteam("Your analysis here", model="gpt-4-turbo"))
546
532
  print(ask("Your question here", source="reports/", model="claude-3-opus-20240229"))
547
533
  ```
548
534
 
535
+ **Ollama setup:** install and start [Ollama](https://ollama.com) locally, then pull a model (e.g. `ollama pull llama3.2`). By default Bridgekit connects to `http://localhost:11434`; set `OLLAMA_HOST` to point at a different host.
536
+
549
537
  **Provider auto-detection:**
550
538
  Bridgekit automatically detects the provider from model names:
551
539
  - Models starting with "claude" → Anthropic
552
540
  - Models starting with "gpt" → OpenAI
553
541
  - Models starting with "gemini" → Google Gemini
542
+ - Model names containing "/" (e.g. `deepseek/deepseek-v4-flash-0731:free`) → OpenRouter
543
+ - Common local model families (e.g. "llama", "mistral", "mixtral", "gemma", "phi", "qwen") → Ollama
554
544
 
555
545
  **Default models by provider:**
556
546
  - Anthropic: `claude-opus-4-8`
557
547
  - OpenAI: `gpt-4o`
558
548
  - Gemini: `gemini-1.5-pro`
549
+ - Ollama: `llama3.2`
550
+ - OpenRouter: `deepseek/deepseek-v4-flash-0731:free`
551
+
552
+ > **Note:** OpenRouter's free-tier model lineup rotates over time. Check the current list at [openrouter.ai/models?max_price=0](https://openrouter.ai/models?max_price=0) if a `:free` model stops working, and pass `model=` explicitly with whichever slug is currently free.
559
553
 
560
554
  All tools support the same `provider` and `model` parameters:
561
555
  - `evaluate(text, provider=None, model=None, system_prompt=None)`
@@ -7,25 +7,39 @@ class Provider(Enum):
7
7
  ANTHROPIC = "anthropic"
8
8
  OPENAI = "openai"
9
9
  GEMINI = "gemini"
10
+ OLLAMA = "ollama"
11
+ OPENROUTER = "openrouter"
10
12
 
11
13
 
12
14
  # Default models for each provider
13
15
  DEFAULT_MODELS = {
14
16
  Provider.ANTHROPIC: "claude-opus-4-8",
15
17
  Provider.OPENAI: "gpt-4o",
16
- Provider.GEMINI: "gemini-1.5-pro"
18
+ Provider.GEMINI: "gemini-1.5-pro",
19
+ Provider.OLLAMA: "llama3.2",
20
+ Provider.OPENROUTER: "deepseek/deepseek-v4-flash-0731:free",
17
21
  }
18
22
 
19
23
  # Legacy support
20
24
  DEFAULT_MODEL = DEFAULT_MODELS[Provider.ANTHROPIC]
21
25
 
26
+ # Common local model family names served by Ollama. Used to infer the
27
+ # provider from a bare model name when no explicit provider is given.
28
+ OLLAMA_MODEL_PREFIXES = (
29
+ "llama", "mistral", "mixtral", "gemma", "phi", "qwen",
30
+ "codellama", "vicuna", "deepseek", "tinyllama",
31
+ )
22
32
 
23
- def require_api_key(provider: Provider = Provider.ANTHROPIC) -> str:
33
+
34
+ def require_api_key(provider: Provider = Provider.ANTHROPIC) -> Optional[str]:
24
35
  """Return the API key for the specified provider from the environment, or raise a clear error.
25
36
 
26
37
  Each Bridgekit tool calls this before constructing a client so
27
38
  users get the same friendly message instead of whatever the SDK surfaces
28
39
  when the key is missing.
40
+
41
+ Ollama runs locally and doesn't use an API key, so this returns None
42
+ for that provider instead of raising.
29
43
  """
30
44
  if provider == Provider.ANTHROPIC:
31
45
  api_key = os.environ.get("ANTHROPIC_API_KEY")
@@ -48,6 +62,15 @@ def require_api_key(provider: Provider = Provider.ANTHROPIC) -> str:
48
62
  "GOOGLE_API_KEY not found. Set it with: export GOOGLE_API_KEY=your_key_here"
49
63
  )
50
64
  return api_key
65
+ elif provider == Provider.OLLAMA:
66
+ return None
67
+ elif provider == Provider.OPENROUTER:
68
+ api_key = os.environ.get("OPENROUTER_API_KEY")
69
+ if not api_key:
70
+ raise EnvironmentError(
71
+ "OPENROUTER_API_KEY not found. Set it with: export OPENROUTER_API_KEY=your_key_here"
72
+ )
73
+ return api_key
51
74
  else:
52
75
  raise ValueError(f"Unsupported provider: {provider}")
53
76
 
@@ -73,7 +96,13 @@ def parse_provider(provider: Optional[str] = None, model: Optional[str] = None)
73
96
  return Provider.OPENAI
74
97
  elif model.startswith("gemini"):
75
98
  return Provider.GEMINI
76
-
99
+ elif "/" in model:
100
+ # OpenRouter model names use a "vendor/model" format,
101
+ # e.g. "deepseek/deepseek-v4-flash-0731:free"
102
+ return Provider.OPENROUTER
103
+ elif model.startswith(OLLAMA_MODEL_PREFIXES):
104
+ return Provider.OLLAMA
105
+
77
106
  # Default to Anthropic for backward compatibility
78
107
  return Provider.ANTHROPIC
79
108
 
@@ -1,5 +1,6 @@
1
1
  """Provider client factory for Bridgekit multi-provider support."""
2
2
 
3
+ import os
3
4
  from typing import Union, Dict, Any, List
4
5
  from .config import Provider, require_api_key, get_default_model
5
6
 
@@ -39,11 +40,14 @@ class AnthropicClient(BaseProviderClient):
39
40
 
40
41
  class OpenAIClient(BaseProviderClient):
41
42
  """OpenAI provider client."""
42
-
43
- def __init__(self, api_key: str):
43
+
44
+ def __init__(self, api_key: str, base_url: str = None):
44
45
  super().__init__(api_key)
45
46
  import openai
46
- self.client = openai.OpenAI(api_key=api_key)
47
+ if base_url:
48
+ self.client = openai.OpenAI(api_key=api_key, base_url=base_url)
49
+ else:
50
+ self.client = openai.OpenAI(api_key=api_key)
47
51
 
48
52
  def create_message(self, system_prompt: str, user_message: str, model: str, max_tokens: int = 1024) -> str:
49
53
  """Create a message using OpenAI's API."""
@@ -86,6 +90,35 @@ class GeminiClient(BaseProviderClient):
86
90
  return response.text
87
91
 
88
92
 
93
+ class OllamaClient(BaseProviderClient):
94
+ """Ollama provider client for locally-hosted models. No API key required."""
95
+
96
+ def __init__(self, api_key: str = None):
97
+ super().__init__(api_key)
98
+ import ollama
99
+ host = os.environ.get("OLLAMA_HOST", "http://localhost:11434")
100
+ self.client = ollama.Client(host=host)
101
+
102
+ def create_message(self, system_prompt: str, user_message: str, model: str, max_tokens: int = 1024) -> str:
103
+ """Create a message using a locally-hosted Ollama model."""
104
+ response = self.client.chat(
105
+ model=model,
106
+ messages=[
107
+ {"role": "system", "content": system_prompt},
108
+ {"role": "user", "content": user_message}
109
+ ],
110
+ options={"num_predict": max_tokens}
111
+ )
112
+ return response["message"]["content"]
113
+
114
+
115
+ class OpenRouterClient(OpenAIClient):
116
+ """OpenRouter provider client. OpenAI-compatible API routing to many models, including free tiers."""
117
+
118
+ def __init__(self, api_key: str):
119
+ super().__init__(api_key, base_url="https://openrouter.ai/api/v1")
120
+
121
+
89
122
  def create_client(provider: Provider, model: str = None) -> BaseProviderClient:
90
123
  """Create a client for the specified provider."""
91
124
  if model is None:
@@ -99,6 +132,10 @@ def create_client(provider: Provider, model: str = None) -> BaseProviderClient:
99
132
  return OpenAIClient(api_key)
100
133
  elif provider == Provider.GEMINI:
101
134
  return GeminiClient(api_key)
135
+ elif provider == Provider.OLLAMA:
136
+ return OllamaClient(api_key)
137
+ elif provider == Provider.OPENROUTER:
138
+ return OpenRouterClient(api_key)
102
139
  else:
103
140
  raise ValueError(f"Unsupported provider: {provider}")
104
141
 
@@ -1,3 +1,39 @@
1
+ Metadata-Version: 2.4
2
+ Name: bridgekit
3
+ Version: 0.3.11
4
+ Summary: AI tools that make you a better data scientist, not a redundant one.
5
+ License: MIT
6
+ Project-URL: Homepage, https://usebridgekit.com
7
+ Project-URL: Issues, https://github.com/getbridgekit/bridgekit/issues
8
+ Keywords: data science,AI,analysis,evaluation,anthropic
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: anthropic>=0.20.0
18
+ Requires-Dist: chromadb>=0.4.0
19
+ Requires-Dist: sentence-transformers>=2.0.0
20
+ Requires-Dist: pypdf>=3.0.0
21
+ Requires-Dist: python-docx>=1.0.0
22
+ Requires-Dist: python-pptx>=0.6.0
23
+ Requires-Dist: nbformat>=5.0.0
24
+ Provides-Extra: openai
25
+ Requires-Dist: openai>=1.0.0; extra == "openai"
26
+ Provides-Extra: gemini
27
+ Requires-Dist: google-generativeai>=0.3.0; extra == "gemini"
28
+ Provides-Extra: ollama
29
+ Requires-Dist: ollama>=0.3.0; extra == "ollama"
30
+ Provides-Extra: openrouter
31
+ Requires-Dist: openai>=1.0.0; extra == "openrouter"
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
34
+ Requires-Dist: pytest-mock>=3.0.0; extra == "dev"
35
+ Dynamic: license-file
36
+
1
37
  <p align="center">
2
38
  <img src="https://raw.githubusercontent.com/getbridgekit/bridgekit/main/assets/logo.png" alt="Bridgekit" width="200"/>
3
39
  </p>
@@ -55,6 +91,18 @@ pip install bridgekit[gemini]
55
91
  export GOOGLE_API_KEY=your_key_here
56
92
  ```
57
93
 
94
+ **Ollama (local models, free, no API key):**
95
+ ```bash
96
+ pip install bridgekit[ollama]
97
+ # Make sure Ollama is running locally: https://ollama.com
98
+ ```
99
+
100
+ **OpenRouter (free and paid models via one API):**
101
+ ```bash
102
+ pip install bridgekit[openrouter]
103
+ export OPENROUTER_API_KEY=your_key_here
104
+ ```
105
+
58
106
  ---
59
107
 
60
108
  ## Getting Started
@@ -496,7 +544,7 @@ high-confidence growth lever worth testing immediately.
496
544
 
497
545
  ## Multi-Provider Support
498
546
 
499
- Bridgekit now supports multiple AI providers so you're not locked into one API. You can use Anthropic, OpenAI, or Google Gemini models with any tool.
547
+ Bridgekit now supports multiple AI providers so you're not locked into one API. You can use Anthropic, OpenAI, Google Gemini, local models via Ollama, or any model on OpenRouter with any tool.
500
548
 
501
549
  **Using different providers:**
502
550
 
@@ -509,21 +557,35 @@ print(evaluate("Your analysis here", provider="openai"))
509
557
  # Use Google Gemini (default model: gemini-1.5-pro)
510
558
  print(plan("Your question here", provider="gemini"))
511
559
 
560
+ # Use a local model via Ollama - free, private, no API key (default model: llama3.2)
561
+ print(evaluate("Your analysis here", provider="ollama"))
562
+
563
+ # Use OpenRouter to access a wide range of models, including free tiers
564
+ print(evaluate("Your analysis here", provider="openrouter", model="deepseek/deepseek-v4-flash-0731:free"))
565
+
512
566
  # Use specific model
513
567
  print(redteam("Your analysis here", model="gpt-4-turbo"))
514
568
  print(ask("Your question here", source="reports/", model="claude-3-opus-20240229"))
515
569
  ```
516
570
 
571
+ **Ollama setup:** install and start [Ollama](https://ollama.com) locally, then pull a model (e.g. `ollama pull llama3.2`). By default Bridgekit connects to `http://localhost:11434`; set `OLLAMA_HOST` to point at a different host.
572
+
517
573
  **Provider auto-detection:**
518
574
  Bridgekit automatically detects the provider from model names:
519
575
  - Models starting with "claude" → Anthropic
520
576
  - Models starting with "gpt" → OpenAI
521
577
  - Models starting with "gemini" → Google Gemini
578
+ - Model names containing "/" (e.g. `deepseek/deepseek-v4-flash-0731:free`) → OpenRouter
579
+ - Common local model families (e.g. "llama", "mistral", "mixtral", "gemma", "phi", "qwen") → Ollama
522
580
 
523
581
  **Default models by provider:**
524
582
  - Anthropic: `claude-opus-4-8`
525
583
  - OpenAI: `gpt-4o`
526
584
  - Gemini: `gemini-1.5-pro`
585
+ - Ollama: `llama3.2`
586
+ - OpenRouter: `deepseek/deepseek-v4-flash-0731:free`
587
+
588
+ > **Note:** OpenRouter's free-tier model lineup rotates over time. Check the current list at [openrouter.ai/models?max_price=0](https://openrouter.ai/models?max_price=0) if a `:free` model stops working, and pass `model=` explicitly with whichever slug is currently free.
527
589
 
528
590
  All tools support the same `provider` and `model` parameters:
529
591
  - `evaluate(text, provider=None, model=None, system_prompt=None)`
@@ -13,5 +13,11 @@ pytest-mock>=3.0.0
13
13
  [gemini]
14
14
  google-generativeai>=0.3.0
15
15
 
16
+ [ollama]
17
+ ollama>=0.3.0
18
+
16
19
  [openai]
17
20
  openai>=1.0.0
21
+
22
+ [openrouter]
23
+ openai>=1.0.0
@@ -7,7 +7,7 @@ include = ["bridgekit*"]
7
7
 
8
8
  [project]
9
9
  name = "bridgekit"
10
- version = "0.3.10"
10
+ version = "0.3.11"
11
11
  description = "AI tools that make you a better data scientist, not a redundant one."
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.9"
@@ -33,6 +33,8 @@ dependencies = [
33
33
  [project.optional-dependencies]
34
34
  openai = ["openai>=1.0.0"]
35
35
  gemini = ["google-generativeai>=0.3.0"]
36
+ ollama = ["ollama>=0.3.0"]
37
+ openrouter = ["openai>=1.0.0"]
36
38
  dev = [
37
39
  "pytest>=7.0.0",
38
40
  "pytest-mock>=3.0.0",
@@ -0,0 +1,214 @@
1
+ import os
2
+ import sys
3
+ import types
4
+ import pytest
5
+ from unittest.mock import MagicMock, patch
6
+
7
+ from bridgekit.config import parse_provider, require_api_key, Provider
8
+ from bridgekit.providers import OllamaClient, OpenRouterClient, create_client
9
+
10
+
11
+ class TestParseProvider:
12
+ """parse_provider() returns the correct Provider enum."""
13
+
14
+ def test_explicit_anthropic(self):
15
+ assert parse_provider(provider="anthropic") == Provider.ANTHROPIC
16
+
17
+ def test_explicit_openai(self):
18
+ assert parse_provider(provider="openai") == Provider.OPENAI
19
+
20
+ def test_explicit_gemini(self):
21
+ assert parse_provider(provider="gemini") == Provider.GEMINI
22
+
23
+ def test_infer_anthropic_from_model(self):
24
+ assert parse_provider(model="claude-opus-4-6") == Provider.ANTHROPIC
25
+
26
+ def test_infer_openai_from_model(self):
27
+ assert parse_provider(model="gpt-4o") == Provider.OPENAI
28
+
29
+ def test_infer_gemini_from_model(self):
30
+ assert parse_provider(model="gemini-1.5-pro") == Provider.GEMINI
31
+
32
+ def test_infer_openrouter_from_model(self):
33
+ assert parse_provider(model="deepseek/deepseek-v4-flash-0731:free") == Provider.OPENROUTER
34
+
35
+ def test_infer_ollama_from_model(self):
36
+ assert parse_provider(model="llama3.2") == Provider.OLLAMA
37
+
38
+ def test_infer_ollama_from_mistral_model(self):
39
+ assert parse_provider(model="mistral") == Provider.OLLAMA
40
+
41
+ def test_defaults_to_anthropic(self):
42
+ assert parse_provider() == Provider.ANTHROPIC
43
+
44
+ def test_raises_on_unsupported_provider(self):
45
+ with pytest.raises(ValueError, match="Unsupported provider"):
46
+ parse_provider(provider="cohere")
47
+
48
+ def test_provider_takes_precedence_over_model(self):
49
+ assert parse_provider(provider="openai", model="claude-opus-4-6") == Provider.OPENAI
50
+
51
+ def test_explicit_ollama(self):
52
+ assert parse_provider(provider="ollama") == Provider.OLLAMA
53
+
54
+ def test_explicit_openrouter(self):
55
+ assert parse_provider(provider="openrouter") == Provider.OPENROUTER
56
+
57
+
58
+ class TestRequireApiKey:
59
+ """require_api_key() returns the correct key or raises EnvironmentError."""
60
+
61
+ def test_returns_anthropic_key(self):
62
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-ant-test"}, clear=True):
63
+ assert require_api_key(Provider.ANTHROPIC) == "sk-ant-test"
64
+
65
+ def test_returns_openai_key(self):
66
+ with patch.dict(os.environ, {"OPENAI_API_KEY": "sk-openai-test"}, clear=True):
67
+ assert require_api_key(Provider.OPENAI) == "sk-openai-test"
68
+
69
+ def test_returns_gemini_key(self):
70
+ with patch.dict(os.environ, {"GOOGLE_API_KEY": "gemini-test"}, clear=True):
71
+ assert require_api_key(Provider.GEMINI) == "gemini-test"
72
+
73
+ def test_raises_when_anthropic_key_missing(self):
74
+ env = {k: v for k, v in os.environ.items() if k != "ANTHROPIC_API_KEY"}
75
+ with patch.dict(os.environ, env, clear=True):
76
+ with pytest.raises(EnvironmentError, match="ANTHROPIC_API_KEY"):
77
+ require_api_key(Provider.ANTHROPIC)
78
+
79
+ def test_raises_when_openai_key_missing(self):
80
+ env = {k: v for k, v in os.environ.items() if k != "OPENAI_API_KEY"}
81
+ with patch.dict(os.environ, env, clear=True):
82
+ with pytest.raises(EnvironmentError, match="OPENAI_API_KEY"):
83
+ require_api_key(Provider.OPENAI)
84
+
85
+ def test_raises_when_gemini_key_missing(self):
86
+ env = {k: v for k, v in os.environ.items() if k != "GOOGLE_API_KEY"}
87
+ with patch.dict(os.environ, env, clear=True):
88
+ with pytest.raises(EnvironmentError, match="GOOGLE_API_KEY"):
89
+ require_api_key(Provider.GEMINI)
90
+
91
+ def test_returns_openrouter_key(self):
92
+ with patch.dict(os.environ, {"OPENROUTER_API_KEY": "sk-or-test"}, clear=True):
93
+ assert require_api_key(Provider.OPENROUTER) == "sk-or-test"
94
+
95
+ def test_raises_when_openrouter_key_missing(self):
96
+ env = {k: v for k, v in os.environ.items() if k != "OPENROUTER_API_KEY"}
97
+ with patch.dict(os.environ, env, clear=True):
98
+ with pytest.raises(EnvironmentError, match="OPENROUTER_API_KEY"):
99
+ require_api_key(Provider.OPENROUTER)
100
+
101
+ def test_ollama_does_not_require_a_key(self):
102
+ env = {k: v for k, v in os.environ.items() if k != "OLLAMA_API_KEY"}
103
+ with patch.dict(os.environ, env, clear=True):
104
+ assert require_api_key(Provider.OLLAMA) is None
105
+
106
+
107
+ class TestOllamaClient:
108
+ """OllamaClient talks to a local Ollama server and needs no API key."""
109
+
110
+ def _fake_ollama_module(self):
111
+ fake_module = types.ModuleType("ollama")
112
+ fake_module.Client = MagicMock()
113
+ return fake_module
114
+
115
+ def test_no_api_key_required(self):
116
+ fake_ollama = self._fake_ollama_module()
117
+ with patch.dict(sys.modules, {"ollama": fake_ollama}):
118
+ client = OllamaClient()
119
+ assert client.api_key is None
120
+
121
+ def test_uses_default_host_when_env_var_not_set(self):
122
+ fake_ollama = self._fake_ollama_module()
123
+ env = {k: v for k, v in os.environ.items() if k != "OLLAMA_HOST"}
124
+ with patch.dict(os.environ, env, clear=True), patch.dict(sys.modules, {"ollama": fake_ollama}):
125
+ OllamaClient()
126
+ fake_ollama.Client.assert_called_once_with(host="http://localhost:11434")
127
+
128
+ def test_uses_ollama_host_env_var_when_set(self):
129
+ fake_ollama = self._fake_ollama_module()
130
+ with patch.dict(os.environ, {"OLLAMA_HOST": "http://remote-box:11434"}), \
131
+ patch.dict(sys.modules, {"ollama": fake_ollama}):
132
+ OllamaClient()
133
+ fake_ollama.Client.assert_called_once_with(host="http://remote-box:11434")
134
+
135
+ def test_create_message_returns_response_content(self):
136
+ fake_ollama = self._fake_ollama_module()
137
+ mock_client_instance = MagicMock()
138
+ mock_client_instance.chat.return_value = {"message": {"content": "hello from llama"}}
139
+ fake_ollama.Client.return_value = mock_client_instance
140
+
141
+ with patch.dict(sys.modules, {"ollama": fake_ollama}):
142
+ client = OllamaClient()
143
+ result = client.create_message("system prompt", "user message", model="llama3.2", max_tokens=50)
144
+
145
+ assert result == "hello from llama"
146
+ mock_client_instance.chat.assert_called_once_with(
147
+ model="llama3.2",
148
+ messages=[
149
+ {"role": "system", "content": "system prompt"},
150
+ {"role": "user", "content": "user message"}
151
+ ],
152
+ options={"num_predict": 50}
153
+ )
154
+
155
+
156
+ def _fake_openai_module():
157
+ """A stand-in for the `openai` package so these tests don't require it
158
+ to actually be installed - it's an optional extra, and CI only installs
159
+ `.[dev]`."""
160
+ fake_module = types.ModuleType("openai")
161
+ fake_module.OpenAI = MagicMock()
162
+ return fake_module
163
+
164
+
165
+ class TestOpenRouterClient:
166
+ """OpenRouterClient reuses OpenAI's SDK pointed at OpenRouter's base URL."""
167
+
168
+ def test_uses_openrouter_base_url(self):
169
+ fake_openai = _fake_openai_module()
170
+ with patch.dict(sys.modules, {"openai": fake_openai}):
171
+ OpenRouterClient(api_key="or-test-key")
172
+ fake_openai.OpenAI.assert_called_once_with(api_key="or-test-key", base_url="https://openrouter.ai/api/v1")
173
+
174
+ def test_create_message_returns_response_content(self):
175
+ fake_openai = _fake_openai_module()
176
+ mock_response = MagicMock()
177
+ mock_response.choices[0].message.content = "response text"
178
+ mock_client_instance = MagicMock()
179
+ mock_client_instance.chat.completions.create.return_value = mock_response
180
+ fake_openai.OpenAI.return_value = mock_client_instance
181
+
182
+ with patch.dict(sys.modules, {"openai": fake_openai}):
183
+ client = OpenRouterClient(api_key="or-test-key")
184
+ result = client.create_message(
185
+ "system prompt", "user message", model="deepseek/deepseek-v4-flash-0731:free", max_tokens=100
186
+ )
187
+
188
+ assert result == "response text"
189
+ mock_client_instance.chat.completions.create.assert_called_once_with(
190
+ model="deepseek/deepseek-v4-flash-0731:free",
191
+ messages=[
192
+ {"role": "system", "content": "system prompt"},
193
+ {"role": "user", "content": "user message"}
194
+ ],
195
+ max_tokens=100
196
+ )
197
+
198
+
199
+ class TestCreateClientRouting:
200
+ """create_client() routes to the right client class for the new providers."""
201
+
202
+ def test_routes_ollama(self):
203
+ fake_ollama = types.ModuleType("ollama")
204
+ fake_ollama.Client = MagicMock()
205
+ with patch.dict(sys.modules, {"ollama": fake_ollama}):
206
+ client = create_client(Provider.OLLAMA, model="llama3.2")
207
+ assert isinstance(client, OllamaClient)
208
+
209
+ def test_routes_openrouter(self):
210
+ fake_openai = _fake_openai_module()
211
+ with patch.dict(os.environ, {"OPENROUTER_API_KEY": "sk-or-test"}, clear=True), \
212
+ patch.dict(sys.modules, {"openai": fake_openai}):
213
+ client = create_client(Provider.OPENROUTER, model="deepseek/deepseek-v4-flash-0731:free")
214
+ assert isinstance(client, OpenRouterClient)
@@ -1,71 +0,0 @@
1
- import os
2
- import pytest
3
- from unittest.mock import patch
4
-
5
- from bridgekit.config import parse_provider, require_api_key, Provider
6
-
7
-
8
- class TestParseProvider:
9
- """parse_provider() returns the correct Provider enum."""
10
-
11
- def test_explicit_anthropic(self):
12
- assert parse_provider(provider="anthropic") == Provider.ANTHROPIC
13
-
14
- def test_explicit_openai(self):
15
- assert parse_provider(provider="openai") == Provider.OPENAI
16
-
17
- def test_explicit_gemini(self):
18
- assert parse_provider(provider="gemini") == Provider.GEMINI
19
-
20
- def test_infer_anthropic_from_model(self):
21
- assert parse_provider(model="claude-opus-4-6") == Provider.ANTHROPIC
22
-
23
- def test_infer_openai_from_model(self):
24
- assert parse_provider(model="gpt-4o") == Provider.OPENAI
25
-
26
- def test_infer_gemini_from_model(self):
27
- assert parse_provider(model="gemini-1.5-pro") == Provider.GEMINI
28
-
29
- def test_defaults_to_anthropic(self):
30
- assert parse_provider() == Provider.ANTHROPIC
31
-
32
- def test_raises_on_unsupported_provider(self):
33
- with pytest.raises(ValueError, match="Unsupported provider"):
34
- parse_provider(provider="cohere")
35
-
36
- def test_provider_takes_precedence_over_model(self):
37
- assert parse_provider(provider="openai", model="claude-opus-4-6") == Provider.OPENAI
38
-
39
-
40
- class TestRequireApiKey:
41
- """require_api_key() returns the correct key or raises EnvironmentError."""
42
-
43
- def test_returns_anthropic_key(self):
44
- with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-ant-test"}, clear=True):
45
- assert require_api_key(Provider.ANTHROPIC) == "sk-ant-test"
46
-
47
- def test_returns_openai_key(self):
48
- with patch.dict(os.environ, {"OPENAI_API_KEY": "sk-openai-test"}, clear=True):
49
- assert require_api_key(Provider.OPENAI) == "sk-openai-test"
50
-
51
- def test_returns_gemini_key(self):
52
- with patch.dict(os.environ, {"GOOGLE_API_KEY": "gemini-test"}, clear=True):
53
- assert require_api_key(Provider.GEMINI) == "gemini-test"
54
-
55
- def test_raises_when_anthropic_key_missing(self):
56
- env = {k: v for k, v in os.environ.items() if k != "ANTHROPIC_API_KEY"}
57
- with patch.dict(os.environ, env, clear=True):
58
- with pytest.raises(EnvironmentError, match="ANTHROPIC_API_KEY"):
59
- require_api_key(Provider.ANTHROPIC)
60
-
61
- def test_raises_when_openai_key_missing(self):
62
- env = {k: v for k, v in os.environ.items() if k != "OPENAI_API_KEY"}
63
- with patch.dict(os.environ, env, clear=True):
64
- with pytest.raises(EnvironmentError, match="OPENAI_API_KEY"):
65
- require_api_key(Provider.OPENAI)
66
-
67
- def test_raises_when_gemini_key_missing(self):
68
- env = {k: v for k, v in os.environ.items() if k != "GOOGLE_API_KEY"}
69
- with patch.dict(os.environ, env, clear=True):
70
- with pytest.raises(EnvironmentError, match="GOOGLE_API_KEY"):
71
- require_api_key(Provider.GEMINI)
File without changes
File without changes
File without changes
File without changes