stravinsky 0.2.67__py3-none-any.whl → 0.4.66__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.

Potentially problematic release.


This version of stravinsky might be problematic. Click here for more details.

Files changed (190) hide show
  1. mcp_bridge/__init__.py +1 -1
  2. mcp_bridge/auth/__init__.py +16 -6
  3. mcp_bridge/auth/cli.py +202 -11
  4. mcp_bridge/auth/oauth.py +1 -2
  5. mcp_bridge/auth/openai_oauth.py +4 -7
  6. mcp_bridge/auth/token_store.py +112 -11
  7. mcp_bridge/cli/__init__.py +1 -1
  8. mcp_bridge/cli/install_hooks.py +503 -107
  9. mcp_bridge/cli/session_report.py +0 -3
  10. mcp_bridge/config/MANIFEST_SCHEMA.md +305 -0
  11. mcp_bridge/config/README.md +276 -0
  12. mcp_bridge/config/__init__.py +2 -2
  13. mcp_bridge/config/hook_config.py +247 -0
  14. mcp_bridge/config/hooks_manifest.json +138 -0
  15. mcp_bridge/config/rate_limits.py +317 -0
  16. mcp_bridge/config/skills_manifest.json +128 -0
  17. mcp_bridge/hooks/HOOKS_SETTINGS.json +17 -4
  18. mcp_bridge/hooks/__init__.py +19 -4
  19. mcp_bridge/hooks/agent_reminder.py +4 -4
  20. mcp_bridge/hooks/auto_slash_command.py +5 -5
  21. mcp_bridge/hooks/budget_optimizer.py +2 -2
  22. mcp_bridge/hooks/claude_limits_hook.py +114 -0
  23. mcp_bridge/hooks/comment_checker.py +3 -4
  24. mcp_bridge/hooks/compaction.py +2 -2
  25. mcp_bridge/hooks/context.py +2 -1
  26. mcp_bridge/hooks/context_monitor.py +2 -2
  27. mcp_bridge/hooks/delegation_policy.py +85 -0
  28. mcp_bridge/hooks/directory_context.py +3 -3
  29. mcp_bridge/hooks/edit_recovery.py +3 -2
  30. mcp_bridge/hooks/edit_recovery_policy.py +49 -0
  31. mcp_bridge/hooks/empty_message_sanitizer.py +2 -2
  32. mcp_bridge/hooks/events.py +160 -0
  33. mcp_bridge/hooks/git_noninteractive.py +4 -4
  34. mcp_bridge/hooks/keyword_detector.py +8 -10
  35. mcp_bridge/hooks/manager.py +43 -22
  36. mcp_bridge/hooks/notification_hook.py +13 -6
  37. mcp_bridge/hooks/parallel_enforcement_policy.py +67 -0
  38. mcp_bridge/hooks/parallel_enforcer.py +5 -5
  39. mcp_bridge/hooks/parallel_execution.py +22 -10
  40. mcp_bridge/hooks/post_tool/parallel_validation.py +103 -0
  41. mcp_bridge/hooks/pre_compact.py +8 -9
  42. mcp_bridge/hooks/pre_tool/agent_spawn_validator.py +115 -0
  43. mcp_bridge/hooks/preemptive_compaction.py +2 -3
  44. mcp_bridge/hooks/routing_notifications.py +80 -0
  45. mcp_bridge/hooks/rules_injector.py +11 -19
  46. mcp_bridge/hooks/session_idle.py +4 -4
  47. mcp_bridge/hooks/session_notifier.py +4 -4
  48. mcp_bridge/hooks/session_recovery.py +4 -5
  49. mcp_bridge/hooks/stravinsky_mode.py +1 -1
  50. mcp_bridge/hooks/subagent_stop.py +1 -3
  51. mcp_bridge/hooks/task_validator.py +2 -2
  52. mcp_bridge/hooks/tmux_manager.py +7 -8
  53. mcp_bridge/hooks/todo_delegation.py +4 -1
  54. mcp_bridge/hooks/todo_enforcer.py +180 -10
  55. mcp_bridge/hooks/tool_messaging.py +113 -10
  56. mcp_bridge/hooks/truncation_policy.py +37 -0
  57. mcp_bridge/hooks/truncator.py +1 -2
  58. mcp_bridge/metrics/cost_tracker.py +115 -0
  59. mcp_bridge/native_search.py +93 -0
  60. mcp_bridge/native_watcher.py +118 -0
  61. mcp_bridge/notifications.py +150 -0
  62. mcp_bridge/orchestrator/enums.py +11 -0
  63. mcp_bridge/orchestrator/router.py +165 -0
  64. mcp_bridge/orchestrator/state.py +32 -0
  65. mcp_bridge/orchestrator/visualization.py +14 -0
  66. mcp_bridge/orchestrator/wisdom.py +34 -0
  67. mcp_bridge/prompts/__init__.py +1 -8
  68. mcp_bridge/prompts/dewey.py +1 -1
  69. mcp_bridge/prompts/planner.py +2 -4
  70. mcp_bridge/prompts/stravinsky.py +53 -31
  71. mcp_bridge/proxy/__init__.py +0 -0
  72. mcp_bridge/proxy/client.py +70 -0
  73. mcp_bridge/proxy/model_server.py +157 -0
  74. mcp_bridge/routing/__init__.py +43 -0
  75. mcp_bridge/routing/config.py +250 -0
  76. mcp_bridge/routing/model_tiers.py +135 -0
  77. mcp_bridge/routing/provider_state.py +261 -0
  78. mcp_bridge/routing/task_classifier.py +190 -0
  79. mcp_bridge/server.py +542 -59
  80. mcp_bridge/server_tools.py +738 -6
  81. mcp_bridge/tools/__init__.py +40 -25
  82. mcp_bridge/tools/agent_manager.py +616 -697
  83. mcp_bridge/tools/background_tasks.py +13 -17
  84. mcp_bridge/tools/code_search.py +70 -53
  85. mcp_bridge/tools/continuous_loop.py +0 -1
  86. mcp_bridge/tools/dashboard.py +19 -0
  87. mcp_bridge/tools/find_code.py +296 -0
  88. mcp_bridge/tools/init.py +1 -0
  89. mcp_bridge/tools/list_directory.py +42 -0
  90. mcp_bridge/tools/lsp/__init__.py +12 -5
  91. mcp_bridge/tools/lsp/manager.py +471 -0
  92. mcp_bridge/tools/lsp/tools.py +723 -207
  93. mcp_bridge/tools/model_invoke.py +1195 -273
  94. mcp_bridge/tools/mux_client.py +75 -0
  95. mcp_bridge/tools/project_context.py +1 -2
  96. mcp_bridge/tools/query_classifier.py +406 -0
  97. mcp_bridge/tools/read_file.py +84 -0
  98. mcp_bridge/tools/replace.py +45 -0
  99. mcp_bridge/tools/run_shell_command.py +38 -0
  100. mcp_bridge/tools/search_enhancements.py +347 -0
  101. mcp_bridge/tools/semantic_search.py +3627 -0
  102. mcp_bridge/tools/session_manager.py +0 -2
  103. mcp_bridge/tools/skill_loader.py +0 -1
  104. mcp_bridge/tools/task_runner.py +5 -7
  105. mcp_bridge/tools/templates.py +3 -3
  106. mcp_bridge/tools/tool_search.py +331 -0
  107. mcp_bridge/tools/write_file.py +29 -0
  108. mcp_bridge/update_manager.py +585 -0
  109. mcp_bridge/update_manager_pypi.py +297 -0
  110. mcp_bridge/utils/cache.py +82 -0
  111. mcp_bridge/utils/process.py +71 -0
  112. mcp_bridge/utils/session_state.py +51 -0
  113. mcp_bridge/utils/truncation.py +76 -0
  114. stravinsky-0.4.66.dist-info/METADATA +517 -0
  115. stravinsky-0.4.66.dist-info/RECORD +198 -0
  116. {stravinsky-0.2.67.dist-info → stravinsky-0.4.66.dist-info}/entry_points.txt +1 -0
  117. stravinsky_claude_assets/HOOKS_INTEGRATION.md +316 -0
  118. stravinsky_claude_assets/agents/HOOKS.md +437 -0
  119. stravinsky_claude_assets/agents/code-reviewer.md +210 -0
  120. stravinsky_claude_assets/agents/comment_checker.md +580 -0
  121. stravinsky_claude_assets/agents/debugger.md +254 -0
  122. stravinsky_claude_assets/agents/delphi.md +495 -0
  123. stravinsky_claude_assets/agents/dewey.md +248 -0
  124. stravinsky_claude_assets/agents/explore.md +1198 -0
  125. stravinsky_claude_assets/agents/frontend.md +472 -0
  126. stravinsky_claude_assets/agents/implementation-lead.md +164 -0
  127. stravinsky_claude_assets/agents/momus.md +464 -0
  128. stravinsky_claude_assets/agents/research-lead.md +141 -0
  129. stravinsky_claude_assets/agents/stravinsky.md +730 -0
  130. stravinsky_claude_assets/commands/delphi.md +9 -0
  131. stravinsky_claude_assets/commands/dewey.md +54 -0
  132. stravinsky_claude_assets/commands/git-master.md +112 -0
  133. stravinsky_claude_assets/commands/index.md +49 -0
  134. stravinsky_claude_assets/commands/publish.md +86 -0
  135. stravinsky_claude_assets/commands/review.md +73 -0
  136. stravinsky_claude_assets/commands/str/agent_cancel.md +70 -0
  137. stravinsky_claude_assets/commands/str/agent_list.md +56 -0
  138. stravinsky_claude_assets/commands/str/agent_output.md +92 -0
  139. stravinsky_claude_assets/commands/str/agent_progress.md +74 -0
  140. stravinsky_claude_assets/commands/str/agent_retry.md +94 -0
  141. stravinsky_claude_assets/commands/str/cancel.md +51 -0
  142. stravinsky_claude_assets/commands/str/clean.md +97 -0
  143. stravinsky_claude_assets/commands/str/continue.md +38 -0
  144. stravinsky_claude_assets/commands/str/index.md +199 -0
  145. stravinsky_claude_assets/commands/str/list_watchers.md +96 -0
  146. stravinsky_claude_assets/commands/str/search.md +205 -0
  147. stravinsky_claude_assets/commands/str/start_filewatch.md +136 -0
  148. stravinsky_claude_assets/commands/str/stats.md +71 -0
  149. stravinsky_claude_assets/commands/str/stop_filewatch.md +89 -0
  150. stravinsky_claude_assets/commands/str/unwatch.md +42 -0
  151. stravinsky_claude_assets/commands/str/watch.md +45 -0
  152. stravinsky_claude_assets/commands/strav.md +53 -0
  153. stravinsky_claude_assets/commands/stravinsky.md +292 -0
  154. stravinsky_claude_assets/commands/verify.md +60 -0
  155. stravinsky_claude_assets/commands/version.md +5 -0
  156. stravinsky_claude_assets/hooks/README.md +248 -0
  157. stravinsky_claude_assets/hooks/comment_checker.py +193 -0
  158. stravinsky_claude_assets/hooks/context.py +38 -0
  159. stravinsky_claude_assets/hooks/context_monitor.py +153 -0
  160. stravinsky_claude_assets/hooks/dependency_tracker.py +73 -0
  161. stravinsky_claude_assets/hooks/edit_recovery.py +46 -0
  162. stravinsky_claude_assets/hooks/execution_state_tracker.py +68 -0
  163. stravinsky_claude_assets/hooks/notification_hook.py +103 -0
  164. stravinsky_claude_assets/hooks/notification_hook_v2.py +96 -0
  165. stravinsky_claude_assets/hooks/parallel_execution.py +241 -0
  166. stravinsky_claude_assets/hooks/parallel_reinforcement.py +106 -0
  167. stravinsky_claude_assets/hooks/parallel_reinforcement_v2.py +112 -0
  168. stravinsky_claude_assets/hooks/pre_compact.py +123 -0
  169. stravinsky_claude_assets/hooks/ralph_loop.py +173 -0
  170. stravinsky_claude_assets/hooks/session_recovery.py +263 -0
  171. stravinsky_claude_assets/hooks/stop_hook.py +89 -0
  172. stravinsky_claude_assets/hooks/stravinsky_metrics.py +164 -0
  173. stravinsky_claude_assets/hooks/stravinsky_mode.py +146 -0
  174. stravinsky_claude_assets/hooks/subagent_stop.py +98 -0
  175. stravinsky_claude_assets/hooks/todo_continuation.py +111 -0
  176. stravinsky_claude_assets/hooks/todo_delegation.py +96 -0
  177. stravinsky_claude_assets/hooks/tool_messaging.py +281 -0
  178. stravinsky_claude_assets/hooks/truncator.py +23 -0
  179. stravinsky_claude_assets/rules/deployment_safety.md +51 -0
  180. stravinsky_claude_assets/rules/integration_wiring.md +89 -0
  181. stravinsky_claude_assets/rules/pypi_deployment.md +220 -0
  182. stravinsky_claude_assets/rules/stravinsky_orchestrator.md +32 -0
  183. stravinsky_claude_assets/settings.json +152 -0
  184. stravinsky_claude_assets/skills/chrome-devtools/SKILL.md +81 -0
  185. stravinsky_claude_assets/skills/sqlite/SKILL.md +77 -0
  186. stravinsky_claude_assets/skills/supabase/SKILL.md +74 -0
  187. stravinsky_claude_assets/task_dependencies.json +34 -0
  188. stravinsky-0.2.67.dist-info/METADATA +0 -284
  189. stravinsky-0.2.67.dist-info/RECORD +0 -76
  190. {stravinsky-0.2.67.dist-info → stravinsky-0.4.66.dist-info}/WHEEL +0 -0
@@ -12,8 +12,6 @@ Key naming conventions (Stravinsky equivalents):
12
12
  - agent_spawn (not call-omo-agent) - spawn background agents
13
13
  """
14
14
 
15
- from typing import Optional
16
-
17
15
  # Core role definition
18
16
  STRAVINSKY_ROLE_SECTION = """<Role>
19
17
  You are "Stravinsky" - Powerful AI Agent with orchestration capabilities from Stravinsky MCP.
@@ -36,7 +34,7 @@ Named after the composer known for revolutionary orchestration.
36
34
  </Role>"""
37
35
 
38
36
 
39
- STRAVINSKY_PHASE0_STEP1_3 = """### Step 0: Check Skills FIRST (BLOCKING)
37
+ STRAVINSKY_PHASE0_STEP1_3 = """### Step 0: Check Skills FUWT (BLOCKING)
40
38
 
41
39
  **Before ANY classification or action, scan for matching skills.**
42
40
 
@@ -54,7 +52,7 @@ Skills are specialized workflows. When relevant, they handle the task better tha
54
52
 
55
53
  | Type | Signal | Action |
56
54
  |------|--------|--------|
57
- | **Skill Match** | Matches skill trigger phrase | **INVOKE skill FIRST** via `skill_get` tool |
55
+ | **Skill Match** | Matches skill trigger phrase | **INVOKE skill FUWT** via `skill_get` tool |
58
56
  | **Trivial** | Single file, known location, direct answer | Direct tools only (UNLESS Key Trigger applies) |
59
57
  | **Explicit** | Specific file/line, clear command | Execute directly |
60
58
  | **Exploratory** | "How does X work?", "Find Y" | Fire explore (1-3) + tools in parallel |
@@ -159,7 +157,7 @@ STOP searching when:
159
157
  **DO NOT over-explore. Time is precious.**"""
160
158
 
161
159
 
162
- STRAVINSKY_PHASE2B_PRE_IMPLEMENTATION = """## ⚠️ CRITICAL: PARALLEL-FIRST WORKFLOW
160
+ STRAVINSKY_PHASE2B_PRE_IMPLEMENTATION = """## ⚠️ CRITICAL: PARALLEL-FUWT WORKFLOW
163
161
 
164
162
  **BLOCKING REQUIREMENT**: For implementation tasks, your response structure MUST be:
165
163
 
@@ -204,54 +202,78 @@ todowrite([todo1, todo2, todo3])
204
202
  4. THEN mark todos complete"""
205
203
 
206
204
 
207
- STRAVINSKY_DELEGATION_PROMPT_STRUCTURE = """### Delegation Prompt Structure (MANDATORY - ALL 7 sections):
205
+ STRAVINSKY_DELEGATION_PROMPT_STRUCTURE = """### Delegation Prompt Structure (RECOMMENDED - 5 KEY SECTIONS):
208
206
 
209
- When delegating via `agent_spawn`, your prompt MUST include ALL 7 sections:
207
+ When delegating via `agent_spawn`, your prompt SHOULD include these sections:
210
208
 
211
209
  ```
212
- 1. TASK: Atomic, specific goal (one sentence)
210
+ 1. TASK: Clear, natural language description of what needs to be found/analyzed
213
211
  2. EXPECTED OUTCOME: Concrete deliverables with success criteria
214
- 3. REQUIRED TOOLS: Explicit tool whitelist (Read, Grep, Glob, etc.)
215
- 4. MUST DO: Exhaustive requirements list
216
- 5. MUST NOT DO: Forbidden actions (prevent rogue behavior)
217
- 6. CONTEXT: File paths, existing patterns, constraints
218
- 7. SUCCESS CRITERIA: How to verify completion
212
+ 3. MUST DO: Exhaustive requirements list
213
+ 4. MUST NOT DO: Forbidden actions (prevent rogue behavior)
214
+ 5. CONTEXT: File paths, existing patterns, constraints (if known)
219
215
  ```
220
216
 
221
- **Example Delegation Prompt:**
217
+ **❌ WRONG - Over-Prescribing Tools:**
222
218
  ```
223
219
  ## TASK
224
220
  Find all API endpoint definitions in the auth module.
225
221
 
226
- ## EXPECTED OUTCOME
227
- List of endpoints with: path, method, handler function, file location.
228
-
229
222
  ## REQUIRED TOOLS
230
- Read, Grep, Glob
223
+ Read, Grep, Glob # ❌ DON'T PRESCRIBE TOOLS - agents choose optimal approach
224
+
225
+ ## MUST DO
226
+ - Use grep_search to find "def" patterns # ❌ TOO SPECIFIC
227
+ ```
228
+
229
+ **✅ CORRECT - Natural Language + Trust Agent Intelligence:**
230
+ ```
231
+ ## TASK
232
+ Find and explain all API endpoint definitions in the auth module, including their request/response patterns and how they connect to each other.
233
+
234
+ ## EXPECTED OUTCOME
235
+ Complete list of endpoints with: path, method, handler function, file location, and architectural notes on how they integrate.
231
236
 
232
237
  ## MUST DO
233
- - Search in src/auth/ directory
234
- - Include path parameters
235
- - Report line numbers
238
+ - Search in src/auth/ directory and related integration points
239
+ - Include path parameters and query string handling
240
+ - Report exact line numbers for each endpoint
241
+ - Explain the authentication flow across endpoints
236
242
 
237
243
  ## MUST NOT DO
238
244
  - Modify any files
239
- - Search outside src/auth/
245
+ - Skip integration patterns (how endpoints call each other)
240
246
 
241
247
  ## CONTEXT
242
248
  Project uses FastAPI. Auth endpoints handle login, logout, token refresh.
249
+ This is a CONCEPTUAL/ARCHITECTURAL query - the agent should use semantic_search + grep for comprehensive coverage.
243
250
 
244
251
  ## SUCCESS CRITERIA
245
- All endpoints documented with complete paths and handlers.
252
+ All endpoints documented with complete paths, handlers, AND architectural understanding of how they work together.
246
253
  ```
247
254
 
255
+ **WHY THIS WORKS BETTER:**
256
+ - ✅ Explore agent has `semantic_search` in its toolset and knows when to use it
257
+ - ✅ Natural language tasks → agent classifies as SEMANTIC → uses semantic_search
258
+ - ✅ Agent combines semantic_search (concepts) + grep_search (exact matches) automatically
259
+ - ❌ "REQUIRED TOOLS: grep_search" → blocks semantic_search even for conceptual queries
260
+
261
+ **TRUST THE AGENTS:**
262
+ The explore agent already has comprehensive tool selection logic:
263
+ - Semantic queries → semantic_search (primary)
264
+ - Exact syntax → grep_search
265
+ - Code structure → ast_grep_search
266
+ - Symbol navigation → LSP tools
267
+
268
+ Let them choose the optimal search strategy based on your TASK description, not prescriptive tool lists.
269
+
248
270
  AFTER THE WORK YOU DELEGATED SEEMS DONE, ALWAYS VERIFY THE RESULTS:
249
271
  - DOES IT WORK AS EXPECTED?
250
272
  - DOES IT FOLLOW THE EXISTING CODEBASE PATTERN?
251
273
  - EXPECTED RESULT CAME OUT?
252
274
  - DID THE AGENT FOLLOW "MUST DO" AND "MUST NOT DO" REQUIREMENTS?
253
275
 
254
- **Vague prompts = rejected. Be exhaustive.**"""
276
+ **Natural language task descriptions = agent intelligence. Tool prescriptions = micromanagement.**"""
255
277
 
256
278
 
257
279
  STRAVINSKY_GITHUB_WORKFLOW = """### GitHub Workflow (CRITICAL - When mentioned in issues/PRs):
@@ -358,7 +380,7 @@ If verification fails:
358
380
 
359
381
  STRAVINSKY_KEY_TRIGGERS = """### Key Triggers (check BEFORE classification):
360
382
 
361
- **BLOCKING: Check skills FIRST before any action.**
383
+ **BLOCKING: Check skills FUWT before any action.**
362
384
  If a skill matches, invoke it IMMEDIATELY via `skill_get` tool.
363
385
 
364
386
  - External library/source mentioned -> fire `dewey` background via `agent_spawn`
@@ -472,12 +494,12 @@ Delphi is an expensive, high-quality reasoning model. Use it wisely via `agent_s
472
494
 
473
495
  | Trigger | Action |
474
496
  |---------|--------|
475
- | Complex architecture design | Delphi FIRST, then implement |
476
- | After completing significant work | Delphi FIRST, then implement |
477
- | 2+ failed fix attempts | Delphi FIRST, then implement |
478
- | Unfamiliar code patterns | Delphi FIRST, then implement |
479
- | Security/performance concerns | Delphi FIRST, then implement |
480
- | Multi-system tradeoffs | Delphi FIRST, then implement |
497
+ | Complex architecture design | Delphi FUWT, then implement |
498
+ | After completing significant work | Delphi FUWT, then implement |
499
+ | 2+ failed fix attempts | Delphi FUWT, then implement |
500
+ | Unfamiliar code patterns | Delphi FUWT, then implement |
501
+ | Security/performance concerns | Delphi FUWT, then implement |
502
+ | Multi-system tradeoffs | Delphi FUWT, then implement |
481
503
 
482
504
  ### WHEN NOT to Consult:
483
505
 
File without changes
@@ -0,0 +1,70 @@
1
+ import os
2
+ import httpx
3
+ import logging
4
+ from typing import Any, Optional
5
+
6
+ logger = logging.getLogger("stravinsky.proxy_client")
7
+
8
+ PROXY_URL = os.getenv("STRAVINSKY_PROXY_URL", "http://127.0.0.1:8765")
9
+
10
+ async def proxy_invoke_gemini(
11
+ prompt: str,
12
+ model: str = "gemini-3-flash",
13
+ temperature: float = 0.7,
14
+ max_tokens: int = 8192,
15
+ thinking_budget: int = 0,
16
+ image_path: Optional[str] = None,
17
+ agent_context: Optional[dict[str, Any]] = None
18
+ ) -> str:
19
+ """Routes Gemini invocation to the proxy server."""
20
+ payload = {
21
+ "prompt": prompt,
22
+ "model": model,
23
+ "temperature": temperature,
24
+ "max_tokens": max_tokens,
25
+ "thinking_budget": thinking_budget,
26
+ "image_path": image_path,
27
+ "agent_context": agent_context
28
+ }
29
+
30
+ async with httpx.AsyncClient(timeout=130.0) as client:
31
+ try:
32
+ response = await client.post(f"{PROXY_URL}/v1/gemini/generate", json=payload)
33
+ response.raise_for_status()
34
+ return response.json()["response"]
35
+ except Exception as e:
36
+ logger.error(f"Proxy request failed: {e}")
37
+ raise
38
+
39
+ async def proxy_invoke_openai(
40
+ prompt: str,
41
+ model: str = "gpt-5.2-codex",
42
+ temperature: float = 0.7,
43
+ max_tokens: int = 4096,
44
+ thinking_budget: int = 0,
45
+ reasoning_effort: str = "medium",
46
+ agent_context: Optional[dict[str, Any]] = None
47
+ ) -> str:
48
+ """Routes OpenAI invocation to the proxy server."""
49
+ payload = {
50
+ "prompt": prompt,
51
+ "model": model,
52
+ "temperature": temperature,
53
+ "max_tokens": max_tokens,
54
+ "thinking_budget": thinking_budget,
55
+ "reasoning_effort": reasoning_effort,
56
+ "agent_context": agent_context
57
+ }
58
+
59
+ async with httpx.AsyncClient(timeout=130.0) as client:
60
+ try:
61
+ response = await client.post(f"{PROXY_URL}/v1/openai/chat", json=payload)
62
+ response.raise_for_status()
63
+ return response.json()["response"]
64
+ except Exception as e:
65
+ logger.error(f"Proxy request failed: {e}")
66
+ raise
67
+
68
+ def is_proxy_enabled() -> bool:
69
+ """Checks if proxy usage is enabled via environment variable."""
70
+ return os.getenv("STRAVINSKY_USE_PROXY", "false").lower() == "true"
@@ -0,0 +1,157 @@
1
+ import logging
2
+ import os
3
+ import time
4
+ import uuid
5
+ from typing import Any
6
+
7
+ import uvicorn
8
+ from fastapi import FastAPI, HTTPException, Request
9
+ from pydantic import BaseModel
10
+
11
+ from mcp_bridge.auth.token_store import TokenStore
12
+ from mcp_bridge.tools.model_invoke import invoke_gemini, invoke_openai
13
+
14
+ # Configure logging
15
+ logging.basicConfig(
16
+ level=logging.INFO,
17
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
18
+ )
19
+ logger = logging.getLogger("stravinsky.proxy")
20
+
21
+ app = FastAPI(title="Stravinsky Model Proxy")
22
+
23
+
24
+ @app.middleware("http")
25
+ async def add_process_time_header(request: Request, call_next):
26
+ request_id = str(uuid.uuid4())
27
+ start_time = time.time()
28
+ logger.info(f"[{request_id}] {request.method} {request.url.path}")
29
+ response = await call_next(request)
30
+ process_time = time.time() - start_time
31
+ response.headers["X-Process-Time"] = str(process_time)
32
+ response.headers["X-Request-ID"] = request_id
33
+ logger.info(f"[{request_id}] Completed in {process_time:.4f}s")
34
+ return response
35
+
36
+
37
+ # Shared token store
38
+ _token_store = None
39
+
40
+
41
+ def get_token_store():
42
+ global _token_store
43
+ if _token_store is None:
44
+ _token_store = TokenStore()
45
+ return _token_store
46
+
47
+
48
+ class GeminiRequest(BaseModel):
49
+ prompt: str
50
+ model: str = "gemini-3-flash"
51
+ temperature: float = 0.7
52
+ max_tokens: int = 8192
53
+ thinking_budget: int = 0
54
+ image_path: str | None = None
55
+ agent_context: dict[str, Any] | None = None
56
+
57
+
58
+ class GeminiAgenticRequest(BaseModel):
59
+ prompt: str
60
+ model: str = "gemini-3-flash"
61
+ max_turns: int = 10
62
+ timeout: int = 120
63
+
64
+
65
+ class OpenAIRequest(BaseModel):
66
+ prompt: str
67
+ model: str = "gpt-5.2-codex"
68
+ temperature: float = 0.7
69
+ max_tokens: int = 4096
70
+ thinking_budget: int = 0
71
+ reasoning_effort: str = "medium"
72
+ agent_context: dict[str, Any] | None = None
73
+
74
+
75
+ @app.get("/health")
76
+ async def health():
77
+ return {"status": "ok"}
78
+
79
+
80
+ @app.post("/v1/gemini/generate")
81
+ async def gemini_generate(request: GeminiRequest):
82
+ """Proxy endpoint for Gemini generation."""
83
+ try:
84
+ token_store = get_token_store()
85
+ # We need to ensure agent_context is passed correctly if invoke_gemini supports it
86
+ # Based on previous read, invoke_gemini takes image_path, but agent_context
87
+ # might be extracted from hooks or passed in params.
88
+ # Actually, invoke_gemini extracts it from params.
89
+
90
+ response = await invoke_gemini(
91
+ token_store=token_store,
92
+ prompt=request.prompt,
93
+ model=request.model,
94
+ temperature=request.temperature,
95
+ max_tokens=request.max_tokens,
96
+ thinking_budget=request.thinking_budget,
97
+ image_path=request.image_path,
98
+ )
99
+ return {"response": response}
100
+ except Exception as e:
101
+ logger.error(f"Error in gemini_generate proxy: {e}", exc_info=True)
102
+ raise HTTPException(status_code=500, detail=str(e)) from e
103
+
104
+
105
+ @app.post("/v1/gemini/agentic")
106
+ async def gemini_agentic(request: GeminiAgenticRequest):
107
+ """Proxy endpoint for Agentic Gemini."""
108
+ try:
109
+ from mcp_bridge.tools.model_invoke import invoke_gemini_agentic
110
+
111
+ token_store = get_token_store()
112
+ response = await invoke_gemini_agentic(
113
+ token_store=token_store,
114
+ prompt=request.prompt,
115
+ model=request.model,
116
+ max_turns=request.max_turns,
117
+ timeout=request.timeout,
118
+ )
119
+ return {"response": response}
120
+ except Exception as e:
121
+ logger.error(f"Error in gemini_agentic proxy: {e}", exc_info=True)
122
+ raise HTTPException(status_code=500, detail=str(e)) from e
123
+
124
+
125
+ @app.post("/v1/openai/chat")
126
+ async def openai_chat(request: OpenAIRequest):
127
+ """Proxy endpoint for OpenAI chat."""
128
+ try:
129
+ token_store = get_token_store()
130
+ response = await invoke_openai(
131
+ token_store=token_store,
132
+ prompt=request.prompt,
133
+ model=request.model,
134
+ temperature=request.temperature,
135
+ max_tokens=request.max_tokens,
136
+ thinking_budget=request.thinking_budget,
137
+ reasoning_effort=request.reasoning_effort,
138
+ )
139
+ return {"response": response}
140
+ except Exception as e:
141
+ logger.error(f"Error in openai_chat proxy: {e}", exc_info=True)
142
+ raise HTTPException(status_code=500, detail=str(e)) from e
143
+
144
+
145
+ def main():
146
+ """Entry point for the proxy server."""
147
+ # CRITICAL: Disable proxy usage within the proxy process to avoid infinite loops
148
+ os.environ["STRAVINSKY_USE_PROXY"] = "false"
149
+
150
+ port = int(os.getenv("STRAVINSKY_PROXY_PORT", 8765))
151
+ host = os.getenv("STRAVINSKY_PROXY_HOST", "127.0.0.1")
152
+ logger.info(f"Starting Stravinsky Model Proxy on {host}:{port}")
153
+ uvicorn.run(app, host=host, port=port, log_level="info")
154
+
155
+
156
+ if __name__ == "__main__":
157
+ main()
@@ -0,0 +1,43 @@
1
+ """
2
+ Stravinsky Multi-Provider Routing System.
3
+
4
+ This module provides intelligent routing between providers (Claude, OpenAI, Gemini)
5
+ with automatic fallback when providers hit rate limits or capacity constraints.
6
+
7
+ Components:
8
+ - ProviderState: Tracks availability of each provider
9
+ - ProviderStateTracker: Global state management for all providers
10
+ - TaskClassifier: Classifies tasks to route to optimal providers
11
+ - RoutingConfig: Project-local configuration loader
12
+ """
13
+
14
+ from .config import (
15
+ DEFAULT_ROUTING_CONFIG,
16
+ RoutingConfig,
17
+ load_routing_config,
18
+ )
19
+ from .provider_state import (
20
+ ProviderState,
21
+ ProviderStateTracker,
22
+ get_provider_tracker,
23
+ )
24
+ from .task_classifier import (
25
+ TaskType,
26
+ classify_task,
27
+ get_routing_for_task,
28
+ )
29
+
30
+ __all__ = [
31
+ # Provider state
32
+ "ProviderState",
33
+ "ProviderStateTracker",
34
+ "get_provider_tracker",
35
+ # Task classification
36
+ "TaskType",
37
+ "classify_task",
38
+ "get_routing_for_task",
39
+ # Configuration
40
+ "load_routing_config",
41
+ "RoutingConfig",
42
+ "DEFAULT_ROUTING_CONFIG",
43
+ ]
@@ -0,0 +1,250 @@
1
+ """
2
+ Routing Configuration with Project-Local Priority.
3
+
4
+ Loads routing configuration from:
5
+ 1. .stravinsky/routing.json (project-local - highest priority)
6
+ 2. ~/.stravinsky/routing.json (user-global fallback)
7
+ 3. Built-in defaults
8
+
9
+ This allows per-project customization of routing behavior.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import logging
16
+ from dataclasses import dataclass, field
17
+ from pathlib import Path
18
+ from typing import Any, Literal
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # Default routing configuration
23
+ DEFAULT_ROUTING_CONFIG: dict[str, Any] = {
24
+ "routing": {
25
+ "enabled": True,
26
+ "task_routing": {
27
+ "code_generation": {
28
+ "provider": "claude",
29
+ "model": "claude-4.5-opus",
30
+ "tier": "premium",
31
+ },
32
+ "code_refactoring": {
33
+ "provider": "claude",
34
+ "model": "claude-4.5-sonnet",
35
+ "tier": "standard",
36
+ },
37
+ "debugging": {
38
+ "provider": "openai",
39
+ "model": "gpt-5.2",
40
+ "tier": "standard",
41
+ },
42
+ "architecture": {
43
+ "provider": "openai",
44
+ "model": "gpt-5.2",
45
+ "tier": "standard",
46
+ },
47
+ "documentation": {
48
+ "provider": "gemini",
49
+ "model": "gemini-3-flash-preview",
50
+ "tier": "standard",
51
+ },
52
+ "code_search": {
53
+ "provider": "gemini",
54
+ "model": "gemini-3-flash-preview",
55
+ "tier": "standard",
56
+ },
57
+ "security_review": {
58
+ "provider": "claude",
59
+ "model": "claude-4.5-opus",
60
+ "tier": "premium",
61
+ },
62
+ "general": {
63
+ "provider": "claude",
64
+ "model": "claude-4.5-sonnet",
65
+ "tier": "standard",
66
+ },
67
+ },
68
+ "fallback": {
69
+ "enabled": True,
70
+ "chain": ["claude", "openai", "gemini"],
71
+ "cooldown_seconds": 300,
72
+ },
73
+ "claude_limits": {
74
+ "detection_enabled": True,
75
+ "slow_response_threshold_seconds": 30,
76
+ "auto_fallback": True,
77
+ },
78
+ }
79
+ }
80
+
81
+
82
+ @dataclass
83
+ class TaskRoutingRule:
84
+ """Routing rule for a specific task type."""
85
+
86
+ provider: str
87
+ model: str | None = None
88
+ tier: Literal["premium", "standard"] = "standard"
89
+
90
+
91
+ @dataclass
92
+ class FallbackConfig:
93
+ """Fallback configuration."""
94
+
95
+ enabled: bool = True
96
+ chain: list[str] = field(default_factory=lambda: ["claude", "openai", "gemini"])
97
+ cooldown_seconds: int = 300
98
+
99
+
100
+ @dataclass
101
+ class ClaudeLimitsConfig:
102
+ """Claude limits detection configuration."""
103
+
104
+ detection_enabled: bool = True
105
+ slow_response_threshold_seconds: int = 30
106
+ auto_fallback: bool = True
107
+
108
+
109
+ @dataclass
110
+ class RoutingConfig:
111
+ """Complete routing configuration."""
112
+
113
+ enabled: bool = True
114
+ task_routing: dict[str, TaskRoutingRule] = field(default_factory=dict)
115
+ fallback: FallbackConfig = field(default_factory=FallbackConfig)
116
+ claude_limits: ClaudeLimitsConfig = field(default_factory=ClaudeLimitsConfig)
117
+ source: str = "default" # Where config was loaded from
118
+
119
+ @classmethod
120
+ def from_dict(cls, data: dict[str, Any], source: str = "dict") -> RoutingConfig:
121
+ """Create RoutingConfig from a dictionary."""
122
+ routing = data.get("routing", data) # Handle both wrapped and unwrapped
123
+
124
+ # Parse task routing
125
+ task_routing = {}
126
+ for task_type, rule in routing.get("task_routing", {}).items():
127
+ if isinstance(rule, dict):
128
+ task_routing[task_type] = TaskRoutingRule(
129
+ provider=rule.get("provider", "claude"),
130
+ model=rule.get("model"),
131
+ )
132
+
133
+ # Parse fallback config
134
+ fallback_data = routing.get("fallback", {})
135
+ fallback = FallbackConfig(
136
+ enabled=fallback_data.get("enabled", True),
137
+ chain=fallback_data.get("chain", ["claude", "openai", "gemini"]),
138
+ cooldown_seconds=fallback_data.get("cooldown_seconds", 300),
139
+ )
140
+
141
+ # Parse claude limits config
142
+ claude_data = routing.get("claude_limits", {})
143
+ claude_limits = ClaudeLimitsConfig(
144
+ detection_enabled=claude_data.get("detection_enabled", True),
145
+ slow_response_threshold_seconds=claude_data.get("slow_response_threshold_seconds", 30),
146
+ auto_fallback=claude_data.get("auto_fallback", True),
147
+ )
148
+
149
+ return cls(
150
+ enabled=routing.get("enabled", True),
151
+ task_routing=task_routing,
152
+ fallback=fallback,
153
+ claude_limits=claude_limits,
154
+ source=source,
155
+ )
156
+
157
+ def get_routing_for_task(self, task_type: str) -> TaskRoutingRule:
158
+ """Get routing rule for a task type, with fallback to general."""
159
+ if task_type in self.task_routing:
160
+ return self.task_routing[task_type]
161
+ if "general" in self.task_routing:
162
+ return self.task_routing["general"]
163
+ return TaskRoutingRule(provider="claude", model=None)
164
+
165
+
166
+ def load_routing_config(project_path: str = ".") -> RoutingConfig:
167
+ """
168
+ Load routing config with project-local priority.
169
+
170
+ Discovery order:
171
+ 1. .stravinsky/routing.json (project-local)
172
+ 2. ~/.stravinsky/routing.json (user-global)
173
+ 3. Built-in defaults
174
+
175
+ Args:
176
+ project_path: Path to the project root
177
+
178
+ Returns:
179
+ RoutingConfig instance
180
+ """
181
+ # Project-local config
182
+ project_config_path = Path(project_path) / ".stravinsky" / "routing.json"
183
+ if project_config_path.exists():
184
+ try:
185
+ data = json.loads(project_config_path.read_text())
186
+ config = RoutingConfig.from_dict(data, source=str(project_config_path))
187
+ logger.info(f"[RoutingConfig] Loaded project-local config from {project_config_path}")
188
+ return config
189
+ except Exception as e:
190
+ logger.warning(f"[RoutingConfig] Failed to load {project_config_path}: {e}")
191
+
192
+ # User-global fallback
193
+ global_config_path = Path.home() / ".stravinsky" / "routing.json"
194
+ if global_config_path.exists():
195
+ try:
196
+ data = json.loads(global_config_path.read_text())
197
+ config = RoutingConfig.from_dict(data, source=str(global_config_path))
198
+ logger.info(f"[RoutingConfig] Loaded user-global config from {global_config_path}")
199
+ return config
200
+ except Exception as e:
201
+ logger.warning(f"[RoutingConfig] Failed to load {global_config_path}: {e}")
202
+
203
+ # Built-in defaults
204
+ logger.info("[RoutingConfig] Using built-in defaults")
205
+ return RoutingConfig.from_dict(DEFAULT_ROUTING_CONFIG, source="default")
206
+
207
+
208
+ def init_routing_config(project_path: str = ".") -> Path:
209
+ """
210
+ Initialize a project-local routing config file.
211
+
212
+ Creates .stravinsky/routing.json with default configuration.
213
+
214
+ Args:
215
+ project_path: Path to the project root
216
+
217
+ Returns:
218
+ Path to the created config file
219
+ """
220
+ config_dir = Path(project_path) / ".stravinsky"
221
+ config_dir.mkdir(parents=True, exist_ok=True)
222
+
223
+ config_path = config_dir / "routing.json"
224
+
225
+ if config_path.exists():
226
+ logger.warning(f"[RoutingConfig] Config already exists at {config_path}")
227
+ return config_path
228
+
229
+ config_path.write_text(json.dumps(DEFAULT_ROUTING_CONFIG, indent=2))
230
+ logger.info(f"[RoutingConfig] Created config at {config_path}")
231
+
232
+ return config_path
233
+
234
+
235
+ def get_config_source(project_path: str = ".") -> str:
236
+ """
237
+ Get the source of the active routing config.
238
+
239
+ Returns:
240
+ Path to the config file being used, or "default" for built-in
241
+ """
242
+ project_config = Path(project_path) / ".stravinsky" / "routing.json"
243
+ if project_config.exists():
244
+ return str(project_config)
245
+
246
+ global_config = Path.home() / ".stravinsky" / "routing.json"
247
+ if global_config.exists():
248
+ return str(global_config)
249
+
250
+ return "default"