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.
Files changed (189) hide show
  1. api/__init__.py +17 -0
  2. api/admin_config.py +1303 -0
  3. api/admin_routes.py +287 -0
  4. api/admin_static/admin.css +459 -0
  5. api/admin_static/admin.js +497 -0
  6. api/admin_static/index.html +77 -0
  7. api/admin_urls.py +34 -0
  8. api/app.py +194 -0
  9. api/command_utils.py +164 -0
  10. api/dependencies.py +144 -0
  11. api/detection.py +152 -0
  12. api/gateway_model_ids.py +54 -0
  13. api/model_catalog.py +133 -0
  14. api/model_router.py +125 -0
  15. api/models/__init__.py +45 -0
  16. api/models/anthropic.py +234 -0
  17. api/models/openai_responses.py +28 -0
  18. api/models/responses.py +60 -0
  19. api/optimization_handlers.py +154 -0
  20. api/request_pipeline.py +424 -0
  21. api/routes.py +156 -0
  22. api/runtime.py +334 -0
  23. api/validation_log.py +48 -0
  24. api/web_server_tools.py +22 -0
  25. api/web_tools/__init__.py +17 -0
  26. api/web_tools/constants.py +15 -0
  27. api/web_tools/egress.py +99 -0
  28. api/web_tools/outbound.py +278 -0
  29. api/web_tools/parsers.py +104 -0
  30. api/web_tools/request.py +87 -0
  31. api/web_tools/streaming.py +206 -0
  32. cli/__init__.py +5 -0
  33. cli/claude_env.py +12 -0
  34. cli/entrypoints.py +166 -0
  35. cli/env.example +209 -0
  36. cli/launchers/__init__.py +1 -0
  37. cli/launchers/claude.py +84 -0
  38. cli/launchers/codex.py +204 -0
  39. cli/launchers/codex_model_catalog.py +186 -0
  40. cli/launchers/common.py +93 -0
  41. cli/managed/__init__.py +6 -0
  42. cli/managed/claude.py +215 -0
  43. cli/managed/manager.py +157 -0
  44. cli/managed/session.py +260 -0
  45. cli/process_registry.py +78 -0
  46. config/__init__.py +5 -0
  47. config/constants.py +13 -0
  48. config/logging_config.py +159 -0
  49. config/nim.py +118 -0
  50. config/paths.py +91 -0
  51. config/provider_catalog.py +259 -0
  52. config/provider_ids.py +7 -0
  53. config/settings.py +538 -0
  54. core/__init__.py +1 -0
  55. core/anthropic/__init__.py +46 -0
  56. core/anthropic/content.py +31 -0
  57. core/anthropic/conversion.py +587 -0
  58. core/anthropic/emitted_sse_tracker.py +346 -0
  59. core/anthropic/errors.py +70 -0
  60. core/anthropic/native_messages_request.py +280 -0
  61. core/anthropic/native_sse_block_policy.py +313 -0
  62. core/anthropic/provider_stream_error.py +34 -0
  63. core/anthropic/server_tool_sse.py +14 -0
  64. core/anthropic/sse.py +440 -0
  65. core/anthropic/stream_contracts.py +205 -0
  66. core/anthropic/stream_recovery.py +346 -0
  67. core/anthropic/stream_recovery_session.py +133 -0
  68. core/anthropic/thinking.py +140 -0
  69. core/anthropic/tokens.py +117 -0
  70. core/anthropic/tools.py +212 -0
  71. core/anthropic/utils.py +9 -0
  72. core/openai_responses/__init__.py +5 -0
  73. core/openai_responses/adapter.py +31 -0
  74. core/openai_responses/anthropic_sse.py +59 -0
  75. core/openai_responses/errors.py +22 -0
  76. core/openai_responses/events.py +19 -0
  77. core/openai_responses/ids.py +21 -0
  78. core/openai_responses/input.py +258 -0
  79. core/openai_responses/items.py +37 -0
  80. core/openai_responses/reasoning.py +52 -0
  81. core/openai_responses/stream.py +25 -0
  82. core/openai_responses/stream_state.py +654 -0
  83. core/openai_responses/tools.py +374 -0
  84. core/openai_responses/usage.py +37 -0
  85. core/rate_limit.py +60 -0
  86. core/trace.py +216 -0
  87. devcopilot-0.2.0.dist-info/METADATA +687 -0
  88. devcopilot-0.2.0.dist-info/RECORD +189 -0
  89. devcopilot-0.2.0.dist-info/WHEEL +4 -0
  90. devcopilot-0.2.0.dist-info/entry_points.txt +6 -0
  91. devcopilot-0.2.0.dist-info/licenses/LICENSE +21 -0
  92. messaging/__init__.py +26 -0
  93. messaging/cli_event_constants.py +67 -0
  94. messaging/command_context.py +66 -0
  95. messaging/command_dispatcher.py +37 -0
  96. messaging/commands.py +275 -0
  97. messaging/event_parser.py +181 -0
  98. messaging/limiter.py +300 -0
  99. messaging/models.py +36 -0
  100. messaging/node_event_pipeline.py +127 -0
  101. messaging/node_runner.py +342 -0
  102. messaging/platforms/__init__.py +15 -0
  103. messaging/platforms/base.py +228 -0
  104. messaging/platforms/discord.py +567 -0
  105. messaging/platforms/factory.py +103 -0
  106. messaging/platforms/outbox.py +144 -0
  107. messaging/platforms/telegram.py +688 -0
  108. messaging/platforms/voice_flow.py +295 -0
  109. messaging/rendering/__init__.py +3 -0
  110. messaging/rendering/discord_markdown.py +318 -0
  111. messaging/rendering/markdown_tables.py +49 -0
  112. messaging/rendering/profiles.py +55 -0
  113. messaging/rendering/telegram_markdown.py +327 -0
  114. messaging/safe_diagnostics.py +17 -0
  115. messaging/session.py +334 -0
  116. messaging/transcript.py +581 -0
  117. messaging/transcription.py +164 -0
  118. messaging/trees/__init__.py +15 -0
  119. messaging/trees/data.py +482 -0
  120. messaging/trees/manager.py +433 -0
  121. messaging/trees/processor.py +179 -0
  122. messaging/trees/repository.py +177 -0
  123. messaging/turn_intake.py +235 -0
  124. messaging/ui_updates.py +101 -0
  125. messaging/voice.py +76 -0
  126. messaging/workflow.py +200 -0
  127. providers/__init__.py +31 -0
  128. providers/base.py +152 -0
  129. providers/cerebras/__init__.py +7 -0
  130. providers/cerebras/client.py +31 -0
  131. providers/cerebras/request.py +55 -0
  132. providers/codestral/__init__.py +7 -0
  133. providers/codestral/client.py +34 -0
  134. providers/deepseek/__init__.py +11 -0
  135. providers/deepseek/client.py +51 -0
  136. providers/deepseek/request.py +475 -0
  137. providers/defaults.py +41 -0
  138. providers/error_mapping.py +309 -0
  139. providers/exceptions.py +113 -0
  140. providers/fireworks/__init__.py +5 -0
  141. providers/fireworks/client.py +45 -0
  142. providers/fireworks/request.py +48 -0
  143. providers/gemini/__init__.py +7 -0
  144. providers/gemini/client.py +49 -0
  145. providers/gemini/request.py +199 -0
  146. providers/groq/__init__.py +7 -0
  147. providers/groq/client.py +31 -0
  148. providers/groq/request.py +83 -0
  149. providers/kimi/__init__.py +10 -0
  150. providers/kimi/client.py +53 -0
  151. providers/kimi/request.py +42 -0
  152. providers/llamacpp/__init__.py +3 -0
  153. providers/llamacpp/client.py +16 -0
  154. providers/lmstudio/__init__.py +5 -0
  155. providers/lmstudio/client.py +16 -0
  156. providers/mistral/__init__.py +7 -0
  157. providers/mistral/client.py +31 -0
  158. providers/mistral/request.py +37 -0
  159. providers/model_listing.py +133 -0
  160. providers/nvidia_nim/__init__.py +7 -0
  161. providers/nvidia_nim/client.py +91 -0
  162. providers/nvidia_nim/request.py +430 -0
  163. providers/nvidia_nim/voice.py +95 -0
  164. providers/ollama/__init__.py +7 -0
  165. providers/ollama/client.py +39 -0
  166. providers/open_router/__init__.py +7 -0
  167. providers/open_router/client.py +124 -0
  168. providers/open_router/request.py +42 -0
  169. providers/opencode/__init__.py +11 -0
  170. providers/opencode/client.py +31 -0
  171. providers/opencode/request.py +35 -0
  172. providers/rate_limit.py +300 -0
  173. providers/registry.py +527 -0
  174. providers/transports/__init__.py +1 -0
  175. providers/transports/anthropic_messages/__init__.py +5 -0
  176. providers/transports/anthropic_messages/http.py +118 -0
  177. providers/transports/anthropic_messages/recovery.py +206 -0
  178. providers/transports/anthropic_messages/stream.py +295 -0
  179. providers/transports/anthropic_messages/transport.py +236 -0
  180. providers/transports/openai_chat/__init__.py +5 -0
  181. providers/transports/openai_chat/recovery.py +217 -0
  182. providers/transports/openai_chat/stream.py +384 -0
  183. providers/transports/openai_chat/tool_calls.py +293 -0
  184. providers/transports/openai_chat/transport.py +156 -0
  185. providers/wafer/__init__.py +10 -0
  186. providers/wafer/client.py +50 -0
  187. providers/zai/__init__.py +10 -0
  188. providers/zai/client.py +46 -0
  189. 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."""
@@ -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)}"