code-puppy 0.0.142__py3-none-any.whl → 0.0.144__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.
@@ -8,6 +8,7 @@ import httpx
8
8
  from anthropic import AsyncAnthropic
9
9
  from openai import AsyncAzureOpenAI # For Azure OpenAI client
10
10
  from pydantic_ai.models.anthropic import AnthropicModel
11
+ from pydantic_ai.models.fallback import infer_model
11
12
  from pydantic_ai.models.gemini import GeminiModel
12
13
  from pydantic_ai.models.openai import OpenAIChatModel
13
14
  from pydantic_ai.providers.anthropic import AnthropicProvider
@@ -18,6 +19,7 @@ from pydantic_ai.providers.cerebras import CerebrasProvider
18
19
  from . import callbacks
19
20
  from .config import EXTRA_MODELS_FILE
20
21
  from .http_utils import create_async_client
22
+ from .round_robin_model import RoundRobinModel
21
23
 
22
24
  # Environment variables used in this module:
23
25
  # - GEMINI_API_KEY: API key for Google's Gemini models. Required when using Gemini models.
@@ -246,5 +248,22 @@ class ModelFactory:
246
248
  model = OpenAIChatModel(model_name=model_config["name"], provider=provider)
247
249
  setattr(model, "provider", provider)
248
250
  return model
251
+
252
+ elif model_type == "round_robin":
253
+ # Get the list of model names to use in the round-robin
254
+ model_names = model_config.get("models")
255
+ if not model_names or not isinstance(model_names, list):
256
+ raise ValueError(f"Round-robin model '{model_name}' requires a 'models' list in its configuration.")
257
+
258
+ # Resolve each model name to an actual model instance
259
+ models = []
260
+ for name in model_names:
261
+ # Recursively get each model using the factory
262
+ model = ModelFactory.get_model(name, config)
263
+ models.append(model)
264
+
265
+ # Create and return the round-robin model
266
+ return RoundRobinModel(*models)
267
+
249
268
  else:
250
269
  raise ValueError(f"Unsupported model type: {model_type}")
@@ -0,0 +1,115 @@
1
+ from contextlib import asynccontextmanager, suppress
2
+ from dataclasses import dataclass, field
3
+ from typing import Any, Callable, AsyncIterator, List
4
+
5
+ from pydantic_ai.models import Model, ModelMessage, ModelSettings, ModelRequestParameters, ModelResponse, StreamedResponse
6
+ from pydantic_ai.models.fallback import KnownModelName, infer_model, merge_model_settings
7
+ from pydantic_ai.result import RunContext
8
+
9
+ try:
10
+ from opentelemetry.context import get_current_span
11
+ except ImportError:
12
+ # If opentelemetry is not installed, provide a dummy implementation
13
+ def get_current_span():
14
+ class DummySpan:
15
+ def is_recording(self):
16
+ return False
17
+ def set_attributes(self, attributes):
18
+ pass
19
+ return DummySpan()
20
+
21
+ @dataclass(init=False)
22
+ class RoundRobinModel(Model):
23
+ """A model that cycles through multiple models in a round-robin fashion.
24
+
25
+ This model distributes requests across multiple candidate models to help
26
+ overcome rate limits or distribute load.
27
+ """
28
+
29
+ models: List[Model]
30
+ _current_index: int = field(default=0, repr=False)
31
+ _model_name: str = field(repr=False)
32
+
33
+ def __init__(
34
+ self,
35
+ *models: Model | KnownModelName | str,
36
+ ):
37
+ """Initialize a round-robin model instance.
38
+
39
+ Args:
40
+ models: The names or instances of models to cycle through.
41
+ """
42
+ super().__init__()
43
+ if not models:
44
+ raise ValueError("At least one model must be provided")
45
+ self.models = [infer_model(m) for m in models]
46
+ self._current_index = 0
47
+
48
+ @property
49
+ def model_name(self) -> str:
50
+ """The model name showing this is a round-robin model with its candidates."""
51
+ return f'round_robin:{",".join(model.model_name for model in self.models)}'
52
+
53
+ @property
54
+ def system(self) -> str:
55
+ """System prompt from the current model."""
56
+ return self.models[self._current_index].system
57
+
58
+ @property
59
+ def base_url(self) -> str | None:
60
+ """Base URL from the current model."""
61
+ return self.models[self._current_index].base_url
62
+
63
+ def _get_next_model(self) -> Model:
64
+ """Get the next model in the round-robin sequence and update the index."""
65
+ model = self.models[self._current_index]
66
+ self._current_index = (self._current_index + 1) % len(self.models)
67
+ return model
68
+
69
+ async def request(
70
+ self,
71
+ messages: list[ModelMessage],
72
+ model_settings: ModelSettings | None,
73
+ model_request_parameters: ModelRequestParameters,
74
+ ) -> ModelResponse:
75
+ """Make a request using the next model in the round-robin sequence."""
76
+ current_model = self._get_next_model()
77
+ merged_settings = merge_model_settings(current_model.settings, model_settings)
78
+ customized_model_request_parameters = current_model.customize_request_parameters(model_request_parameters)
79
+
80
+ try:
81
+ response = await current_model.request(messages, merged_settings, customized_model_request_parameters)
82
+ self._set_span_attributes(current_model)
83
+ return response
84
+ except Exception as exc:
85
+ # Unlike FallbackModel, we don't try other models here
86
+ # The round-robin strategy is about distribution, not failover
87
+ raise exc
88
+
89
+ @asynccontextmanager
90
+ async def request_stream(
91
+ self,
92
+ messages: list[ModelMessage],
93
+ model_settings: ModelSettings | None,
94
+ model_request_parameters: ModelRequestParameters,
95
+ run_context: RunContext[Any] | None = None,
96
+ ) -> AsyncIterator[StreamedResponse]:
97
+ """Make a streaming request using the next model in the round-robin sequence."""
98
+ current_model = self._get_next_model()
99
+ merged_settings = merge_model_settings(current_model.settings, model_settings)
100
+ customized_model_request_parameters = current_model.customize_request_parameters(model_request_parameters)
101
+
102
+ async with current_model.request_stream(
103
+ messages, merged_settings, customized_model_request_parameters, run_context
104
+ ) as response:
105
+ self._set_span_attributes(current_model)
106
+ yield response
107
+
108
+ def _set_span_attributes(self, model: Model):
109
+ """Set span attributes for observability."""
110
+ with suppress(Exception):
111
+ span = get_current_span()
112
+ if span.is_recording():
113
+ attributes = getattr(span, 'attributes', {})
114
+ if attributes.get('gen_ai.request.model') == self.model_name:
115
+ span.set_attributes(model.model_attributes(model))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-puppy
3
- Version: 0.0.142
3
+ Version: 0.0.144
4
4
  Summary: Code generation agent
5
5
  Project-URL: repository, https://github.com/mpfaffenberger/code_puppy
6
6
  Project-URL: HomePage, https://github.com/mpfaffenberger/code_puppy
@@ -189,6 +189,68 @@ If you need to run more exotic setups or connect to remote MCPs, just update you
189
189
 
190
190
  ---
191
191
 
192
+ ## Round Robin Model Distribution
193
+
194
+ Code Puppy supports **Round Robin model distribution** to help you overcome rate limits and distribute load across multiple AI models. This feature automatically cycles through configured models with each request, maximizing your API usage while staying within rate limits.
195
+
196
+ ### Configuration
197
+ Add a round-robin model configuration to your `extra_models.json` file:
198
+
199
+ ```bash
200
+ export CEREBRAS_API_KEY1=csk-...
201
+ export CEREBRAS_API_KEY2=csk-...
202
+ export CEREBRAS_API_KEY3=csk-...
203
+
204
+ ```
205
+
206
+ ```json
207
+ {
208
+ "qwen1": {
209
+ "type": "cerebras",
210
+ "name": "qwen-3-coder-480b",
211
+ "custom_endpoint": {
212
+ "url": "https://api.cerebras.ai/v1",
213
+ "api_key": "$CEREBRAS_API_KEY1"
214
+ },
215
+ "context_length": 131072
216
+ },
217
+ "qwen2": {
218
+ "type": "cerebras",
219
+ "name": "qwen-3-coder-480b",
220
+ "custom_endpoint": {
221
+ "url": "https://api.cerebras.ai/v1",
222
+ "api_key": "$CEREBRAS_API_KEY2"
223
+ },
224
+ "context_length": 131072
225
+ },
226
+ "qwen3": {
227
+ "type": "cerebras",
228
+ "name": "qwen-3-coder-480b",
229
+ "custom_endpoint": {
230
+ "url": "https://api.cerebras.ai/v1",
231
+ "api_key": "$CEREBRAS_API_KEY3"
232
+ },
233
+ "context_length": 131072
234
+ },
235
+ "cerebras_round_robin": {
236
+ "type": "round_robin",
237
+ "models": ["qwen1", "qwen2", "qwen3"]
238
+ }
239
+ }
240
+ ```
241
+
242
+ Then just use /model and tab to select your round-robin model!
243
+
244
+ ### Benefits
245
+ - **Rate Limit Protection**: Automatically distribute requests across multiple models
246
+ - **Load Balancing**: Share workload between different model providers
247
+ - **Fallback Resilience**: Continue working even if one model has temporary issues
248
+ - **Cost Optimization**: Use different models for different types of tasks
249
+
250
+ **NOTE:** Unlike fallback models, round-robin models distribute load but don't automatically retry with another model on failure. If a request fails, it will raise the exception directly.
251
+
252
+ ---
253
+
192
254
  ## Create your own Agent!!!
193
255
 
194
256
  Code Puppy features a flexible agent system that allows you to work with specialized AI assistants tailored for different coding tasks. The system supports both built-in Python agents and custom JSON agents that you can create yourself.
@@ -6,9 +6,10 @@ code_puppy/config.py,sha256=9yWKHKjLJ2Ddl4frrBI9VRIwPvoWpIx1fAd1YpAvOSQ,15330
6
6
  code_puppy/http_utils.py,sha256=BAvt4hed7fVMXglA7eS9gOb08h2YTuOyai6VmQq09fg,3432
7
7
  code_puppy/main.py,sha256=Vv5HSJnkgZhCvvOoXrJ2zqM5P-i47-RcYAU00Z1Pfx0,21733
8
8
  code_puppy/message_history_processor.py,sha256=O2rKp7W6YeIg93W8b0XySTUEQgIZm0f_06--_kzHugM,16145
9
- code_puppy/model_factory.py,sha256=NoG9wDTosaaDrFIGtq3oq8gDe0J_7N6CUKuesXz87qM,10878
9
+ code_puppy/model_factory.py,sha256=kTVaHNm6S1cLw6vHE6kH0WS6JZLRoZ8qFGKCp_fdDM4,11756
10
10
  code_puppy/models.json,sha256=dAfpMMI2EEeOMv0ynHSmMuJAYDLcZrs5gCLX3voC4-A,3252
11
11
  code_puppy/reopenable_async_client.py,sha256=4UJRaMp5np8cbef9F0zKQ7TPKOfyf5U-Kv-0zYUWDho,8274
12
+ code_puppy/round_robin_model.py,sha256=DmbO1_SIWevdhb9nN1eNVh0dNIF-XzLYX-9gra5xVsY,4670
12
13
  code_puppy/state_management.py,sha256=o4mNBCPblRyVrNBH-992-1YqffgH6AKHU7iZRqgP1LI,5925
13
14
  code_puppy/status_display.py,sha256=F6eEAkGePDp4StM2BWj-uLLQTDGtJrf0IufzCeP1rRg,8336
14
15
  code_puppy/summarization_agent.py,sha256=-e6yUGZ22ahSaF0y7QhgVcQBfx5ktNUkPxBIWQfPaA4,3275
@@ -125,9 +126,9 @@ code_puppy/tui/tests/test_sidebar_history_navigation.py,sha256=JGiyua8A2B8dLfwiE
125
126
  code_puppy/tui/tests/test_status_bar.py,sha256=nYT_FZGdmqnnbn6o0ZuOkLtNUtJzLSmtX8P72liQ5Vo,1797
126
127
  code_puppy/tui/tests/test_timestamped_history.py,sha256=nVXt9hExZZ_8MFP-AZj4L4bB_1Eo_mc-ZhVICzTuw3I,1799
127
128
  code_puppy/tui/tests/test_tools.py,sha256=kgzzAkK4r0DPzQwHHD4cePpVNgrHor6cFr05Pg6DBWg,2687
128
- code_puppy-0.0.142.data/data/code_puppy/models.json,sha256=dAfpMMI2EEeOMv0ynHSmMuJAYDLcZrs5gCLX3voC4-A,3252
129
- code_puppy-0.0.142.dist-info/METADATA,sha256=NbPyMrnJMdLPcVGsRVp6EF9rvRyWDRi7G7kDfRQrmJI,19873
130
- code_puppy-0.0.142.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
131
- code_puppy-0.0.142.dist-info/entry_points.txt,sha256=d8YkBvIUxF-dHNJAj-x4fPEqizbY5d_TwvYpc01U5kw,58
132
- code_puppy-0.0.142.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
133
- code_puppy-0.0.142.dist-info/RECORD,,
129
+ code_puppy-0.0.144.data/data/code_puppy/models.json,sha256=dAfpMMI2EEeOMv0ynHSmMuJAYDLcZrs5gCLX3voC4-A,3252
130
+ code_puppy-0.0.144.dist-info/METADATA,sha256=86kIwQ2Vf9hFT7PL6NBbHaMZGBzJ6L-CtXVn3IXULk0,21743
131
+ code_puppy-0.0.144.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
132
+ code_puppy-0.0.144.dist-info/entry_points.txt,sha256=d8YkBvIUxF-dHNJAj-x4fPEqizbY5d_TwvYpc01U5kw,58
133
+ code_puppy-0.0.144.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
134
+ code_puppy-0.0.144.dist-info/RECORD,,