devcopilot 0.2.0__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.
- api/__init__.py +17 -0
- api/admin_config.py +1303 -0
- api/admin_routes.py +287 -0
- api/admin_static/admin.css +459 -0
- api/admin_static/admin.js +497 -0
- api/admin_static/index.html +77 -0
- api/admin_urls.py +34 -0
- api/app.py +194 -0
- api/command_utils.py +164 -0
- api/dependencies.py +144 -0
- api/detection.py +152 -0
- api/gateway_model_ids.py +54 -0
- api/model_catalog.py +133 -0
- api/model_router.py +125 -0
- api/models/__init__.py +45 -0
- api/models/anthropic.py +234 -0
- api/models/openai_responses.py +28 -0
- api/models/responses.py +60 -0
- api/optimization_handlers.py +154 -0
- api/request_pipeline.py +424 -0
- api/routes.py +156 -0
- api/runtime.py +334 -0
- api/validation_log.py +48 -0
- api/web_server_tools.py +22 -0
- api/web_tools/__init__.py +17 -0
- api/web_tools/constants.py +15 -0
- api/web_tools/egress.py +99 -0
- api/web_tools/outbound.py +278 -0
- api/web_tools/parsers.py +104 -0
- api/web_tools/request.py +87 -0
- api/web_tools/streaming.py +206 -0
- cli/__init__.py +5 -0
- cli/claude_env.py +12 -0
- cli/entrypoints.py +166 -0
- cli/env.example +209 -0
- cli/launchers/__init__.py +1 -0
- cli/launchers/claude.py +84 -0
- cli/launchers/codex.py +204 -0
- cli/launchers/codex_model_catalog.py +186 -0
- cli/launchers/common.py +93 -0
- cli/managed/__init__.py +6 -0
- cli/managed/claude.py +215 -0
- cli/managed/manager.py +157 -0
- cli/managed/session.py +260 -0
- cli/process_registry.py +78 -0
- config/__init__.py +5 -0
- config/constants.py +13 -0
- config/logging_config.py +159 -0
- config/nim.py +118 -0
- config/paths.py +91 -0
- config/provider_catalog.py +259 -0
- config/provider_ids.py +7 -0
- config/settings.py +538 -0
- core/__init__.py +1 -0
- core/anthropic/__init__.py +46 -0
- core/anthropic/content.py +31 -0
- core/anthropic/conversion.py +587 -0
- core/anthropic/emitted_sse_tracker.py +346 -0
- core/anthropic/errors.py +70 -0
- core/anthropic/native_messages_request.py +280 -0
- core/anthropic/native_sse_block_policy.py +313 -0
- core/anthropic/provider_stream_error.py +34 -0
- core/anthropic/server_tool_sse.py +14 -0
- core/anthropic/sse.py +440 -0
- core/anthropic/stream_contracts.py +205 -0
- core/anthropic/stream_recovery.py +346 -0
- core/anthropic/stream_recovery_session.py +133 -0
- core/anthropic/thinking.py +140 -0
- core/anthropic/tokens.py +117 -0
- core/anthropic/tools.py +212 -0
- core/anthropic/utils.py +9 -0
- core/openai_responses/__init__.py +5 -0
- core/openai_responses/adapter.py +31 -0
- core/openai_responses/anthropic_sse.py +59 -0
- core/openai_responses/errors.py +22 -0
- core/openai_responses/events.py +19 -0
- core/openai_responses/ids.py +21 -0
- core/openai_responses/input.py +258 -0
- core/openai_responses/items.py +37 -0
- core/openai_responses/reasoning.py +52 -0
- core/openai_responses/stream.py +25 -0
- core/openai_responses/stream_state.py +654 -0
- core/openai_responses/tools.py +374 -0
- core/openai_responses/usage.py +37 -0
- core/rate_limit.py +60 -0
- core/trace.py +216 -0
- devcopilot-0.2.0.dist-info/METADATA +687 -0
- devcopilot-0.2.0.dist-info/RECORD +189 -0
- devcopilot-0.2.0.dist-info/WHEEL +4 -0
- devcopilot-0.2.0.dist-info/entry_points.txt +6 -0
- devcopilot-0.2.0.dist-info/licenses/LICENSE +21 -0
- messaging/__init__.py +26 -0
- messaging/cli_event_constants.py +67 -0
- messaging/command_context.py +66 -0
- messaging/command_dispatcher.py +37 -0
- messaging/commands.py +275 -0
- messaging/event_parser.py +181 -0
- messaging/limiter.py +300 -0
- messaging/models.py +36 -0
- messaging/node_event_pipeline.py +127 -0
- messaging/node_runner.py +342 -0
- messaging/platforms/__init__.py +15 -0
- messaging/platforms/base.py +228 -0
- messaging/platforms/discord.py +567 -0
- messaging/platforms/factory.py +103 -0
- messaging/platforms/outbox.py +144 -0
- messaging/platforms/telegram.py +688 -0
- messaging/platforms/voice_flow.py +295 -0
- messaging/rendering/__init__.py +3 -0
- messaging/rendering/discord_markdown.py +318 -0
- messaging/rendering/markdown_tables.py +49 -0
- messaging/rendering/profiles.py +55 -0
- messaging/rendering/telegram_markdown.py +327 -0
- messaging/safe_diagnostics.py +17 -0
- messaging/session.py +334 -0
- messaging/transcript.py +581 -0
- messaging/transcription.py +164 -0
- messaging/trees/__init__.py +15 -0
- messaging/trees/data.py +482 -0
- messaging/trees/manager.py +433 -0
- messaging/trees/processor.py +179 -0
- messaging/trees/repository.py +177 -0
- messaging/turn_intake.py +235 -0
- messaging/ui_updates.py +101 -0
- messaging/voice.py +76 -0
- messaging/workflow.py +200 -0
- providers/__init__.py +31 -0
- providers/base.py +152 -0
- providers/cerebras/__init__.py +7 -0
- providers/cerebras/client.py +31 -0
- providers/cerebras/request.py +55 -0
- providers/codestral/__init__.py +7 -0
- providers/codestral/client.py +34 -0
- providers/deepseek/__init__.py +11 -0
- providers/deepseek/client.py +51 -0
- providers/deepseek/request.py +475 -0
- providers/defaults.py +41 -0
- providers/error_mapping.py +309 -0
- providers/exceptions.py +113 -0
- providers/fireworks/__init__.py +5 -0
- providers/fireworks/client.py +45 -0
- providers/fireworks/request.py +48 -0
- providers/gemini/__init__.py +7 -0
- providers/gemini/client.py +49 -0
- providers/gemini/request.py +199 -0
- providers/groq/__init__.py +7 -0
- providers/groq/client.py +31 -0
- providers/groq/request.py +83 -0
- providers/kimi/__init__.py +10 -0
- providers/kimi/client.py +53 -0
- providers/kimi/request.py +42 -0
- providers/llamacpp/__init__.py +3 -0
- providers/llamacpp/client.py +16 -0
- providers/lmstudio/__init__.py +5 -0
- providers/lmstudio/client.py +16 -0
- providers/mistral/__init__.py +7 -0
- providers/mistral/client.py +31 -0
- providers/mistral/request.py +37 -0
- providers/model_listing.py +133 -0
- providers/nvidia_nim/__init__.py +7 -0
- providers/nvidia_nim/client.py +91 -0
- providers/nvidia_nim/request.py +430 -0
- providers/nvidia_nim/voice.py +95 -0
- providers/ollama/__init__.py +7 -0
- providers/ollama/client.py +39 -0
- providers/open_router/__init__.py +7 -0
- providers/open_router/client.py +124 -0
- providers/open_router/request.py +42 -0
- providers/opencode/__init__.py +11 -0
- providers/opencode/client.py +31 -0
- providers/opencode/request.py +35 -0
- providers/rate_limit.py +300 -0
- providers/registry.py +527 -0
- providers/transports/__init__.py +1 -0
- providers/transports/anthropic_messages/__init__.py +5 -0
- providers/transports/anthropic_messages/http.py +118 -0
- providers/transports/anthropic_messages/recovery.py +206 -0
- providers/transports/anthropic_messages/stream.py +295 -0
- providers/transports/anthropic_messages/transport.py +236 -0
- providers/transports/openai_chat/__init__.py +5 -0
- providers/transports/openai_chat/recovery.py +217 -0
- providers/transports/openai_chat/stream.py +384 -0
- providers/transports/openai_chat/tool_calls.py +293 -0
- providers/transports/openai_chat/transport.py +156 -0
- providers/wafer/__init__.py +10 -0
- providers/wafer/client.py +50 -0
- providers/zai/__init__.py +10 -0
- providers/zai/client.py +46 -0
- providers/zai/request.py +42 -0
cli/entrypoints.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""CLI entry points for the installed package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
import webbrowser
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import uvicorn
|
|
13
|
+
|
|
14
|
+
from api.admin_urls import local_admin_url, local_proxy_root_url
|
|
15
|
+
from api.app import GracefulLifespanApp, create_app
|
|
16
|
+
from cli.launchers.common import preflight_proxy
|
|
17
|
+
from cli.process_registry import (
|
|
18
|
+
kill_all_best_effort,
|
|
19
|
+
)
|
|
20
|
+
from config.paths import (
|
|
21
|
+
config_dir_path,
|
|
22
|
+
legacy_env_paths,
|
|
23
|
+
managed_env_path,
|
|
24
|
+
)
|
|
25
|
+
from config.settings import Settings, get_settings
|
|
26
|
+
|
|
27
|
+
SERVER_GRACEFUL_SHUTDOWN_SECONDS = 5
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _load_env_template() -> str:
|
|
31
|
+
"""Load the canonical root env template from package resources or source."""
|
|
32
|
+
import importlib.resources
|
|
33
|
+
|
|
34
|
+
packaged = importlib.resources.files("cli").joinpath("env.example")
|
|
35
|
+
if packaged.is_file():
|
|
36
|
+
return packaged.read_text("utf-8")
|
|
37
|
+
|
|
38
|
+
source_template = Path(__file__).resolve().parents[1] / ".env.example"
|
|
39
|
+
if source_template.is_file():
|
|
40
|
+
return source_template.read_text(encoding="utf-8")
|
|
41
|
+
|
|
42
|
+
raise FileNotFoundError("Could not find bundled or source .env.example template.")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def serve() -> None:
|
|
46
|
+
"""Start the FastAPI server (registered as `devcopilot-server` script)."""
|
|
47
|
+
opened_admin_browser = False
|
|
48
|
+
try:
|
|
49
|
+
try:
|
|
50
|
+
while True:
|
|
51
|
+
_migrate_legacy_env_if_missing()
|
|
52
|
+
settings = get_settings()
|
|
53
|
+
if not _run_supervised_server(
|
|
54
|
+
settings, open_admin_browser=not opened_admin_browser
|
|
55
|
+
):
|
|
56
|
+
return
|
|
57
|
+
opened_admin_browser = True
|
|
58
|
+
get_settings.cache_clear()
|
|
59
|
+
except KeyboardInterrupt:
|
|
60
|
+
return
|
|
61
|
+
finally:
|
|
62
|
+
kill_all_best_effort()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _admin_browser_open_enabled() -> bool:
|
|
66
|
+
"""Whether to open /admin when the server becomes reachable (FCC_OPEN_BROWSER)."""
|
|
67
|
+
|
|
68
|
+
raw = os.environ.get("FCC_OPEN_BROWSER", "true").strip().lower()
|
|
69
|
+
return raw not in {"", "0", "false", "no"}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _schedule_open_admin_browser(settings: Settings) -> None:
|
|
73
|
+
"""After /health succeeds, open the admin UI in the default browser (daemon thread)."""
|
|
74
|
+
|
|
75
|
+
if not _admin_browser_open_enabled():
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
admin_url = local_admin_url(settings)
|
|
79
|
+
proxy_root_url = local_proxy_root_url(settings)
|
|
80
|
+
|
|
81
|
+
def open_when_ready() -> None:
|
|
82
|
+
deadline = time.monotonic() + 30.0
|
|
83
|
+
while time.monotonic() < deadline:
|
|
84
|
+
if preflight_proxy(proxy_root_url) is None:
|
|
85
|
+
webbrowser.open(admin_url)
|
|
86
|
+
return
|
|
87
|
+
time.sleep(0.15)
|
|
88
|
+
|
|
89
|
+
threading.Thread(
|
|
90
|
+
target=open_when_ready, name="devcopilot-open-admin-browser", daemon=True
|
|
91
|
+
).start()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _run_supervised_server(settings: Settings, *, open_admin_browser: bool) -> bool:
|
|
95
|
+
"""Run one uvicorn server instance; return whether admin requested restart."""
|
|
96
|
+
|
|
97
|
+
restart_requested = False
|
|
98
|
+
server_holder: dict[str, uvicorn.Server] = {}
|
|
99
|
+
|
|
100
|
+
def request_restart() -> None:
|
|
101
|
+
nonlocal restart_requested
|
|
102
|
+
restart_requested = True
|
|
103
|
+
if server := server_holder.get("server"):
|
|
104
|
+
server.should_exit = True
|
|
105
|
+
|
|
106
|
+
app = create_app(lifespan_enabled=False)
|
|
107
|
+
app.state.admin_restart_callback = request_restart
|
|
108
|
+
asgi_app = GracefulLifespanApp(app)
|
|
109
|
+
config = uvicorn.Config(
|
|
110
|
+
asgi_app,
|
|
111
|
+
host=settings.host,
|
|
112
|
+
port=settings.port,
|
|
113
|
+
log_level="debug",
|
|
114
|
+
timeout_graceful_shutdown=SERVER_GRACEFUL_SHUTDOWN_SECONDS,
|
|
115
|
+
)
|
|
116
|
+
server = uvicorn.Server(config)
|
|
117
|
+
server_holder["server"] = server
|
|
118
|
+
if open_admin_browser:
|
|
119
|
+
_schedule_open_admin_browser(settings)
|
|
120
|
+
server.run()
|
|
121
|
+
return restart_requested
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def init() -> None:
|
|
125
|
+
"""Scaffold config at ~/.fcc/.env (registered as `devcopilot-init`)."""
|
|
126
|
+
config_dir = config_dir_path()
|
|
127
|
+
env_file = managed_env_path()
|
|
128
|
+
|
|
129
|
+
migrated_from = _migrate_legacy_env_if_missing()
|
|
130
|
+
if migrated_from is not None:
|
|
131
|
+
print(f"Config migrated from {migrated_from} to {env_file}")
|
|
132
|
+
print(
|
|
133
|
+
"Edit it to set your API keys and model preferences, then run: devcopilot-server"
|
|
134
|
+
)
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
if env_file.exists():
|
|
138
|
+
print(f"Config already exists at {env_file}")
|
|
139
|
+
print("Delete it first if you want to reset to defaults.")
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
143
|
+
template = _load_env_template()
|
|
144
|
+
env_file.write_text(template, encoding="utf-8")
|
|
145
|
+
print(f"Config created at {env_file}")
|
|
146
|
+
print(
|
|
147
|
+
"Edit it to set your API keys and model preferences, then run: devcopilot-server"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _migrate_legacy_env_if_missing() -> Path | None:
|
|
152
|
+
"""Copy a legacy user env into the managed config path when absent."""
|
|
153
|
+
|
|
154
|
+
env_file = managed_env_path()
|
|
155
|
+
if env_file.exists():
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
# TODO: Remove after the ~/.fcc/.env migration has had a release cycle.
|
|
159
|
+
for legacy_env in legacy_env_paths():
|
|
160
|
+
if not legacy_env.is_file():
|
|
161
|
+
continue
|
|
162
|
+
env_file.parent.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
shutil.copyfile(legacy_env, env_file)
|
|
164
|
+
return legacy_env
|
|
165
|
+
|
|
166
|
+
return None
|
cli/env.example
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# NVIDIA NIM Config
|
|
2
|
+
NVIDIA_NIM_API_KEY=""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
# OpenRouter Config
|
|
6
|
+
OPENROUTER_API_KEY=""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Mistral La Plateforme Config (Experiment plan free tier – rate limits; OpenAI-compatible at api.mistral.ai/v1)
|
|
10
|
+
MISTRAL_API_KEY=""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Mistral Codestral (separate key from La Plateforme; OpenAI-compatible at codestral.mistral.ai/v1)
|
|
14
|
+
CODESTRAL_API_KEY=""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# DeepSeek Config (uses native Anthropic Messages at api.deepseek.com/anthropic)
|
|
18
|
+
DEEPSEEK_API_KEY=""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Kimi Config (Anthropic-compatible Messages at api.moonshot.ai/anthropic/v1)
|
|
22
|
+
KIMI_API_KEY=""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Wafer Config (uses native Anthropic Messages at pass.wafer.ai/v1/messages)
|
|
26
|
+
WAFER_API_KEY=""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# OpenCode Zen (opencode.ai/zen/v1) and OpenCode Go (opencode.ai/zen/go/v1) share OPENCODE_API_KEY
|
|
30
|
+
OPENCODE_API_KEY=""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Z.ai Config (Anthropic-compatible Messages at api.z.ai/api/anthropic/v1)
|
|
34
|
+
ZAI_API_KEY=""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Fireworks AI Config (Anthropic-compatible Messages at api.fireworks.ai/inference/v1)
|
|
38
|
+
FIREWORKS_API_KEY=""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Gemini / Google AI Studio (OpenAI-compatible Chat Completions; see https://ai.google.dev/gemini-api/docs/openai)
|
|
42
|
+
GEMINI_API_KEY=""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Groq Cloud (OpenAI-compatible Chat Completions; see https://console.groq.com/docs/openai)
|
|
46
|
+
GROQ_API_KEY=""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Cerebras Inference (OpenAI-compatible Chat Completions; see https://inference-docs.cerebras.ai/resources/openai)
|
|
50
|
+
CEREBRAS_API_KEY=""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# LM Studio Config (local provider, no API key required)
|
|
54
|
+
LM_STUDIO_BASE_URL="http://localhost:1234/v1"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Llama.cpp Config (local provider, no API key required)
|
|
58
|
+
LLAMACPP_BASE_URL="http://localhost:8080/v1"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# Ollama Config (local provider, no API key required)
|
|
62
|
+
OLLAMA_BASE_URL="http://localhost:11434"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# All Claude model requests are mapped to these models, plain model is fallback
|
|
66
|
+
# Format: provider_type/model/name
|
|
67
|
+
# Valid providers: "nvidia_nim" | "open_router" | "gemini" | "deepseek" | "mistral" | "mistral_codestral" | "opencode" | "opencode_go" | "wafer" | "kimi" | "cerebras" | "groq" | "fireworks" | "zai" | "lmstudio" | "llamacpp" | "ollama"
|
|
68
|
+
MODEL_OPUS=
|
|
69
|
+
MODEL_SONNET=
|
|
70
|
+
MODEL_HAIKU=
|
|
71
|
+
MODEL="nvidia_nim/nvidia/nemotron-3-super-120b-a12b"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# Optional live smoke model overrides. Provider smoke runs once per configured
|
|
75
|
+
# provider even when MODEL/MODEL_* route to a different provider.
|
|
76
|
+
DEVCOPILOT_SMOKE_MODEL_NVIDIA_NIM=
|
|
77
|
+
DEVCOPILOT_SMOKE_MODEL_OPEN_ROUTER=
|
|
78
|
+
DEVCOPILOT_SMOKE_MODEL_MISTRAL=
|
|
79
|
+
DEVCOPILOT_SMOKE_MODEL_MISTRAL_CODESTRAL=
|
|
80
|
+
DEVCOPILOT_SMOKE_MODEL_DEEPSEEK=
|
|
81
|
+
DEVCOPILOT_SMOKE_MODEL_LMSTUDIO=
|
|
82
|
+
DEVCOPILOT_SMOKE_MODEL_LLAMACPP=
|
|
83
|
+
DEVCOPILOT_SMOKE_MODEL_OLLAMA=
|
|
84
|
+
FCC_SMOKE_MODEL_KIMI=
|
|
85
|
+
FCC_SMOKE_MODEL_WAFER=
|
|
86
|
+
FCC_SMOKE_MODEL_OPENCODE=
|
|
87
|
+
FCC_SMOKE_MODEL_OPENCODE_GO=
|
|
88
|
+
FCC_SMOKE_MODEL_ZAI=
|
|
89
|
+
FCC_SMOKE_MODEL_FIREWORKS=
|
|
90
|
+
FCC_SMOKE_MODEL_GEMINI=
|
|
91
|
+
FCC_SMOKE_MODEL_GROQ=
|
|
92
|
+
FCC_SMOKE_MODEL_CEREBRAS=
|
|
93
|
+
FCC_SMOKE_NIM_MODELS=
|
|
94
|
+
FCC_SMOKE_NIM_EXTRA_MODELS=
|
|
95
|
+
FCC_SMOKE_OPENROUTER_FREE_MODELS=
|
|
96
|
+
FCC_SMOKE_OPENROUTER_FREE_EXTRA_MODELS=
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Thinking output
|
|
100
|
+
# Per-Claude-model switches for provider reasoning requests and Claude thinking blocks.
|
|
101
|
+
# Blank per-model switches inherit ENABLE_MODEL_THINKING.
|
|
102
|
+
ENABLE_OPUS_THINKING=
|
|
103
|
+
ENABLE_SONNET_THINKING=
|
|
104
|
+
ENABLE_HAIKU_THINKING=
|
|
105
|
+
ENABLE_MODEL_THINKING=true
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# Provider config
|
|
109
|
+
# Per-provider proxy support: http and socks5, example: "http://username:password@host:port"
|
|
110
|
+
NVIDIA_NIM_PROXY=""
|
|
111
|
+
OPENROUTER_PROXY=""
|
|
112
|
+
MISTRAL_PROXY=""
|
|
113
|
+
CODESTRAL_PROXY=""
|
|
114
|
+
LMSTUDIO_PROXY=""
|
|
115
|
+
LLAMACPP_PROXY=""
|
|
116
|
+
KIMI_PROXY=""
|
|
117
|
+
WAFER_PROXY=""
|
|
118
|
+
OPENCODE_PROXY=""
|
|
119
|
+
OPENCODE_GO_PROXY=""
|
|
120
|
+
ZAI_PROXY=""
|
|
121
|
+
FIREWORKS_PROXY=""
|
|
122
|
+
GEMINI_PROXY=""
|
|
123
|
+
GROQ_PROXY=""
|
|
124
|
+
CEREBRAS_PROXY=""
|
|
125
|
+
|
|
126
|
+
PROVIDER_RATE_LIMIT=1
|
|
127
|
+
PROVIDER_RATE_WINDOW=3
|
|
128
|
+
PROVIDER_MAX_CONCURRENCY=5
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# HTTP client timeouts (seconds) for provider API requests
|
|
132
|
+
HTTP_READ_TIMEOUT=300
|
|
133
|
+
HTTP_WRITE_TIMEOUT=60
|
|
134
|
+
HTTP_CONNECT_TIMEOUT=60
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# Optional server API key (Anthropic-style)
|
|
138
|
+
ANTHROPIC_AUTH_TOKEN="freecc"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# Open /admin in the default browser when fcc-server becomes healthy (set 0/false/no to disable)
|
|
142
|
+
FCC_OPEN_BROWSER=true
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# Messaging Platform: "telegram" | "discord" | "none"
|
|
146
|
+
MESSAGING_PLATFORM="discord"
|
|
147
|
+
MESSAGING_RATE_LIMIT=1
|
|
148
|
+
MESSAGING_RATE_WINDOW=1
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# Voice Note Transcription
|
|
152
|
+
VOICE_NOTE_ENABLED=false
|
|
153
|
+
# WHISPER_DEVICE: "cpu" | "cuda" | "nvidia_nim"
|
|
154
|
+
# - "cpu"/"cuda": Hugging Face transformers Whisper (offline, free; install with: uv sync --extra voice_local)
|
|
155
|
+
# - "nvidia_nim": NVIDIA NIM Whisper via Riva gRPC (requires NVIDIA_NIM_API_KEY; install with: uv sync --extra voice)
|
|
156
|
+
# (Independent of MODEL=nvidia_nim/...: that selects the *chat* provider; this selects voice STT only.)
|
|
157
|
+
WHISPER_DEVICE="nvidia_nim"
|
|
158
|
+
# WHISPER_MODEL:
|
|
159
|
+
# - For cpu/cuda: Hugging Face ID or short name (tiny, base, small, medium, large-v2, large-v3, large-v3-turbo)
|
|
160
|
+
# - For nvidia_nim: NVIDIA NIM model (e.g., "nvidia/parakeet-ctc-1.1b-asr", "openai/whisper-large-v3")
|
|
161
|
+
# - For nvidia_nim, default to "openai/whisper-large-v3" for best performance
|
|
162
|
+
WHISPER_MODEL="openai/whisper-large-v3"
|
|
163
|
+
HF_TOKEN=""
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# Telegram Config
|
|
167
|
+
TELEGRAM_BOT_TOKEN=""
|
|
168
|
+
ALLOWED_TELEGRAM_USER_ID=""
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# Discord Config
|
|
172
|
+
DISCORD_BOT_TOKEN=""
|
|
173
|
+
ALLOWED_DISCORD_CHANNELS=""
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# Agent Config
|
|
177
|
+
ALLOWED_DIR=""
|
|
178
|
+
FAST_PREFIX_DETECTION=true
|
|
179
|
+
ENABLE_NETWORK_PROBE_MOCK=true
|
|
180
|
+
ENABLE_TITLE_GENERATION_SKIP=true
|
|
181
|
+
ENABLE_SUGGESTION_MODE_SKIP=true
|
|
182
|
+
ENABLE_FILEPATH_EXTRACTION_MOCK=true
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# Local Anthropic web_search / web_fetch handling (performs outbound HTTP; on by default)
|
|
186
|
+
ENABLE_WEB_SERVER_TOOLS=true
|
|
187
|
+
WEB_FETCH_ALLOWED_SCHEMES=http,https
|
|
188
|
+
WEB_FETCH_ALLOW_PRIVATE_NETWORKS=false
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# Structured TRACE logs: lines with `"trace": true` merge ingress/routing/cli/provider/egress
|
|
192
|
+
# stages. Conversation text is logged in those payloads (verbatim). Values under keys named
|
|
193
|
+
# like ``api_key`` / ``authorization`` are redacted. Raw transport payloads still require
|
|
194
|
+
# the LOG_RAW_* toggles below.
|
|
195
|
+
#
|
|
196
|
+
# Verbose diagnostics (avoid logging raw prompts / SSE bodies in production)
|
|
197
|
+
DEBUG_PLATFORM_EDITS=false
|
|
198
|
+
DEBUG_SUBAGENT_STACK=false
|
|
199
|
+
# When true, also allows DEBUG-level httpx/httpcore/telegram log noise (not just payload logging).
|
|
200
|
+
LOG_RAW_API_PAYLOADS=false
|
|
201
|
+
LOG_RAW_SSE_EVENTS=false
|
|
202
|
+
# When true, log full exception text and tracebacks for unhandled errors (may leak request-derived data).
|
|
203
|
+
LOG_API_ERROR_TRACEBACKS=false
|
|
204
|
+
# When true, log message/transcription text previews in messaging adapters only (handler ingress always TRACEs verbatim text separately).
|
|
205
|
+
LOG_RAW_MESSAGING_CONTENT=false
|
|
206
|
+
# When true, log full Claude CLI stderr, non-JSON stdout lines, and parser error text.
|
|
207
|
+
LOG_RAW_CLI_DIAGNOSTICS=false
|
|
208
|
+
# When true, log full exception and CLI error message strings in messaging (may leak user content).
|
|
209
|
+
LOG_MESSAGING_ERROR_DETAILS=false
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Installed FCC client CLI launchers."""
|
cli/launchers/claude.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Installed `devcopilot-claude` launcher."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Mapping, Sequence
|
|
8
|
+
|
|
9
|
+
from api.admin_urls import local_proxy_root_url
|
|
10
|
+
from cli.claude_env import CLAUDE_CODE_AUTO_COMPACT_WINDOW, claude_auth_token
|
|
11
|
+
from config.settings import Settings, get_settings
|
|
12
|
+
|
|
13
|
+
from .common import preflight_proxy, resolve_client_binary, run_client_process
|
|
14
|
+
|
|
15
|
+
_DISPLAY_NAME = "Claude Code"
|
|
16
|
+
_DEFAULT_BINARY = "claude"
|
|
17
|
+
_INSTALL_HINT = "Install Claude Code with: npm install -g @anthropic-ai/claude-code"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def launch(argv: Sequence[str] | None = None) -> None:
|
|
21
|
+
"""Launch Claude Code with DevCopilot proxy environment variables."""
|
|
22
|
+
|
|
23
|
+
settings = get_settings()
|
|
24
|
+
proxy_root_url = local_proxy_root_url(settings)
|
|
25
|
+
if error := preflight_proxy(proxy_root_url):
|
|
26
|
+
print(
|
|
27
|
+
f"DevCopilot proxy is not reachable at {proxy_root_url}: {error}",
|
|
28
|
+
file=sys.stderr,
|
|
29
|
+
)
|
|
30
|
+
print("Start it in another terminal with: devcopilot-server", file=sys.stderr)
|
|
31
|
+
raise SystemExit(1)
|
|
32
|
+
|
|
33
|
+
binary_name = claude_binary_name(settings)
|
|
34
|
+
binary_path = resolve_client_binary(
|
|
35
|
+
binary_name=binary_name,
|
|
36
|
+
display_name=_DISPLAY_NAME,
|
|
37
|
+
install_hint=_INSTALL_HINT,
|
|
38
|
+
)
|
|
39
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
40
|
+
run_client_process(
|
|
41
|
+
command=build_claude_launcher_command(binary_path=binary_path, argv=args),
|
|
42
|
+
env=build_claude_launcher_env(
|
|
43
|
+
proxy_root_url=proxy_root_url,
|
|
44
|
+
auth_token=settings.anthropic_auth_token,
|
|
45
|
+
base_env=os.environ,
|
|
46
|
+
),
|
|
47
|
+
binary_name=binary_name,
|
|
48
|
+
display_name=_DISPLAY_NAME,
|
|
49
|
+
install_hint=_INSTALL_HINT,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def claude_binary_name(settings: Settings) -> str:
|
|
54
|
+
"""Return the configured Claude Code binary name."""
|
|
55
|
+
|
|
56
|
+
return settings.claude_cli_bin or _DEFAULT_BINARY
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def build_claude_launcher_command(
|
|
60
|
+
*, binary_path: str, argv: Sequence[str]
|
|
61
|
+
) -> list[str]:
|
|
62
|
+
"""Return the Claude wrapper command without changing user arguments."""
|
|
63
|
+
|
|
64
|
+
return [binary_path, *argv]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def build_claude_launcher_env(
|
|
68
|
+
*,
|
|
69
|
+
proxy_root_url: str,
|
|
70
|
+
auth_token: str,
|
|
71
|
+
base_env: Mapping[str, str],
|
|
72
|
+
) -> dict[str, str]:
|
|
73
|
+
"""Return a Claude Code environment that targets the local proxy."""
|
|
74
|
+
|
|
75
|
+
env = {
|
|
76
|
+
key: value
|
|
77
|
+
for key, value in base_env.items()
|
|
78
|
+
if not key.startswith("ANTHROPIC_")
|
|
79
|
+
}
|
|
80
|
+
env["ANTHROPIC_BASE_URL"] = proxy_root_url
|
|
81
|
+
env["CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY"] = "1"
|
|
82
|
+
env["CLAUDE_CODE_AUTO_COMPACT_WINDOW"] = CLAUDE_CODE_AUTO_COMPACT_WINDOW
|
|
83
|
+
env["ANTHROPIC_AUTH_TOKEN"] = claude_auth_token(auth_token)
|
|
84
|
+
return env
|
cli/launchers/codex.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Installed `devcopilot-codex` launcher."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from collections.abc import Mapping, Sequence
|
|
9
|
+
from urllib.request import Request, urlopen
|
|
10
|
+
|
|
11
|
+
from api.admin_urls import local_proxy_root_url
|
|
12
|
+
from config.paths import codex_model_catalog_path
|
|
13
|
+
from config.settings import Settings, get_settings
|
|
14
|
+
|
|
15
|
+
from .codex_model_catalog import build_codex_model_catalog, write_codex_model_catalog
|
|
16
|
+
from .common import (
|
|
17
|
+
PROXY_PREFLIGHT_TIMEOUT_SECONDS,
|
|
18
|
+
preflight_proxy,
|
|
19
|
+
resolve_client_binary,
|
|
20
|
+
run_client_process,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_CODEX_AUTH_ENV_KEY = "FCC_CODEX_API_KEY"
|
|
24
|
+
_DISPLAY_NAME = "Codex CLI"
|
|
25
|
+
_DEFAULT_BINARY = "codex"
|
|
26
|
+
_INSTALL_HINT = "Install Codex with: npm install -g @openai/codex"
|
|
27
|
+
_STRIPPED_CODEX_ENV_KEYS = frozenset(
|
|
28
|
+
{
|
|
29
|
+
"OPENAI_API_KEY",
|
|
30
|
+
"OPENAI_BASE_URL",
|
|
31
|
+
"OPENAI_API_BASE",
|
|
32
|
+
"OPENAI_ORG_ID",
|
|
33
|
+
"OPENAI_ORGANIZATION",
|
|
34
|
+
"CODEX_API_KEY",
|
|
35
|
+
_CODEX_AUTH_ENV_KEY,
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def launch(argv: Sequence[str] | None = None) -> None:
|
|
41
|
+
"""Launch Codex CLI with DevCopilot proxy configuration."""
|
|
42
|
+
|
|
43
|
+
settings = get_settings()
|
|
44
|
+
proxy_root_url = local_proxy_root_url(settings)
|
|
45
|
+
if error := preflight_proxy(proxy_root_url):
|
|
46
|
+
print(
|
|
47
|
+
f"DevCopilot proxy is not reachable at {proxy_root_url}: {error}",
|
|
48
|
+
file=sys.stderr,
|
|
49
|
+
)
|
|
50
|
+
print("Start it in another terminal with: devcopilot-server", file=sys.stderr)
|
|
51
|
+
raise SystemExit(1)
|
|
52
|
+
|
|
53
|
+
binary_name = codex_binary_name(settings)
|
|
54
|
+
binary_path = resolve_client_binary(
|
|
55
|
+
binary_name=binary_name,
|
|
56
|
+
display_name=_DISPLAY_NAME,
|
|
57
|
+
install_hint=_INSTALL_HINT,
|
|
58
|
+
)
|
|
59
|
+
catalog_args = codex_model_catalog_config_args(proxy_root_url, settings)
|
|
60
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
61
|
+
run_client_process(
|
|
62
|
+
command=build_codex_launcher_command(
|
|
63
|
+
binary_path=binary_path,
|
|
64
|
+
argv=args,
|
|
65
|
+
settings=settings,
|
|
66
|
+
proxy_root_url=proxy_root_url,
|
|
67
|
+
catalog_config_args=catalog_args,
|
|
68
|
+
),
|
|
69
|
+
env=build_codex_launcher_env(
|
|
70
|
+
auth_token=settings.anthropic_auth_token,
|
|
71
|
+
base_env=os.environ,
|
|
72
|
+
),
|
|
73
|
+
binary_name=binary_name,
|
|
74
|
+
display_name=_DISPLAY_NAME,
|
|
75
|
+
install_hint=_INSTALL_HINT,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def codex_binary_name(settings: Settings) -> str:
|
|
80
|
+
"""Return the configured Codex binary name."""
|
|
81
|
+
|
|
82
|
+
return settings.codex_cli_bin or _DEFAULT_BINARY
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def build_codex_launcher_command(
|
|
86
|
+
*,
|
|
87
|
+
binary_path: str,
|
|
88
|
+
argv: Sequence[str],
|
|
89
|
+
settings: Settings,
|
|
90
|
+
proxy_root_url: str,
|
|
91
|
+
catalog_config_args: Sequence[str] = (),
|
|
92
|
+
) -> list[str]:
|
|
93
|
+
"""Return a Codex command with ephemeral DevCopilot provider config."""
|
|
94
|
+
|
|
95
|
+
return [
|
|
96
|
+
binary_path,
|
|
97
|
+
*catalog_config_args,
|
|
98
|
+
*codex_config_args(
|
|
99
|
+
api_url=_ensure_v1_url(proxy_root_url),
|
|
100
|
+
model=getattr(settings, "model", None),
|
|
101
|
+
),
|
|
102
|
+
*argv,
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def build_codex_launcher_env(
|
|
107
|
+
*,
|
|
108
|
+
auth_token: str,
|
|
109
|
+
base_env: Mapping[str, str],
|
|
110
|
+
) -> dict[str, str]:
|
|
111
|
+
"""Return a Codex environment that targets the local proxy provider."""
|
|
112
|
+
|
|
113
|
+
env = {
|
|
114
|
+
key: value
|
|
115
|
+
for key, value in base_env.items()
|
|
116
|
+
if key not in _STRIPPED_CODEX_ENV_KEYS and not key.startswith("OPENAI_")
|
|
117
|
+
}
|
|
118
|
+
env[_CODEX_AUTH_ENV_KEY] = auth_token.strip() or "fcc-no-auth"
|
|
119
|
+
return env
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def codex_model_catalog_config_args(
|
|
123
|
+
proxy_root_url: str, settings: Settings
|
|
124
|
+
) -> list[str]:
|
|
125
|
+
"""Prepare the generated Codex model catalog and return its config args."""
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
models_response = fetch_proxy_models_response(
|
|
129
|
+
proxy_root_url, settings.anthropic_auth_token
|
|
130
|
+
)
|
|
131
|
+
catalog = build_codex_model_catalog(models_response)
|
|
132
|
+
models = catalog.get("models")
|
|
133
|
+
if not isinstance(models, list) or not models:
|
|
134
|
+
print(
|
|
135
|
+
"DevCopilot warning: Codex model catalog is empty; "
|
|
136
|
+
"launching without model picker catalog.",
|
|
137
|
+
file=sys.stderr,
|
|
138
|
+
)
|
|
139
|
+
return []
|
|
140
|
+
catalog_path = codex_model_catalog_path()
|
|
141
|
+
write_codex_model_catalog(catalog_path, catalog)
|
|
142
|
+
except Exception as exc:
|
|
143
|
+
print(
|
|
144
|
+
"DevCopilot warning: could not prepare Codex model catalog "
|
|
145
|
+
f"({exc}); launching without model picker catalog.",
|
|
146
|
+
file=sys.stderr,
|
|
147
|
+
)
|
|
148
|
+
return []
|
|
149
|
+
|
|
150
|
+
return build_model_catalog_config_args(str(catalog_path))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def fetch_proxy_models_response(
|
|
154
|
+
proxy_root_url: str, auth_token: str
|
|
155
|
+
) -> dict[str, object]:
|
|
156
|
+
"""Fetch the local proxy `/v1/models` response for Codex catalog generation."""
|
|
157
|
+
|
|
158
|
+
url = f"{proxy_root_url.rstrip('/')}/v1/models"
|
|
159
|
+
headers: dict[str, str] = {}
|
|
160
|
+
if token := auth_token.strip():
|
|
161
|
+
headers["X-API-Key"] = token
|
|
162
|
+
|
|
163
|
+
request = Request(url, headers=headers, method="GET")
|
|
164
|
+
with urlopen(request, timeout=PROXY_PREFLIGHT_TIMEOUT_SECONDS) as response:
|
|
165
|
+
payload = json.loads(response.read().decode("utf-8"))
|
|
166
|
+
|
|
167
|
+
if not isinstance(payload, dict):
|
|
168
|
+
raise ValueError("model list response was not a JSON object")
|
|
169
|
+
return payload
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def build_model_catalog_config_args(catalog_path: str) -> list[str]:
|
|
173
|
+
"""Return Codex config args for a generated model catalog."""
|
|
174
|
+
|
|
175
|
+
return ["-c", _toml_assignment("model_catalog_json", catalog_path)]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def codex_config_args(*, api_url: str, model: str | None = None) -> list[str]:
|
|
179
|
+
"""Return Codex `-c` assignments for the ephemeral FCC provider."""
|
|
180
|
+
|
|
181
|
+
args = [
|
|
182
|
+
"-c",
|
|
183
|
+
_toml_assignment("model_provider", "fcc"),
|
|
184
|
+
"-c",
|
|
185
|
+
_toml_assignment("model_providers.fcc.name", "DevCopilot"),
|
|
186
|
+
"-c",
|
|
187
|
+
_toml_assignment("model_providers.fcc.base_url", _ensure_v1_url(api_url)),
|
|
188
|
+
"-c",
|
|
189
|
+
_toml_assignment("model_providers.fcc.env_key", _CODEX_AUTH_ENV_KEY),
|
|
190
|
+
"-c",
|
|
191
|
+
_toml_assignment("model_providers.fcc.wire_api", "responses"),
|
|
192
|
+
]
|
|
193
|
+
if model:
|
|
194
|
+
args.extend(["-c", _toml_assignment("model", model)])
|
|
195
|
+
return args
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _ensure_v1_url(url: str) -> str:
|
|
199
|
+
stripped = url.rstrip("/")
|
|
200
|
+
return stripped if stripped.endswith("/v1") else f"{stripped}/v1"
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _toml_assignment(key: str, value: str) -> str:
|
|
204
|
+
return f"{key}={json.dumps(value)}"
|