kivi-cli 2.0.6__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.
- kivi_cli-2.0.6.dist-info/METADATA +17 -0
- kivi_cli-2.0.6.dist-info/RECORD +47 -0
- kivi_cli-2.0.6.dist-info/WHEEL +5 -0
- kivi_cli-2.0.6.dist-info/entry_points.txt +2 -0
- kivi_cli-2.0.6.dist-info/top_level.txt +1 -0
- kiviai/__init__.py +0 -0
- kiviai/backend.py +1372 -0
- kiviai/index.html +6172 -0
- kiviai/services/__init__.py +0 -0
- kiviai/services/git_service.py +219 -0
- kiviai/services/monitor.py +97 -0
- kiviai/services/providers/__init__.py +0 -0
- kiviai/services/providers/base.py +131 -0
- kiviai/services/providers/claude.py +125 -0
- kiviai/services/providers/copilot.py +251 -0
- kiviai/services/providers/inhouse.py +130 -0
- kiviai/services/providers/kivi.py +108 -0
- kiviai/services/providers/model_costs.py +137 -0
- kiviai/services/providers/openai_agent.py +113 -0
- kiviai/services/providers/opencode.py +157 -0
- kiviai/services/providers/orchestrator_provider.py +163 -0
- kiviai/services/providers/registry.py +89 -0
- kiviai/services/providers/vllm.py +319 -0
- kiviai/services/scheduler.py +244 -0
- kiviai/services/tools/__init__.py +1 -0
- kiviai/services/tools/copilot_tools/__init__.py +92 -0
- kiviai/services/tools/copilot_tools/agent_orchestration/__init__.py +17 -0
- kiviai/services/tools/copilot_tools/agent_orchestration/tools.py +296 -0
- kiviai/services/tools/copilot_tools/bash_execution/__init__.py +20 -0
- kiviai/services/tools/copilot_tools/bash_execution/sessions.py +335 -0
- kiviai/services/tools/copilot_tools/code_search/__init__.py +10 -0
- kiviai/services/tools/copilot_tools/code_search/tools.py +137 -0
- kiviai/services/tools/copilot_tools/file_operations/__init__.py +12 -0
- kiviai/services/tools/copilot_tools/file_operations/tools.py +197 -0
- kiviai/services/tools/copilot_tools/markdown/__init__.py +41 -0
- kiviai/services/tools/copilot_tools/markdown/custom_markdownify.py +774 -0
- kiviai/services/tools/copilot_tools/markdown/mrkdwn_analysis.py +1155 -0
- kiviai/services/tools/copilot_tools/markdown/tools.py +357 -0
- kiviai/services/tools/copilot_tools/session_workflow/__init__.py +17 -0
- kiviai/services/tools/copilot_tools/session_workflow/tools.py +199 -0
- kiviai/services/tools/copilot_tools/web/__init__.py +37 -0
- kiviai/services/tools/copilot_tools/web/_fetcher.py +39 -0
- kiviai/services/tools/copilot_tools/web/_store.py +122 -0
- kiviai/services/tools/copilot_tools/web/fetch.py +20 -0
- kiviai/services/tools/copilot_tools/web/search.py +116 -0
- kiviai/services/tools/copilot_tools/web/store_tools.py +92 -0
- kiviai/services/tools/tool_manager.py +239 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""OpenAI Agent SDK provider — uses openai-agents package."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from typing import AsyncIterator
|
|
8
|
+
|
|
9
|
+
from .base import BaseProvider, ProviderConfig, ProviderEvent, EventType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OpenAIAgentProvider(BaseProvider):
|
|
13
|
+
name = "openai-agent"
|
|
14
|
+
display_name = "OpenAI Agent"
|
|
15
|
+
description = "OpenAI's agent SDK with built-in tool use, handoffs, and guardrails. Runs multi-step workflows with GPT models. Supports streaming and sub-agent delegation. Requires OPENAI_API_KEY."
|
|
16
|
+
supports_streaming = True
|
|
17
|
+
supports_tools = True
|
|
18
|
+
supports_sessions = False
|
|
19
|
+
supports_agents = True
|
|
20
|
+
available_models = [
|
|
21
|
+
"gpt-5.4", "gpt-5-mini", "gpt-4.1", "gpt-4.1-mini",
|
|
22
|
+
"gpt-4.1-nano", "o3", "o3-mini", "o4-mini",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
self._stop = False
|
|
27
|
+
|
|
28
|
+
async def run(self, prompt: str, config: ProviderConfig, history=None) -> AsyncIterator[ProviderEvent]:
|
|
29
|
+
self._stop = False
|
|
30
|
+
|
|
31
|
+
api_key = config.api_key or os.environ.get("OPENAI_API_KEY", "")
|
|
32
|
+
model = config.model or "gpt-4o"
|
|
33
|
+
|
|
34
|
+
if not api_key:
|
|
35
|
+
yield ProviderEvent(type=EventType.ERROR, content="OPENAI_API_KEY not set")
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
from openai import AsyncOpenAI
|
|
40
|
+
|
|
41
|
+
client = AsyncOpenAI(api_key=api_key)
|
|
42
|
+
|
|
43
|
+
messages = []
|
|
44
|
+
if history:
|
|
45
|
+
for h in history:
|
|
46
|
+
if h.get("role") in ("user", "assistant", "system") and not h.get("metadata"):
|
|
47
|
+
messages.append({"role": h["role"], "content": h["content"]})
|
|
48
|
+
messages.append({"role": "user", "content": prompt})
|
|
49
|
+
|
|
50
|
+
stream = await client.chat.completions.create(
|
|
51
|
+
model=model,
|
|
52
|
+
messages=messages,
|
|
53
|
+
stream=True,
|
|
54
|
+
max_tokens=4096,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
text_buf = ""
|
|
58
|
+
last_chunk = None
|
|
59
|
+
async for chunk in stream:
|
|
60
|
+
if self._stop:
|
|
61
|
+
yield ProviderEvent(type=EventType.TEXT, content="⏹ *stopped*")
|
|
62
|
+
yield ProviderEvent(type=EventType.DONE)
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
last_chunk = chunk
|
|
66
|
+
delta = chunk.choices[0].delta if chunk.choices else None
|
|
67
|
+
if delta and delta.content:
|
|
68
|
+
text_buf += delta.content
|
|
69
|
+
yield ProviderEvent(type=EventType.TEXT, content=delta.content)
|
|
70
|
+
|
|
71
|
+
if chunk.choices and chunk.choices[0].finish_reason:
|
|
72
|
+
break
|
|
73
|
+
|
|
74
|
+
except ImportError:
|
|
75
|
+
yield ProviderEvent(type=EventType.ERROR, content="openai package required: pip install openai")
|
|
76
|
+
return
|
|
77
|
+
except Exception as e:
|
|
78
|
+
yield ProviderEvent(type=EventType.ERROR, content=str(e))
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
# Emit cost event from usage metadata on last chunk
|
|
82
|
+
from .model_costs import estimate_cost
|
|
83
|
+
input_tokens = 0
|
|
84
|
+
output_tokens = 0
|
|
85
|
+
if last_chunk and hasattr(last_chunk, 'usage') and last_chunk.usage:
|
|
86
|
+
input_tokens = last_chunk.usage.prompt_tokens or 0
|
|
87
|
+
output_tokens = last_chunk.usage.completion_tokens or 0
|
|
88
|
+
|
|
89
|
+
usage = {
|
|
90
|
+
"input_tokens": input_tokens,
|
|
91
|
+
"output_tokens": output_tokens,
|
|
92
|
+
}
|
|
93
|
+
total_cost = estimate_cost(
|
|
94
|
+
model,
|
|
95
|
+
input_tokens=input_tokens,
|
|
96
|
+
output_tokens=output_tokens,
|
|
97
|
+
)
|
|
98
|
+
yield ProviderEvent(
|
|
99
|
+
type=EventType.COST,
|
|
100
|
+
metadata={
|
|
101
|
+
"total_cost_usd": total_cost,
|
|
102
|
+
"usage": usage,
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
yield ProviderEvent(type=EventType.DONE)
|
|
107
|
+
|
|
108
|
+
async def stop(self):
|
|
109
|
+
self._stop = True
|
|
110
|
+
|
|
111
|
+
async def health_check(self):
|
|
112
|
+
api_key = os.environ.get("OPENAI_API_KEY", "")
|
|
113
|
+
return {"status": "ok" if api_key else "no_api_key", "provider": self.name}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""OpenCode provider — wraps `opencode run --format json`.
|
|
2
|
+
|
|
3
|
+
Event types from opencode CLI:
|
|
4
|
+
step_start — new LLM turn begins
|
|
5
|
+
text — streaming text chunk
|
|
6
|
+
tool_use — tool announced + completed (state has input/output/error)
|
|
7
|
+
step_finish — step done with token/cost info
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import json
|
|
13
|
+
from typing import AsyncIterator
|
|
14
|
+
|
|
15
|
+
from .base import BaseProvider, ProviderConfig, ProviderEvent, EventType
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OpenCodeProvider(BaseProvider):
|
|
19
|
+
name = "opencode"
|
|
20
|
+
display_name = "OpenCode"
|
|
21
|
+
description = "Open-source agentic coding CLI. File editing, shell access, and step-by-step reasoning with tool use. Supports persistent sessions and multiple LLM backends."
|
|
22
|
+
supports_streaming = True
|
|
23
|
+
supports_tools = True
|
|
24
|
+
supports_sessions = True
|
|
25
|
+
available_models = [
|
|
26
|
+
# opencode/* are free-tier models served by OpenCode itself (no API key
|
|
27
|
+
# needed) — verified live via `opencode models`. This list drifts as
|
|
28
|
+
# OpenCode rotates free models in/out; re-run `opencode models` to refresh.
|
|
29
|
+
"opencode/big-pickle", "opencode/deepseek-v4-flash-free", "opencode/hy3-free",
|
|
30
|
+
"opencode/mimo-v2.5-free", "opencode/nemotron-3-ultra-free", "opencode/north-mini-code-free",
|
|
31
|
+
# Below require the user's own provider credentials (`opencode auth login`).
|
|
32
|
+
"anthropic/claude-sonnet-5", "anthropic/claude-opus-4-8", "anthropic/claude-haiku-4-5",
|
|
33
|
+
"openai/gpt-5.4", "openai/gpt-4.1", "openai/gpt-4.1-mini", "openai/o3", "openai/o4-mini",
|
|
34
|
+
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
def __init__(self):
|
|
38
|
+
self._stop = False
|
|
39
|
+
self._proc = None
|
|
40
|
+
|
|
41
|
+
async def run(self, prompt: str, config: ProviderConfig, history=None) -> AsyncIterator[ProviderEvent]:
|
|
42
|
+
self._stop = False
|
|
43
|
+
cmd = ["opencode", "run", "--format", "json"]
|
|
44
|
+
if config.model:
|
|
45
|
+
cmd += ["--model", config.model]
|
|
46
|
+
if config.session_id:
|
|
47
|
+
cmd += ["--session", config.session_id]
|
|
48
|
+
if config.cwd:
|
|
49
|
+
cmd += ["--dir", config.cwd]
|
|
50
|
+
if config.extra.get("thinking"):
|
|
51
|
+
cmd += ["--thinking"]
|
|
52
|
+
if config.extra.get("fork"):
|
|
53
|
+
cmd += ["--fork"]
|
|
54
|
+
|
|
55
|
+
# File attachments via -f flag
|
|
56
|
+
for f in config.extra.get("files", []):
|
|
57
|
+
cmd += ["-f", f]
|
|
58
|
+
|
|
59
|
+
cmd += [prompt]
|
|
60
|
+
|
|
61
|
+
session_id = None
|
|
62
|
+
total_cost = 0.0
|
|
63
|
+
usage = {}
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
self._proc = await asyncio.create_subprocess_exec(
|
|
67
|
+
*cmd,
|
|
68
|
+
stdout=asyncio.subprocess.PIPE,
|
|
69
|
+
stderr=asyncio.subprocess.PIPE,
|
|
70
|
+
stdin=asyncio.subprocess.DEVNULL,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
async for line in self._proc.stdout:
|
|
74
|
+
if self._stop:
|
|
75
|
+
self._proc.terminate()
|
|
76
|
+
yield ProviderEvent(type=EventType.TEXT, content="⏹ *stopped*")
|
|
77
|
+
yield ProviderEvent(type=EventType.DONE)
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
line = line.decode().strip()
|
|
81
|
+
if not line or not line.startswith("{"):
|
|
82
|
+
continue
|
|
83
|
+
try:
|
|
84
|
+
msg = json.loads(line)
|
|
85
|
+
except Exception:
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
session_id = msg.get("sessionID", session_id)
|
|
89
|
+
mtype = msg.get("type")
|
|
90
|
+
part = msg.get("part", {})
|
|
91
|
+
|
|
92
|
+
if mtype == "text":
|
|
93
|
+
yield ProviderEvent(type=EventType.TEXT, content=part.get("text", ""))
|
|
94
|
+
|
|
95
|
+
elif mtype == "error":
|
|
96
|
+
error_data = msg.get("error", {})
|
|
97
|
+
error_msg = error_data.get("data", {}).get("message", "") or error_data.get("name", "Unknown error")
|
|
98
|
+
yield ProviderEvent(type=EventType.ERROR, content=f"OpenCode error: {error_msg}")
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
elif mtype == "tool_use":
|
|
102
|
+
state = part.get("state", {})
|
|
103
|
+
tool_name = part.get("tool", "tool")
|
|
104
|
+
title = part.get("title", tool_name) or tool_name
|
|
105
|
+
args = json.dumps(state.get("input", {}), indent=2)
|
|
106
|
+
output = state.get("output", "") or state.get("error", "")
|
|
107
|
+
if isinstance(output, list):
|
|
108
|
+
output = "\n".join(c.get("text", str(c)) for c in output)
|
|
109
|
+
status = state.get("status", "")
|
|
110
|
+
is_error = status == "error" or bool(state.get("error"))
|
|
111
|
+
|
|
112
|
+
yield ProviderEvent(
|
|
113
|
+
type=EventType.TOOL_START,
|
|
114
|
+
metadata={"title": f"⚙ {title}", "args": args},
|
|
115
|
+
)
|
|
116
|
+
yield ProviderEvent(
|
|
117
|
+
type=EventType.TOOL_RESULT,
|
|
118
|
+
content=str(output),
|
|
119
|
+
metadata={"is_error": is_error},
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
elif mtype == "step_finish":
|
|
123
|
+
tokens = part.get("tokens", {})
|
|
124
|
+
cache = tokens.get("cache", {})
|
|
125
|
+
step_cost = part.get("cost", 0.0)
|
|
126
|
+
total_cost += step_cost
|
|
127
|
+
usage["input_tokens"] = usage.get("input_tokens", 0) + tokens.get("input", 0)
|
|
128
|
+
usage["output_tokens"] = usage.get("output_tokens", 0) + tokens.get("output", 0)
|
|
129
|
+
usage["cache_read_input_tokens"] = usage.get("cache_read_input_tokens", 0) + cache.get("read", 0)
|
|
130
|
+
usage["cache_creation_input_tokens"] = usage.get("cache_creation_input_tokens", 0) + cache.get("write", 0)
|
|
131
|
+
|
|
132
|
+
await self._proc.wait()
|
|
133
|
+
except Exception as e:
|
|
134
|
+
yield ProviderEvent(type=EventType.ERROR, content=str(e))
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
if session_id or total_cost:
|
|
138
|
+
yield ProviderEvent(
|
|
139
|
+
type=EventType.COST,
|
|
140
|
+
metadata={
|
|
141
|
+
"session_id": session_id,
|
|
142
|
+
"total_cost_usd": total_cost,
|
|
143
|
+
"usage": usage,
|
|
144
|
+
},
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
yield ProviderEvent(type=EventType.DONE)
|
|
148
|
+
|
|
149
|
+
async def stop(self):
|
|
150
|
+
self._stop = True
|
|
151
|
+
if self._proc:
|
|
152
|
+
self._proc.terminate()
|
|
153
|
+
|
|
154
|
+
async def health_check(self):
|
|
155
|
+
import shutil
|
|
156
|
+
has_bin = shutil.which("opencode") is not None
|
|
157
|
+
return {"status": "ok" if has_bin else "unavailable", "provider": self.name}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Multi-agent orchestrator provider — supervisor pattern with parallel workers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from typing import AsyncIterator
|
|
8
|
+
|
|
9
|
+
from .base import BaseProvider, ProviderConfig, ProviderEvent, EventType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OrchestratorProvider(BaseProvider):
|
|
13
|
+
name = "claudeagents"
|
|
14
|
+
display_name = "Multi-Agent Orchestrator"
|
|
15
|
+
description = "Supervisor-worker pattern for parallel execution. Breaks complex tasks into subtasks, runs them across multiple Claude agents simultaneously, then synthesizes results. Best for large refactors, multi-file changes, and research tasks."
|
|
16
|
+
supports_streaming = True
|
|
17
|
+
supports_tools = True
|
|
18
|
+
supports_sessions = False
|
|
19
|
+
supports_agents = True
|
|
20
|
+
available_models = ["claude-sonnet-4.6"]
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self._stop = False
|
|
24
|
+
|
|
25
|
+
async def run(self, prompt: str, config: ProviderConfig, history=None) -> AsyncIterator[ProviderEvent]:
|
|
26
|
+
"""Supervisor loop: plan → parallel workers → review → repeat."""
|
|
27
|
+
self._stop = False
|
|
28
|
+
|
|
29
|
+
# Import Claude provider for worker execution
|
|
30
|
+
from .claude import ClaudeProvider
|
|
31
|
+
from .registry import get_provider
|
|
32
|
+
|
|
33
|
+
cwd = config.cwd or "."
|
|
34
|
+
mode = config.mode or "bypassPermissions"
|
|
35
|
+
|
|
36
|
+
# Step 1: Plan subtasks using supervisor
|
|
37
|
+
yield ProviderEvent(
|
|
38
|
+
type=EventType.TOOL_START,
|
|
39
|
+
metadata={"title": "🎯 Supervisor", "args": "Planning subtasks…"},
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
plan_prompt = (
|
|
43
|
+
"You are a task supervisor. Break this into independent subtasks for parallel execution.\n"
|
|
44
|
+
f'Respond ONLY with a JSON array: [{{"id":"1","prompt":"...","model":"haiku"}}]\n'
|
|
45
|
+
f"Use 'haiku' for most tasks. Use 'sonnet' only for complex reasoning.\n\n"
|
|
46
|
+
f"User request: {prompt}\nWorking directory: {cwd}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
supervisor = ClaudeProvider()
|
|
51
|
+
plan_config = ProviderConfig(mode=mode, cwd=cwd)
|
|
52
|
+
plan_text = ""
|
|
53
|
+
async for event in supervisor.run(plan_prompt, plan_config):
|
|
54
|
+
if event.type == EventType.TEXT:
|
|
55
|
+
plan_text += event.content
|
|
56
|
+
elif event.type == EventType.COST:
|
|
57
|
+
yield event
|
|
58
|
+
|
|
59
|
+
# Parse plan
|
|
60
|
+
plan_text = plan_text.strip()
|
|
61
|
+
if plan_text.startswith("```"):
|
|
62
|
+
plan_text = "\n".join(plan_text.split("\n")[1:])
|
|
63
|
+
if plan_text.endswith("```"):
|
|
64
|
+
plan_text = plan_text[:-3]
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
parsed = json.loads(plan_text.strip())
|
|
68
|
+
except json.JSONDecodeError:
|
|
69
|
+
match = re.search(r'\[.*\]', plan_text, re.DOTALL)
|
|
70
|
+
if match:
|
|
71
|
+
parsed = json.loads(match.group())
|
|
72
|
+
else:
|
|
73
|
+
parsed = [{"id": "1", "prompt": prompt, "model": "haiku"}]
|
|
74
|
+
|
|
75
|
+
if isinstance(parsed, dict):
|
|
76
|
+
subtasks = parsed.get("tasks", [parsed])
|
|
77
|
+
else:
|
|
78
|
+
subtasks = parsed
|
|
79
|
+
|
|
80
|
+
yield ProviderEvent(
|
|
81
|
+
type=EventType.TOOL_RESULT,
|
|
82
|
+
content=f"Planned {len(subtasks)} subtask(s)",
|
|
83
|
+
metadata={"title": "🎯 Plan", "is_error": False},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Step 2: Run workers in parallel
|
|
87
|
+
MODEL_MAP = {
|
|
88
|
+
"haiku": "claude-haiku-4-5-20251001",
|
|
89
|
+
"sonnet": "claude-sonnet-4-6",
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for st in subtasks:
|
|
93
|
+
worker_id = st.get("id", "1")
|
|
94
|
+
worker_prompt = st.get("prompt", prompt)
|
|
95
|
+
model_key = st.get("model", "haiku")
|
|
96
|
+
model = MODEL_MAP.get(model_key, MODEL_MAP["haiku"])
|
|
97
|
+
|
|
98
|
+
yield ProviderEvent(
|
|
99
|
+
type=EventType.AGENT_GROUP,
|
|
100
|
+
metadata={
|
|
101
|
+
"agent_id": worker_id,
|
|
102
|
+
"agent_label": f"Agent {worker_id} ({model_key})",
|
|
103
|
+
"model": model,
|
|
104
|
+
"status": "running",
|
|
105
|
+
"children": [],
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Execute workers
|
|
110
|
+
async def run_worker(st):
|
|
111
|
+
worker_id = st.get("id", "1")
|
|
112
|
+
worker_prompt = st.get("prompt", prompt)
|
|
113
|
+
model_key = st.get("model", "haiku")
|
|
114
|
+
model = MODEL_MAP.get(model_key, MODEL_MAP["haiku"])
|
|
115
|
+
|
|
116
|
+
worker = ClaudeProvider()
|
|
117
|
+
worker_config = ProviderConfig(
|
|
118
|
+
mode=mode, cwd=cwd,
|
|
119
|
+
extra={"model_override": model},
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
result_text = ""
|
|
123
|
+
events = []
|
|
124
|
+
async for event in worker.run(worker_prompt, worker_config):
|
|
125
|
+
if event.type == EventType.TEXT:
|
|
126
|
+
result_text += event.content
|
|
127
|
+
events.append(event)
|
|
128
|
+
|
|
129
|
+
return {"id": worker_id, "text": result_text, "events": events, "model": model_key}
|
|
130
|
+
|
|
131
|
+
results = await asyncio.gather(*[run_worker(st) for st in subtasks])
|
|
132
|
+
|
|
133
|
+
# Update agent groups with results
|
|
134
|
+
for r in results:
|
|
135
|
+
yield ProviderEvent(
|
|
136
|
+
type=EventType.AGENT_GROUP,
|
|
137
|
+
metadata={
|
|
138
|
+
"agent_id": r["id"],
|
|
139
|
+
"agent_label": f"Agent {r['id']} ({r['model']})",
|
|
140
|
+
"model": r["model"],
|
|
141
|
+
"status": "done",
|
|
142
|
+
"children": [{"role": "assistant", "content": r["text"]}],
|
|
143
|
+
},
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Step 3: Summarize
|
|
147
|
+
summary_parts = []
|
|
148
|
+
for r in results:
|
|
149
|
+
summary_parts.append(f"Agent {r['id']}: {r['text'][:200]}")
|
|
150
|
+
|
|
151
|
+
yield ProviderEvent(
|
|
152
|
+
type=EventType.TEXT,
|
|
153
|
+
content=f"All {len(results)} agent(s) completed.\n\n" + "\n\n".join(summary_parts),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
except Exception as e:
|
|
157
|
+
yield ProviderEvent(type=EventType.ERROR, content=str(e))
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
yield ProviderEvent(type=EventType.DONE)
|
|
161
|
+
|
|
162
|
+
async def stop(self):
|
|
163
|
+
self._stop = True
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provider registry — maps model names to provider classes.
|
|
3
|
+
Single source of truth for which providers are available.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Type
|
|
9
|
+
|
|
10
|
+
from .base import BaseProvider, ProviderConfig
|
|
11
|
+
|
|
12
|
+
log = logging.getLogger("cc-ui.registry")
|
|
13
|
+
|
|
14
|
+
_PROVIDERS: dict[str, Type[BaseProvider]] = {}
|
|
15
|
+
_ALIASES: dict[str, str] = {}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def register(name: str, provider_cls: Type[BaseProvider], aliases: list[str] | None = None):
|
|
19
|
+
"""Register a provider class under a canonical name."""
|
|
20
|
+
_PROVIDERS[name] = provider_cls
|
|
21
|
+
if aliases:
|
|
22
|
+
for alias in aliases:
|
|
23
|
+
_ALIASES[alias] = name
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_provider(name: str) -> BaseProvider:
|
|
27
|
+
"""Instantiate a provider by name or alias."""
|
|
28
|
+
canonical = _ALIASES.get(name, name)
|
|
29
|
+
cls = _PROVIDERS.get(canonical)
|
|
30
|
+
if cls is None:
|
|
31
|
+
available = list(_PROVIDERS.keys()) + list(_ALIASES.keys())
|
|
32
|
+
raise ValueError(f"Unknown provider '{name}'. Available: {available}")
|
|
33
|
+
return cls()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def list_providers() -> list[dict]:
|
|
37
|
+
"""Return capabilities of all registered providers."""
|
|
38
|
+
seen = set()
|
|
39
|
+
result = []
|
|
40
|
+
for name, cls in _PROVIDERS.items():
|
|
41
|
+
if name not in seen:
|
|
42
|
+
seen.add(name)
|
|
43
|
+
instance = cls()
|
|
44
|
+
info = instance.get_capabilities()
|
|
45
|
+
info["aliases"] = [a for a, c in _ALIASES.items() if c == name]
|
|
46
|
+
result.append(info)
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def health_check_all() -> dict:
|
|
51
|
+
"""Run health checks on all providers."""
|
|
52
|
+
results = {}
|
|
53
|
+
for name, cls in _PROVIDERS.items():
|
|
54
|
+
try:
|
|
55
|
+
instance = cls()
|
|
56
|
+
results[name] = await instance.health_check()
|
|
57
|
+
except Exception as e:
|
|
58
|
+
results[name] = {"status": "error", "error": str(e)}
|
|
59
|
+
return results
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _auto_register():
|
|
63
|
+
"""Auto-register all built-in providers."""
|
|
64
|
+
try:
|
|
65
|
+
from .claude import ClaudeProvider
|
|
66
|
+
register("claude", ClaudeProvider, aliases=["claude-code", "cc", "sonnet", "opus", "haiku"])
|
|
67
|
+
except ImportError as e:
|
|
68
|
+
log.debug("Claude provider not available: %s", e)
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
from .opencode import OpenCodeProvider
|
|
72
|
+
register("opencode", OpenCodeProvider, aliases=["oc"])
|
|
73
|
+
except ImportError as e:
|
|
74
|
+
log.debug("OpenCode provider not available: %s", e)
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
from .copilot import CopilotProvider
|
|
78
|
+
register("copilot", CopilotProvider, aliases=["gh-copilot"])
|
|
79
|
+
except ImportError as e:
|
|
80
|
+
log.debug("Copilot provider not available: %s", e)
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
from .vllm import VLLMProvider
|
|
84
|
+
register("vllm", VLLMProvider, aliases=["local", "vllm-local"])
|
|
85
|
+
except ImportError as e:
|
|
86
|
+
log.debug("vLLM provider not available: %s", e)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
_auto_register()
|