brrragent 0.4.2__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.
- brrragent/__init__.py +40 -0
- brrragent/_errors.py +7 -0
- brrragent/agent.py +385 -0
- brrragent/auth_cli.py +335 -0
- brrragent/codex_oauth.py +1031 -0
- brrragent/gemini.py +240 -0
- brrragent/keys.py +158 -0
- brrragent/mcp_tools.py +686 -0
- brrragent/openai_direct.py +623 -0
- brrragent/openrouter.py +292 -0
- brrragent/prompt_cache.py +78 -0
- brrragent-0.4.2.dist-info/METADATA +224 -0
- brrragent-0.4.2.dist-info/RECORD +16 -0
- brrragent-0.4.2.dist-info/WHEEL +4 -0
- brrragent-0.4.2.dist-info/entry_points.txt +2 -0
- brrragent-0.4.2.dist-info/licenses/LICENSE +21 -0
brrragent/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
brrragent — Reusable agentic react loop with MCP tool calling.
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- McpToolCaller: Multi-server MCP bridge (OpenAI + Gemini formats)
|
|
6
|
+
- McpServerConfig: Streamable HTTP, SSE, stdio, or legacy JSON configuration
|
|
7
|
+
- run_agent: Smart-routed agentic loop (Gemini direct, OpenAI direct, Codex OAuth, OpenRouter)
|
|
8
|
+
- get_default_caller: Singleton McpToolCaller instance
|
|
9
|
+
- pick_key / pick_env_key: Handle comma-separated API keys
|
|
10
|
+
- KeyPool: Thread-safe key pool with 429 eviction (works for any provider)
|
|
11
|
+
- RateLimitExhausted: Exception when all keys are rate-limited
|
|
12
|
+
|
|
13
|
+
Backends (importable individually):
|
|
14
|
+
- brrragent.openrouter: OpenRouter/OpenAI-compatible backend
|
|
15
|
+
- brrragent.openai_direct: OpenAI native backend
|
|
16
|
+
- brrragent.codex_oauth: ChatGPT/Codex OAuth backend via isolated auth
|
|
17
|
+
- brrragent.gemini: Google Gemini direct backend
|
|
18
|
+
- brrragent.keys: API key utilities
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from brrragent.agent import run_agent
|
|
22
|
+
from brrragent.codex_oauth import CodexAuthConfig, codex_auth_context
|
|
23
|
+
from brrragent.keys import KeyPool, RateLimitExhausted, pick_env_key, pick_key
|
|
24
|
+
from brrragent.mcp_tools import McpServerConfig, McpToolCaller, get_default_caller
|
|
25
|
+
from brrragent.prompt_cache import AgentUsage, PromptCacheConfig
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"AgentUsage",
|
|
29
|
+
"CodexAuthConfig",
|
|
30
|
+
"KeyPool",
|
|
31
|
+
"McpServerConfig",
|
|
32
|
+
"McpToolCaller",
|
|
33
|
+
"PromptCacheConfig",
|
|
34
|
+
"RateLimitExhausted",
|
|
35
|
+
"get_default_caller",
|
|
36
|
+
"codex_auth_context",
|
|
37
|
+
"pick_env_key",
|
|
38
|
+
"pick_key",
|
|
39
|
+
"run_agent",
|
|
40
|
+
]
|
brrragent/_errors.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
def safe_error_summary(error: BaseException) -> str:
|
|
2
|
+
"""Describe an error without copying provider-controlled response text."""
|
|
3
|
+
name = type(error).__name__
|
|
4
|
+
status = getattr(error, "status_code", None)
|
|
5
|
+
if isinstance(status, int):
|
|
6
|
+
return f"{name} (HTTP {status})"
|
|
7
|
+
return name
|
brrragent/agent.py
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agentic React Loop — Smart Router.
|
|
3
|
+
|
|
4
|
+
Automatically chooses between Google Gemini direct, OpenAI direct,
|
|
5
|
+
Codex OAuth, native provider APIs, and OpenRouter based on the model name and
|
|
6
|
+
available keys.
|
|
7
|
+
|
|
8
|
+
Routing logic:
|
|
9
|
+
- model starts with "google/" or "gemini-" + Gemini keys available → Gemini direct
|
|
10
|
+
- model starts with "fireworks/" + FIREWORKS_API_KEY → Fireworks native
|
|
11
|
+
- model starts with "minimax/" + MINIMAX_API_KEY → MiniMax native
|
|
12
|
+
- model starts with "yunwu/" + YUNWU_API_KEY → Yunwu native
|
|
13
|
+
- model starts with "codex/" → Codex OAuth
|
|
14
|
+
- model starts with "openai/" + BRRRAGENT_OPENAI_BACKEND=codex_oauth → Codex OAuth
|
|
15
|
+
- model starts with "openai/" + OpenAI keys available → OpenAI direct
|
|
16
|
+
- Otherwise → OpenRouter with OPENROUTER_API_KEY
|
|
17
|
+
|
|
18
|
+
Supports KeyPool for both backends: keys are acquired per-call and evicted
|
|
19
|
+
on 429 errors. If no pool is provided, falls back to env-var key picking.
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
import os
|
|
23
|
+
|
|
24
|
+
from brrragent import McpToolCaller, run_agent, KeyPool
|
|
25
|
+
|
|
26
|
+
gemini_pool = KeyPool.from_csv("key1,key2,key3", name="gemini")
|
|
27
|
+
or_pool = KeyPool.from_csv("sk-or-1,sk-or-2", name="openrouter")
|
|
28
|
+
|
|
29
|
+
result = run_agent(
|
|
30
|
+
system_prompt="You are a helpful assistant.",
|
|
31
|
+
user_prompt="Complete the requested task.",
|
|
32
|
+
model="google/gemini-3.1-pro-preview",
|
|
33
|
+
gemini_key_pool=gemini_pool,
|
|
34
|
+
openrouter_key_pool=or_pool,
|
|
35
|
+
mcp=McpToolCaller(endpoint=os.environ["BRRRAGENT_MCP_URL"]),
|
|
36
|
+
)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
import logging
|
|
40
|
+
import os
|
|
41
|
+
from collections.abc import Callable
|
|
42
|
+
from typing import TYPE_CHECKING
|
|
43
|
+
|
|
44
|
+
from brrragent.keys import KeyPool
|
|
45
|
+
from brrragent.mcp_tools import McpToolCaller
|
|
46
|
+
from brrragent.prompt_cache import AgentUsage, PromptCacheConfig
|
|
47
|
+
|
|
48
|
+
if TYPE_CHECKING:
|
|
49
|
+
from brrragent.codex_oauth import CodexAuthConfig
|
|
50
|
+
|
|
51
|
+
logger = logging.getLogger(__name__)
|
|
52
|
+
|
|
53
|
+
MAX_TOOL_TURNS = 12
|
|
54
|
+
|
|
55
|
+
# Native provider registry: prefix → (base_url, env_var_name)
|
|
56
|
+
# Models with these prefixes are routed directly to the provider's
|
|
57
|
+
# OpenAI-compatible API instead of going through OpenRouter.
|
|
58
|
+
NATIVE_PROVIDERS: dict[str, tuple[str, str]] = {
|
|
59
|
+
"fireworks/": ("https://api.fireworks.ai/inference/v1", "FIREWORKS_API_KEY"),
|
|
60
|
+
"minimax/": ("https://api.minimax.io/v1", "MINIMAX_API_KEY"),
|
|
61
|
+
"yunwu/": ("https://yunwu.ai/v1", "YUNWU_API_KEY"),
|
|
62
|
+
"z-ai/": (
|
|
63
|
+
os.environ.get("ZAI_BASE_URL", "https://api.z.ai/api/coding/paas/v4"),
|
|
64
|
+
"ZAI_API_KEY",
|
|
65
|
+
),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _is_google_model(model: str) -> bool:
|
|
70
|
+
"""Check if a model identifier is a Google model."""
|
|
71
|
+
return model.startswith(("google/", "gemini-"))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _is_openai_model(model: str) -> bool:
|
|
75
|
+
"""Check if a model identifier should use OpenAI direct when keys exist."""
|
|
76
|
+
return model.startswith("openai/")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _is_codex_model(model: str) -> bool:
|
|
80
|
+
return model.startswith("codex/")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _use_codex_oauth_backend(model: str, api_key: str | None) -> bool:
|
|
84
|
+
if api_key is not None:
|
|
85
|
+
return False
|
|
86
|
+
if _is_codex_model(model):
|
|
87
|
+
return True
|
|
88
|
+
if not _is_openai_model(model):
|
|
89
|
+
return False
|
|
90
|
+
backend = os.environ.get("BRRRAGENT_OPENAI_BACKEND", "").strip().lower()
|
|
91
|
+
enabled = os.environ.get("BRRRAGENT_CODEX_OAUTH", "").strip().lower()
|
|
92
|
+
return backend in {"codex", "codex_oauth"} or enabled in {"1", "true", "yes", "on"}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _get_gemini_model_name(model: str) -> str:
|
|
96
|
+
"""Strip 'google/' prefix to get the bare Gemini model name."""
|
|
97
|
+
if model.startswith("google/"):
|
|
98
|
+
return model[len("google/") :]
|
|
99
|
+
return model
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _resolve_native_provider(model: str) -> tuple[str, str, str, str] | None:
|
|
103
|
+
"""Check if the model matches a native provider prefix.
|
|
104
|
+
|
|
105
|
+
Returns (base_url, api_key, provider_name, bare_model) if a native route
|
|
106
|
+
is available, or None to fall through to OpenRouter.
|
|
107
|
+
|
|
108
|
+
The bare_model has the provider prefix stripped — native APIs expect the
|
|
109
|
+
model ID without it (e.g. "accounts/fireworks/routers/kimi-k2p5-turbo"
|
|
110
|
+
not "fireworks/accounts/fireworks/routers/kimi-k2p5-turbo").
|
|
111
|
+
"""
|
|
112
|
+
for prefix, (base_url, env_var) in NATIVE_PROVIDERS.items():
|
|
113
|
+
if model.startswith(prefix):
|
|
114
|
+
api_key = os.environ.get(env_var, "").strip()
|
|
115
|
+
if api_key:
|
|
116
|
+
provider_name = prefix.rstrip("/")
|
|
117
|
+
bare_model = model[len(prefix) :]
|
|
118
|
+
return base_url, api_key, provider_name, bare_model
|
|
119
|
+
logger.debug(
|
|
120
|
+
"[agent] Model %s matches %s but %s is not set — falling through to OpenRouter",
|
|
121
|
+
model,
|
|
122
|
+
prefix,
|
|
123
|
+
env_var,
|
|
124
|
+
)
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _keys_from_envs(*env_names: str) -> list[str]:
|
|
129
|
+
keys: list[str] = []
|
|
130
|
+
seen: set[str] = set()
|
|
131
|
+
for env_name in env_names:
|
|
132
|
+
raw = os.environ.get(env_name, "")
|
|
133
|
+
for key in raw.split(","):
|
|
134
|
+
key = key.strip()
|
|
135
|
+
if key and key not in seen:
|
|
136
|
+
keys.append(key)
|
|
137
|
+
seen.add(key)
|
|
138
|
+
return keys
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def run_agent(
|
|
142
|
+
*,
|
|
143
|
+
system_prompt: str,
|
|
144
|
+
user_prompt: str,
|
|
145
|
+
model: str = "google/gemini-3.1-pro-preview",
|
|
146
|
+
api_key: str | None = None,
|
|
147
|
+
gemini_key_pool: KeyPool | None = None,
|
|
148
|
+
openai_key_pool: KeyPool | None = None,
|
|
149
|
+
openrouter_key_pool: KeyPool | None = None,
|
|
150
|
+
base_url: str = "https://openrouter.ai/api/v1",
|
|
151
|
+
mcp: McpToolCaller | None = None,
|
|
152
|
+
extra_tools: list[dict] | None = None,
|
|
153
|
+
max_turns: int = MAX_TOOL_TURNS,
|
|
154
|
+
temperature: float = 0.7,
|
|
155
|
+
max_tokens: int = 4096,
|
|
156
|
+
max_retries: int = 3,
|
|
157
|
+
on_tool_call: Callable | None = None,
|
|
158
|
+
response_schema: dict | None = None,
|
|
159
|
+
prompt_cache: PromptCacheConfig | None = None,
|
|
160
|
+
on_usage: Callable[[AgentUsage], None] | None = None,
|
|
161
|
+
codex_auth: "CodexAuthConfig | None" = None,
|
|
162
|
+
) -> str:
|
|
163
|
+
"""
|
|
164
|
+
Run an agentic react loop with tool calling.
|
|
165
|
+
|
|
166
|
+
Smart routing:
|
|
167
|
+
- If model starts with "google/" and Gemini keys are available → Gemini direct
|
|
168
|
+
- If model starts with "codex/" → Codex OAuth
|
|
169
|
+
- If model starts with "openai/" and BRRRAGENT_OPENAI_BACKEND=codex_oauth → Codex OAuth
|
|
170
|
+
- If model starts with "openai/" and OpenAI keys are available → OpenAI direct
|
|
171
|
+
- If model starts with a native provider prefix (fireworks/, minimax/, yunwu/) and
|
|
172
|
+
the corresponding env var is set → provider's native API
|
|
173
|
+
- Otherwise → OpenRouter with OPENROUTER_API_KEY
|
|
174
|
+
|
|
175
|
+
Key pools (preferred) or env vars can provide the keys.
|
|
176
|
+
On 429, pools evict the bad key and rotate to a fresh one.
|
|
177
|
+
RateLimitExhausted propagates up if ALL keys are exhausted.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
system_prompt: System instructions for the agent.
|
|
181
|
+
user_prompt: The user's request / task.
|
|
182
|
+
model: Model identifier (e.g. "google/gemini-3.1-pro-preview").
|
|
183
|
+
api_key: Override API key (skips smart routing, legacy).
|
|
184
|
+
gemini_key_pool: KeyPool for Gemini keys (preferred over env var).
|
|
185
|
+
openai_key_pool: KeyPool for direct OpenAI keys (preferred over env var).
|
|
186
|
+
openrouter_key_pool: KeyPool for OpenRouter keys (preferred over env var).
|
|
187
|
+
base_url: API base URL (only used for OpenRouter path).
|
|
188
|
+
mcp: McpToolCaller instance. Defaults to the shared singleton.
|
|
189
|
+
extra_tools: Additional tool definitions (OpenAI format).
|
|
190
|
+
max_turns: Maximum number of tool-calling rounds.
|
|
191
|
+
temperature: Sampling temperature.
|
|
192
|
+
max_tokens: Max tokens per response.
|
|
193
|
+
max_retries: Maximum attempts per API call on transient errors. Provider
|
|
194
|
+
SDK retries are disabled so this budget is authoritative.
|
|
195
|
+
on_tool_call: Optional callback(tool_name, tool_args) for logging/UI.
|
|
196
|
+
response_schema: JSON Schema dict for structured output (optional).
|
|
197
|
+
prompt_cache: Stable cache key and requested TTL for this prompt version.
|
|
198
|
+
on_usage: Optional callback invoked with provider token and cache usage.
|
|
199
|
+
codex_auth: Per-call Codex credential profile or explicit file paths.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
The model's final text response.
|
|
203
|
+
|
|
204
|
+
Raises:
|
|
205
|
+
ValueError: If all retries are exhausted without a response.
|
|
206
|
+
RateLimitExhausted: If all keys in a pool hit 429.
|
|
207
|
+
"""
|
|
208
|
+
if mcp is None:
|
|
209
|
+
from brrragent.mcp_tools import get_default_caller
|
|
210
|
+
|
|
211
|
+
mcp = get_default_caller()
|
|
212
|
+
|
|
213
|
+
# Build pools from env vars if not explicitly provided
|
|
214
|
+
if gemini_key_pool is None:
|
|
215
|
+
gemini_csv = os.environ.get("GEMINI_API_KEY", "")
|
|
216
|
+
if gemini_csv.strip():
|
|
217
|
+
gemini_key_pool = KeyPool.from_csv(gemini_csv, name="gemini")
|
|
218
|
+
|
|
219
|
+
if openrouter_key_pool is None:
|
|
220
|
+
or_csv = os.environ.get("OPENROUTER_API_KEY", "")
|
|
221
|
+
if or_csv.strip():
|
|
222
|
+
openrouter_key_pool = KeyPool.from_csv(or_csv, name="openrouter")
|
|
223
|
+
|
|
224
|
+
if openai_key_pool is None:
|
|
225
|
+
openai_keys = _keys_from_envs(
|
|
226
|
+
"OPENAI_API_KEY_PERSONAL",
|
|
227
|
+
"OPENAI_API_KEY",
|
|
228
|
+
"OPENAI_API_KEY_CORP",
|
|
229
|
+
)
|
|
230
|
+
if openai_keys:
|
|
231
|
+
openai_key_pool = KeyPool(openai_keys, name="openai")
|
|
232
|
+
|
|
233
|
+
# Smart routing: decide which backend to use
|
|
234
|
+
use_gemini_direct = (
|
|
235
|
+
_is_google_model(model) and gemini_key_pool is not None and api_key is None
|
|
236
|
+
)
|
|
237
|
+
use_openai_direct = (
|
|
238
|
+
_is_openai_model(model) and openai_key_pool is not None and api_key is None
|
|
239
|
+
)
|
|
240
|
+
use_codex_oauth = _use_codex_oauth_backend(model, api_key)
|
|
241
|
+
|
|
242
|
+
if use_gemini_direct:
|
|
243
|
+
from brrragent.gemini import run_gemini_agent
|
|
244
|
+
|
|
245
|
+
bare_model = _get_gemini_model_name(model)
|
|
246
|
+
logger.info(
|
|
247
|
+
"[agent] Using Gemini direct (model=%s, pool=%s)",
|
|
248
|
+
bare_model,
|
|
249
|
+
gemini_key_pool.status(),
|
|
250
|
+
)
|
|
251
|
+
return run_gemini_agent(
|
|
252
|
+
system_prompt=system_prompt,
|
|
253
|
+
user_prompt=user_prompt,
|
|
254
|
+
model=bare_model,
|
|
255
|
+
key_pool=gemini_key_pool,
|
|
256
|
+
mcp=mcp,
|
|
257
|
+
max_turns=max_turns,
|
|
258
|
+
temperature=temperature,
|
|
259
|
+
max_tokens=max_tokens,
|
|
260
|
+
max_retries=max_retries,
|
|
261
|
+
on_tool_call=on_tool_call,
|
|
262
|
+
response_schema=response_schema,
|
|
263
|
+
prompt_cache=prompt_cache,
|
|
264
|
+
on_usage=on_usage,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
if use_codex_oauth:
|
|
268
|
+
from brrragent.codex_oauth import codex_auth_context, run_codex_oauth_agent
|
|
269
|
+
|
|
270
|
+
logger.info("[agent] Using Codex OAuth (model=%s)", model)
|
|
271
|
+
kwargs = {
|
|
272
|
+
"system_prompt": system_prompt,
|
|
273
|
+
"user_prompt": user_prompt,
|
|
274
|
+
"model": model,
|
|
275
|
+
"mcp": mcp,
|
|
276
|
+
"extra_tools": extra_tools,
|
|
277
|
+
"max_turns": max_turns,
|
|
278
|
+
"temperature": temperature,
|
|
279
|
+
"max_tokens": max_tokens,
|
|
280
|
+
"max_retries": max_retries,
|
|
281
|
+
"on_tool_call": on_tool_call,
|
|
282
|
+
"response_schema": response_schema,
|
|
283
|
+
"prompt_cache": prompt_cache,
|
|
284
|
+
"on_usage": on_usage,
|
|
285
|
+
}
|
|
286
|
+
if codex_auth is None:
|
|
287
|
+
return run_codex_oauth_agent(**kwargs)
|
|
288
|
+
with codex_auth_context(codex_auth):
|
|
289
|
+
return run_codex_oauth_agent(**kwargs)
|
|
290
|
+
|
|
291
|
+
if use_openai_direct:
|
|
292
|
+
from brrragent.openai_direct import run_openai_agent
|
|
293
|
+
|
|
294
|
+
logger.info(
|
|
295
|
+
"[agent] Using OpenAI direct (model=%s, pool=%s)",
|
|
296
|
+
model,
|
|
297
|
+
openai_key_pool.status(),
|
|
298
|
+
)
|
|
299
|
+
return run_openai_agent(
|
|
300
|
+
system_prompt=system_prompt,
|
|
301
|
+
user_prompt=user_prompt,
|
|
302
|
+
model=model,
|
|
303
|
+
key_pool=openai_key_pool,
|
|
304
|
+
mcp=mcp,
|
|
305
|
+
extra_tools=extra_tools,
|
|
306
|
+
max_turns=max_turns,
|
|
307
|
+
temperature=temperature,
|
|
308
|
+
max_tokens=max_tokens,
|
|
309
|
+
max_retries=max_retries,
|
|
310
|
+
on_tool_call=on_tool_call,
|
|
311
|
+
response_schema=response_schema,
|
|
312
|
+
prompt_cache=prompt_cache,
|
|
313
|
+
on_usage=on_usage,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Check for native provider routing (fireworks/, minimax/, etc.)
|
|
317
|
+
native = _resolve_native_provider(model) if api_key is None else None
|
|
318
|
+
|
|
319
|
+
if native is not None:
|
|
320
|
+
from brrragent.openai_direct import parse_openai_model
|
|
321
|
+
from brrragent.openrouter import run_openrouter_agent
|
|
322
|
+
|
|
323
|
+
native_base_url, native_api_key, provider_name, bare_model = native
|
|
324
|
+
reasoning_effort = None
|
|
325
|
+
if provider_name in {"yunwu", "z-ai"}:
|
|
326
|
+
bare_model, reasoning_effort = parse_openai_model(bare_model)
|
|
327
|
+
if provider_name == "z-ai" and not reasoning_effort:
|
|
328
|
+
reasoning_effort = (
|
|
329
|
+
os.environ.get("ZAI_REASONING_EFFORT", "xhigh").strip() or "xhigh"
|
|
330
|
+
)
|
|
331
|
+
logger.info("[agent] Using %s native (model=%s)", provider_name, bare_model)
|
|
332
|
+
return run_openrouter_agent(
|
|
333
|
+
system_prompt=system_prompt,
|
|
334
|
+
user_prompt=user_prompt,
|
|
335
|
+
model=bare_model,
|
|
336
|
+
api_key=native_api_key,
|
|
337
|
+
key_pool=None,
|
|
338
|
+
base_url=native_base_url,
|
|
339
|
+
mcp=mcp,
|
|
340
|
+
extra_tools=extra_tools,
|
|
341
|
+
max_turns=max_turns,
|
|
342
|
+
temperature=temperature,
|
|
343
|
+
max_tokens=max_tokens,
|
|
344
|
+
max_retries=max_retries,
|
|
345
|
+
on_tool_call=on_tool_call,
|
|
346
|
+
response_schema=response_schema,
|
|
347
|
+
reasoning_effort=reasoning_effort,
|
|
348
|
+
prompt_cache=None,
|
|
349
|
+
on_usage=on_usage,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# Fallback: OpenRouter
|
|
353
|
+
from brrragent.openrouter import run_openrouter_agent
|
|
354
|
+
|
|
355
|
+
effective_key = api_key
|
|
356
|
+
effective_pool = None if api_key else openrouter_key_pool
|
|
357
|
+
|
|
358
|
+
if effective_key is None and effective_pool is None:
|
|
359
|
+
raise ValueError(
|
|
360
|
+
"No API key: set GEMINI_API_KEY or OPENROUTER_API_KEY, or provide a key pool"
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
logger.info(
|
|
364
|
+
"[agent] Using OpenRouter (model=%s, pool=%s)",
|
|
365
|
+
model,
|
|
366
|
+
effective_pool.status() if effective_pool else "explicit-key",
|
|
367
|
+
)
|
|
368
|
+
return run_openrouter_agent(
|
|
369
|
+
system_prompt=system_prompt,
|
|
370
|
+
user_prompt=user_prompt,
|
|
371
|
+
model=model,
|
|
372
|
+
api_key=effective_key,
|
|
373
|
+
key_pool=effective_pool,
|
|
374
|
+
base_url=base_url,
|
|
375
|
+
mcp=mcp,
|
|
376
|
+
extra_tools=extra_tools,
|
|
377
|
+
max_turns=max_turns,
|
|
378
|
+
temperature=temperature,
|
|
379
|
+
max_tokens=max_tokens,
|
|
380
|
+
max_retries=max_retries,
|
|
381
|
+
on_tool_call=on_tool_call,
|
|
382
|
+
response_schema=response_schema,
|
|
383
|
+
prompt_cache=prompt_cache,
|
|
384
|
+
on_usage=on_usage,
|
|
385
|
+
)
|