multi-forge 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 (311) hide show
  1. forge/__init__.py +3 -0
  2. forge/_extensions/agents/.gitkeep +0 -0
  3. forge/_extensions/commands/.gitkeep +0 -0
  4. forge/_extensions/skills/analyze/SKILL.md +87 -0
  5. forge/_extensions/skills/challenge/SKILL.md +91 -0
  6. forge/_extensions/skills/consensus/SKILL.md +120 -0
  7. forge/_extensions/skills/consensus/resources/code_consensus_evaluation.md +94 -0
  8. forge/_extensions/skills/consensus/resources/consensus_evaluation.md +70 -0
  9. forge/_extensions/skills/consensus/resources/synthesis.md +101 -0
  10. forge/_extensions/skills/debate/SKILL.md +116 -0
  11. forge/_extensions/skills/debate/resources/code_debate_evaluation.md +101 -0
  12. forge/_extensions/skills/debate/resources/debate_evaluation.md +90 -0
  13. forge/_extensions/skills/panel/SKILL.md +141 -0
  14. forge/_extensions/skills/panel/resources/synthesis.md +103 -0
  15. forge/_extensions/skills/qa/SKILL.md +704 -0
  16. forge/_extensions/skills/qa/resources/checklist/0-enable.md +78 -0
  17. forge/_extensions/skills/qa/resources/checklist/1-preflight.md +24 -0
  18. forge/_extensions/skills/qa/resources/checklist/10-resume.md +143 -0
  19. forge/_extensions/skills/qa/resources/checklist/11-config.md +150 -0
  20. forge/_extensions/skills/qa/resources/checklist/12-search.md +58 -0
  21. forge/_extensions/skills/qa/resources/checklist/13-guard.md +237 -0
  22. forge/_extensions/skills/qa/resources/checklist/14-workflow.md +305 -0
  23. forge/_extensions/skills/qa/resources/checklist/15-skills.md +155 -0
  24. forge/_extensions/skills/qa/resources/checklist/16-handoff.md +224 -0
  25. forge/_extensions/skills/qa/resources/checklist/17-info.md +50 -0
  26. forge/_extensions/skills/qa/resources/checklist/18-disable.md +84 -0
  27. forge/_extensions/skills/qa/resources/checklist/19-uninstall.md +146 -0
  28. forge/_extensions/skills/qa/resources/checklist/2-extensions.md +188 -0
  29. forge/_extensions/skills/qa/resources/checklist/20-cleanup.md +36 -0
  30. forge/_extensions/skills/qa/resources/checklist/3-auth.md +234 -0
  31. forge/_extensions/skills/qa/resources/checklist/4-proxy.md +481 -0
  32. forge/_extensions/skills/qa/resources/checklist/5-session.md +541 -0
  33. forge/_extensions/skills/qa/resources/checklist/6-hooks.md +275 -0
  34. forge/_extensions/skills/qa/resources/checklist/7-costs.md +309 -0
  35. forge/_extensions/skills/qa/resources/checklist/8-status-line.md +174 -0
  36. forge/_extensions/skills/qa/resources/checklist/9-direct-commands.md +146 -0
  37. forge/_extensions/skills/qa/resources/checklist.md +103 -0
  38. forge/_extensions/skills/qa/resources/report-template.md +62 -0
  39. forge/_extensions/skills/qa/scripts/start-container.sh +529 -0
  40. forge/_extensions/skills/qa/scripts/walkthrough-state.py +1137 -0
  41. forge/_extensions/skills/review/SKILL.md +125 -0
  42. forge/_extensions/skills/review/references/claude-4.6.md +474 -0
  43. forge/_extensions/skills/review/references/claude-4.7.md +710 -0
  44. forge/_extensions/skills/review/references/gemini-3.1.md +546 -0
  45. forge/_extensions/skills/review/references/gpt-5.5.md +490 -0
  46. forge/_extensions/skills/review/references/skills-writing-guide.md +1588 -0
  47. forge/_extensions/skills/review/resources/code-anthropic.md +160 -0
  48. forge/_extensions/skills/review/resources/code-gemini.md +184 -0
  49. forge/_extensions/skills/review/resources/code-openai.md +203 -0
  50. forge/_extensions/skills/review/resources/code.md +160 -0
  51. forge/_extensions/skills/review-docs/SKILL.md +121 -0
  52. forge/_extensions/skills/review-docs/resources/docs-anthropic.md +170 -0
  53. forge/_extensions/skills/review-docs/resources/docs-gemini.md +204 -0
  54. forge/_extensions/skills/review-docs/resources/docs-openai.md +231 -0
  55. forge/_extensions/skills/review-docs/resources/docs.md +170 -0
  56. forge/_extensions/skills/smoke-test/SKILL.md +27 -0
  57. forge/_extensions/skills/smoke-test/scripts/smoke-test.sh +118 -0
  58. forge/_extensions/skills/understand/SKILL.md +148 -0
  59. forge/_extensions/skills/understand/resources/code-anthropic.md +163 -0
  60. forge/_extensions/skills/understand/resources/code-gemini.md +194 -0
  61. forge/_extensions/skills/understand/resources/code-openai.md +181 -0
  62. forge/_extensions/skills/understand/resources/code.md +163 -0
  63. forge/_extensions/skills/understand/resources/docs-anthropic.md +177 -0
  64. forge/_extensions/skills/understand/resources/docs-gemini.md +202 -0
  65. forge/_extensions/skills/understand/resources/docs-openai.md +191 -0
  66. forge/_extensions/skills/understand/resources/docs.md +177 -0
  67. forge/_extensions/skills/walkthrough/SKILL.md +599 -0
  68. forge/_extensions/skills/walkthrough/resources/checklist.md +765 -0
  69. forge/_extensions/skills/walkthrough/scripts/run-in-repo.sh +118 -0
  70. forge/_extensions/skills/walkthrough/scripts/setup-test-repo.sh +198 -0
  71. forge/_extensions/skills/walkthrough/scripts/walkthrough-state.py +1137 -0
  72. forge/backend/__init__.py +174 -0
  73. forge/backend/adapters/__init__.py +38 -0
  74. forge/backend/adapters/litellm.py +158 -0
  75. forge/backend/creation.py +89 -0
  76. forge/backend/registry.py +178 -0
  77. forge/cli/__init__.py +16 -0
  78. forge/cli/auth.py +483 -0
  79. forge/cli/backend.py +298 -0
  80. forge/cli/claude.py +411 -0
  81. forge/cli/config_cmd.py +303 -0
  82. forge/cli/extensions.py +1001 -0
  83. forge/cli/gc.py +165 -0
  84. forge/cli/guard.py +1018 -0
  85. forge/cli/guards.py +106 -0
  86. forge/cli/handoff.py +110 -0
  87. forge/cli/hooks/__init__.py +36 -0
  88. forge/cli/hooks/_group.py +20 -0
  89. forge/cli/hooks/_helpers.py +149 -0
  90. forge/cli/hooks/commands.py +1677 -0
  91. forge/cli/hooks/direct_commands.py +1304 -0
  92. forge/cli/hooks/install.py +232 -0
  93. forge/cli/hooks/policy.py +151 -0
  94. forge/cli/hooks/read_hygiene.py +74 -0
  95. forge/cli/hooks/verification.py +370 -0
  96. forge/cli/logs.py +406 -0
  97. forge/cli/main.py +292 -0
  98. forge/cli/proxy.py +1821 -0
  99. forge/cli/proxy_costs.py +313 -0
  100. forge/cli/search.py +416 -0
  101. forge/cli/session.py +892 -0
  102. forge/cli/session_addendum.py +81 -0
  103. forge/cli/session_fork.py +750 -0
  104. forge/cli/session_handoff.py +141 -0
  105. forge/cli/session_lifecycle.py +2053 -0
  106. forge/cli/session_manage.py +1336 -0
  107. forge/cli/session_memory.py +201 -0
  108. forge/cli/status_line.py +1398 -0
  109. forge/cli/workflow.py +1964 -0
  110. forge/config/__init__.py +110 -0
  111. forge/config/dataclass_utils.py +88 -0
  112. forge/config/defaults/__init__.py +0 -0
  113. forge/config/defaults/backends/__init__.py +0 -0
  114. forge/config/defaults/backends/litellm.yaml +196 -0
  115. forge/config/defaults/templates/__init__.py +0 -0
  116. forge/config/defaults/templates/litellm-anthropic-local.yaml +33 -0
  117. forge/config/defaults/templates/litellm-anthropic.yaml +24 -0
  118. forge/config/defaults/templates/litellm-gemini-flash-local.yaml +37 -0
  119. forge/config/defaults/templates/litellm-gemini-local.yaml +32 -0
  120. forge/config/defaults/templates/litellm-gemini-test.yaml +34 -0
  121. forge/config/defaults/templates/litellm-gemini.yaml +21 -0
  122. forge/config/defaults/templates/litellm-openai-codex-local.yaml +36 -0
  123. forge/config/defaults/templates/litellm-openai-local.yaml +38 -0
  124. forge/config/defaults/templates/litellm-openai.yaml +28 -0
  125. forge/config/defaults/templates/openrouter-anthropic.yaml +23 -0
  126. forge/config/defaults/templates/openrouter-deepseek.yaml +26 -0
  127. forge/config/defaults/templates/openrouter-gemini-flash.yaml +26 -0
  128. forge/config/defaults/templates/openrouter-gemini.yaml +23 -0
  129. forge/config/defaults/templates/openrouter-glm.yaml +23 -0
  130. forge/config/defaults/templates/openrouter-kimi.yaml +30 -0
  131. forge/config/defaults/templates/openrouter-minimax.yaml +26 -0
  132. forge/config/defaults/templates/openrouter-openai-codex.yaml +23 -0
  133. forge/config/defaults/templates/openrouter-openai.yaml +28 -0
  134. forge/config/defaults/templates/openrouter-qwen.yaml +25 -0
  135. forge/config/loader.py +675 -0
  136. forge/config/schema.py +448 -0
  137. forge/core/__init__.py +5 -0
  138. forge/core/auth/__init__.py +67 -0
  139. forge/core/auth/capabilities.py +219 -0
  140. forge/core/auth/credentials_file.py +244 -0
  141. forge/core/auth/protocols.py +18 -0
  142. forge/core/auth/secrets.py +243 -0
  143. forge/core/auth/template_secrets.py +112 -0
  144. forge/core/data/__init__.py +5 -0
  145. forge/core/data/model_catalog.yaml +1522 -0
  146. forge/core/data/pricing.yaml +140 -0
  147. forge/core/data/system_prompt_addendums/__init__.py +0 -0
  148. forge/core/data/system_prompt_addendums/gemini.md +330 -0
  149. forge/core/data/system_prompt_addendums/openai.md +328 -0
  150. forge/core/llm/__init__.py +231 -0
  151. forge/core/llm/clients/__init__.py +14 -0
  152. forge/core/llm/clients/base.py +115 -0
  153. forge/core/llm/clients/litellm.py +619 -0
  154. forge/core/llm/clients/openai_compat.py +244 -0
  155. forge/core/llm/clients/openrouter.py +234 -0
  156. forge/core/llm/credentials.py +439 -0
  157. forge/core/llm/detection.py +86 -0
  158. forge/core/llm/errors.py +44 -0
  159. forge/core/llm/protocols.py +80 -0
  160. forge/core/llm/types.py +176 -0
  161. forge/core/logging.py +146 -0
  162. forge/core/models/__init__.py +91 -0
  163. forge/core/models/catalog.py +467 -0
  164. forge/core/models/pricing.py +165 -0
  165. forge/core/models/types.py +167 -0
  166. forge/core/naming.py +212 -0
  167. forge/core/ops/__init__.py +73 -0
  168. forge/core/ops/context.py +141 -0
  169. forge/core/ops/gc.py +802 -0
  170. forge/core/ops/proxy.py +146 -0
  171. forge/core/ops/resolution.py +135 -0
  172. forge/core/ops/session.py +344 -0
  173. forge/core/ops/session_context.py +548 -0
  174. forge/core/paths.py +38 -0
  175. forge/core/process.py +54 -0
  176. forge/core/reactive/__init__.py +38 -0
  177. forge/core/reactive/cost_tracking.py +300 -0
  178. forge/core/reactive/env.py +180 -0
  179. forge/core/reactive/proxy.py +78 -0
  180. forge/core/reactive/routing.py +622 -0
  181. forge/core/reactive/session_runner.py +185 -0
  182. forge/core/reactive/structured_output.py +62 -0
  183. forge/core/reactive/tagger.py +94 -0
  184. forge/core/reactive/throttle.py +132 -0
  185. forge/core/state/__init__.py +59 -0
  186. forge/core/state/exceptions.py +59 -0
  187. forge/core/state/io.py +140 -0
  188. forge/core/state/lock.py +99 -0
  189. forge/core/state/timestamps.py +60 -0
  190. forge/core/transcript.py +78 -0
  191. forge/core/typing_helpers.py +24 -0
  192. forge/core/workqueue/__init__.py +67 -0
  193. forge/core/workqueue/queue.py +552 -0
  194. forge/core/workqueue/types.py +63 -0
  195. forge/guard/__init__.py +26 -0
  196. forge/guard/deterministic/__init__.py +26 -0
  197. forge/guard/deterministic/base.py +158 -0
  198. forge/guard/deterministic/coding_standards.py +256 -0
  199. forge/guard/deterministic/registry.py +148 -0
  200. forge/guard/deterministic/tdd.py +171 -0
  201. forge/guard/engine.py +216 -0
  202. forge/guard/protocols.py +91 -0
  203. forge/guard/queries.py +96 -0
  204. forge/guard/semantic/__init__.py +34 -0
  205. forge/guard/semantic/promotion.py +18 -0
  206. forge/guard/semantic/supervisor.py +813 -0
  207. forge/guard/semantic/verdict.py +183 -0
  208. forge/guard/store.py +124 -0
  209. forge/guard/team/__init__.py +6 -0
  210. forge/guard/team/config.py +24 -0
  211. forge/guard/team/handlers.py +209 -0
  212. forge/guard/team/prompts.py +41 -0
  213. forge/guard/types.py +125 -0
  214. forge/guard/workflow/__init__.py +17 -0
  215. forge/guard/workflow/branches.py +67 -0
  216. forge/guard/workflow/config.py +63 -0
  217. forge/guard/workflow/divergence.py +113 -0
  218. forge/guard/workflow/policy.py +87 -0
  219. forge/guard/workflow/stages.py +205 -0
  220. forge/install/__init__.py +55 -0
  221. forge/install/cli.py +281 -0
  222. forge/install/exceptions.py +163 -0
  223. forge/install/hooks.py +109 -0
  224. forge/install/installer.py +1037 -0
  225. forge/install/models.py +321 -0
  226. forge/install/preset.py +272 -0
  227. forge/install/settings_merge.py +831 -0
  228. forge/install/tracking.py +238 -0
  229. forge/install/version.py +141 -0
  230. forge/proxy/__init__.py +0 -0
  231. forge/proxy/base_client.py +181 -0
  232. forge/proxy/client_adapter.py +476 -0
  233. forge/proxy/client_factory.py +531 -0
  234. forge/proxy/converters.py +1206 -0
  235. forge/proxy/cost_logger.py +132 -0
  236. forge/proxy/cost_tracker.py +242 -0
  237. forge/proxy/data_models.py +338 -0
  238. forge/proxy/error_hints.py +92 -0
  239. forge/proxy/metrics.py +222 -0
  240. forge/proxy/model_spec.py +158 -0
  241. forge/proxy/proxies.py +333 -0
  242. forge/proxy/proxy_identity.py +134 -0
  243. forge/proxy/proxy_orchestrator.py +1018 -0
  244. forge/proxy/proxy_startup.py +54 -0
  245. forge/proxy/server.py +1561 -0
  246. forge/proxy/utils.py +537 -0
  247. forge/review/__init__.py +6 -0
  248. forge/review/adversarial.py +111 -0
  249. forge/review/consensus.py +236 -0
  250. forge/review/engine.py +356 -0
  251. forge/review/models.py +437 -0
  252. forge/review/resources/__init__.py +5 -0
  253. forge/review/resources/codereview-performance.md +85 -0
  254. forge/review/resources/codereview-quick.md +75 -0
  255. forge/review/resources/codereview-security.md +92 -0
  256. forge/review/resources/codereview.md +85 -0
  257. forge/review/resources/docreview-quick.md +75 -0
  258. forge/review/resources/docreview.md +86 -0
  259. forge/review/resources/thinkdeep.md +89 -0
  260. forge/review/routing.py +368 -0
  261. forge/review/synthesis.py +73 -0
  262. forge/runtime_config.py +438 -0
  263. forge/search/__init__.py +55 -0
  264. forge/search/bm25_store.py +264 -0
  265. forge/search/content_store.py +197 -0
  266. forge/search/engine.py +352 -0
  267. forge/search/exceptions.py +51 -0
  268. forge/search/extractor.py +234 -0
  269. forge/search/index_state.py +295 -0
  270. forge/search/store.py +215 -0
  271. forge/search/tokenizer.py +24 -0
  272. forge/session/__init__.py +130 -0
  273. forge/session/active.py +339 -0
  274. forge/session/artifacts.py +202 -0
  275. forge/session/claude/__init__.py +50 -0
  276. forge/session/claude/cleanup.py +105 -0
  277. forge/session/claude/invoke.py +236 -0
  278. forge/session/claude/paths.py +200 -0
  279. forge/session/cleanup.py +216 -0
  280. forge/session/config.py +34 -0
  281. forge/session/direct_model.py +107 -0
  282. forge/session/effective.py +169 -0
  283. forge/session/exceptions.py +255 -0
  284. forge/session/handoff.py +881 -0
  285. forge/session/handoff_agent.py +544 -0
  286. forge/session/hooks/__init__.py +35 -0
  287. forge/session/hooks/models.py +73 -0
  288. forge/session/hooks/session_start.py +507 -0
  289. forge/session/identity.py +84 -0
  290. forge/session/index.py +553 -0
  291. forge/session/manager.py +1506 -0
  292. forge/session/models.py +572 -0
  293. forge/session/overrides.py +344 -0
  294. forge/session/plan_resolution.py +286 -0
  295. forge/session/prev_sessions.py +128 -0
  296. forge/session/store.py +431 -0
  297. forge/session/validation.py +47 -0
  298. forge/session/worktree/__init__.py +65 -0
  299. forge/session/worktree/cleanup.py +262 -0
  300. forge/session/worktree/config_copy.py +203 -0
  301. forge/session/worktree/create.py +332 -0
  302. forge/sidecar/__init__.py +29 -0
  303. forge/sidecar/container.py +161 -0
  304. forge/sidecar/docker.py +86 -0
  305. forge/sidecar/secrets.py +19 -0
  306. multi_forge-0.2.0.dist-info/METADATA +242 -0
  307. multi_forge-0.2.0.dist-info/RECORD +311 -0
  308. multi_forge-0.2.0.dist-info/WHEEL +4 -0
  309. multi_forge-0.2.0.dist-info/entry_points.txt +2 -0
  310. multi_forge-0.2.0.dist-info/licenses/LICENSE +203 -0
  311. multi_forge-0.2.0.dist-info/licenses/NOTICE +14 -0
forge/config/schema.py ADDED
@@ -0,0 +1,448 @@
1
+ """Configuration schema definitions using dataclasses.
2
+
3
+ This module defines the structure of all Forge configuration using dataclasses.
4
+ Each dataclass represents a configuration section with typed fields and defaults.
5
+
6
+ The schema is hierarchical:
7
+ ForgeConfig
8
+ ├── proxy: ProxyConfig
9
+ │ ├── gemini: ProviderConfig
10
+ │ ├── openai: ProviderConfig
11
+ │ └── litellm: ProviderConfig
12
+ ├── session: SessionConfig
13
+ └── (future: mcp, guard, status, etc.)
14
+
15
+ Usage:
16
+ from forge.config import config
17
+
18
+ model = config.proxy.litellm.tiers.opus
19
+ overrides = config.proxy.litellm.tier_overrides.get("opus")
20
+ """
21
+
22
+ from dataclasses import dataclass, field
23
+ from typing import Any
24
+
25
+ # --- CONSTANTS ---
26
+
27
+ OPENAI_MODELS = [
28
+ "gpt-4o",
29
+ "gpt-4o-mini",
30
+ "gpt-4.1",
31
+ "gpt-4.1-mini",
32
+ "gpt-5",
33
+ "gpt-5-codex",
34
+ "gpt-5-mini",
35
+ "gpt-5-nano",
36
+ "gpt-5-pro",
37
+ "gpt-5.1",
38
+ "gpt-5.1-codex",
39
+ "gpt-5.1-codex-max",
40
+ "gpt-5.1-codex-mini",
41
+ "gpt-5.1-mini",
42
+ "gpt-5.2",
43
+ "gpt-5.2-codex",
44
+ "gpt-5.2-pro",
45
+ "gpt-5.3-codex",
46
+ "gpt-5.5",
47
+ "gpt-5.4",
48
+ "gpt-5.4-mini",
49
+ "gpt-5.4-nano",
50
+ "gpt-5.4-pro",
51
+ "o1",
52
+ "o1-mini",
53
+ "o3",
54
+ "o3-mini",
55
+ "o3-pro",
56
+ "o4-mini",
57
+ "o4-mini-high",
58
+ ]
59
+
60
+
61
+ # --- HELPER FUNCTIONS ---
62
+
63
+
64
+ def is_openai_model(model_name: str) -> bool:
65
+ """Check if a model name refers to an OpenAI model.
66
+
67
+ Uses strict allowlist-only matching against OPENAI_MODELS.
68
+ No prefix heuristics - unknown gpt-* models will return False.
69
+
70
+ Strips known provider prefixes (openai/, anthropic/) before matching.
71
+ """
72
+ clean_name = model_name.lower()
73
+
74
+ if clean_name.startswith("anthropic/"):
75
+ clean_name = clean_name[10:]
76
+ elif clean_name.startswith("openai/"):
77
+ clean_name = clean_name[7:]
78
+
79
+ return clean_name in {m.lower() for m in OPENAI_MODELS}
80
+
81
+
82
+ # --- DATACLASSES ---
83
+
84
+
85
+ @dataclass
86
+ class TierModels:
87
+ """Model mappings for each tier (haiku/sonnet/opus)."""
88
+
89
+ haiku: str = ""
90
+ sonnet: str = ""
91
+ opus: str = ""
92
+
93
+ def get(self, tier: str) -> str:
94
+ """Get model for tier name."""
95
+ return getattr(self, tier.lower(), self.sonnet)
96
+
97
+
98
+ @dataclass
99
+ class TierOverride:
100
+ """Per-tier hyperparameter overrides.
101
+
102
+ Use this to differentiate tiers that map to the same model.
103
+ For example, if both sonnet and opus map to gpt-5.2, use tier_overrides
104
+ to give opus higher reasoning_effort than sonnet.
105
+
106
+ Values here override model catalog defaults. None means "use catalog default".
107
+ """
108
+
109
+ reasoning_effort: str | None = None # none, low, medium, high, xhigh (model-dependent)
110
+ verbosity: str | None = None # low, medium, high
111
+ temperature: float | None = None # Override temperature for this tier
112
+ thinking_budget_tokens: int | None = None # For models with thinking budgets
113
+
114
+
115
+ @dataclass
116
+ class TierOverrides:
117
+ """Per-tier overrides for hyperparameters.
118
+
119
+ This structure allows families and proxies to customize behavior per tier,
120
+ which is essential when multiple tiers map to the same underlying model.
121
+
122
+ Flow:
123
+ 1. Family config defines tier_overrides as template defaults
124
+ 2. Proxy acquisition copies these to proxy overlay
125
+ 3. CLI args can override at acquisition time
126
+ 4. Proxy overlay can be modified at runtime
127
+ """
128
+
129
+ haiku: TierOverride | None = None
130
+ sonnet: TierOverride | None = None
131
+ opus: TierOverride | None = None
132
+
133
+ def get(self, tier: str) -> TierOverride | None:
134
+ """Get override for tier name, or None if not set."""
135
+ return getattr(self, tier.lower(), None)
136
+
137
+
138
+ @dataclass
139
+ class ProviderConfig:
140
+ """Configuration for a single LLM provider (Gemini, OpenAI, LiteLLM)."""
141
+
142
+ tiers: TierModels = field(default_factory=TierModels)
143
+ tier_overrides: TierOverrides = field(default_factory=TierOverrides)
144
+ model_alternatives: dict[str, dict[str, str]] = field(default_factory=dict)
145
+ auth_url: str = ""
146
+ base_url: str = ""
147
+ cache_ttl: float = 3600.0
148
+ top_p: float | None = None
149
+ enable_preamble: bool = False
150
+
151
+ # LiteLLM-specific: API mode for OpenAI models
152
+ openai_api_mode: str = "auto" # auto, responses, chat_completions
153
+
154
+ # Prompt caching mode (only affects Anthropic/Bedrock models via LiteLLM)
155
+ # "passthrough": forward client cache_control unchanged (default)
156
+ # "auto_inject": auto-add cache_control for long prompts
157
+ prompt_caching: str = "passthrough"
158
+ auto_cache_min_tokens: int = 1024
159
+
160
+ # Error hint enrichment: append corrective hints to tool_result errors
161
+ # before forwarding to the LLM, helping non-Claude models recover faster.
162
+ error_hints: bool = False
163
+
164
+
165
+ def _coerce_optional_usd_cap(name: str, value: Any) -> float | None:
166
+ """Coerce an optional USD cap to a positive float."""
167
+ if value is None:
168
+ return None
169
+ if isinstance(value, bool):
170
+ raise ValueError(f"Invalid {name}: must be a positive number of USD")
171
+ try:
172
+ amount = float(value)
173
+ except (TypeError, ValueError):
174
+ raise ValueError(f"Invalid {name}: must be a positive number of USD") from None
175
+ if amount <= 0:
176
+ raise ValueError(f"Invalid {name}: must be greater than 0")
177
+ return amount
178
+
179
+
180
+ @dataclass
181
+ class CostCaps:
182
+ """Spend cap configuration for a proxy."""
183
+
184
+ per_day: float | None = None # USD, rolling 24h window
185
+ per_month: float | None = None # USD, calendar month
186
+
187
+ def __post_init__(self) -> None:
188
+ self.per_day = _coerce_optional_usd_cap("costs.caps.per_day", self.per_day)
189
+ self.per_month = _coerce_optional_usd_cap("costs.caps.per_month", self.per_month)
190
+
191
+
192
+ def _coerce_cost_caps(value: Any) -> CostCaps:
193
+ """Normalize raw cost cap mappings into ``CostCaps``."""
194
+ if value is None:
195
+ return CostCaps()
196
+ if isinstance(value, CostCaps):
197
+ return value
198
+ if not isinstance(value, dict):
199
+ raise ValueError("Invalid costs.caps: must be a mapping")
200
+ return CostCaps(
201
+ per_day=value.get("per_day"),
202
+ per_month=value.get("per_month"),
203
+ )
204
+
205
+
206
+ @dataclass
207
+ class CostConfig:
208
+ """Cost tracking and cap configuration for a proxy."""
209
+
210
+ caps: CostCaps = field(default_factory=CostCaps)
211
+ cap_mode: str = "post" # "post" (block after exceeded) or "strict" (pre-flight estimate)
212
+ on_cap_hit: str = "reject" # "reject" (HTTP 429) or "warn" (header only)
213
+
214
+ def __post_init__(self) -> None:
215
+ self.caps = _coerce_cost_caps(self.caps)
216
+
217
+ valid_modes = {"post", "strict"}
218
+ if self.cap_mode not in valid_modes:
219
+ raise ValueError(f"Invalid cap_mode: '{self.cap_mode}' (must be one of: {', '.join(sorted(valid_modes))})")
220
+ valid_actions = {"reject", "warn"}
221
+ if self.on_cap_hit not in valid_actions:
222
+ raise ValueError(
223
+ f"Invalid on_cap_hit: '{self.on_cap_hit}' (must be one of: {', '.join(sorted(valid_actions))})"
224
+ )
225
+
226
+
227
+ def _coerce_cost_config(value: Any) -> CostConfig:
228
+ """Normalize raw proxy.yaml cost config into ``CostConfig``."""
229
+ if value is None:
230
+ return CostConfig()
231
+ if isinstance(value, CostConfig):
232
+ return value
233
+ if not isinstance(value, dict):
234
+ raise ValueError("Invalid costs: must be a mapping")
235
+ return CostConfig(
236
+ caps=_coerce_cost_caps(value.get("caps", {}) or {}),
237
+ cap_mode=value.get("cap_mode", "post"),
238
+ on_cap_hit=value.get("on_cap_hit", "reject"),
239
+ )
240
+
241
+
242
+ @dataclass
243
+ class BackendDependency:
244
+ """Backend dependency declaration (proxy runtime requirement).
245
+
246
+ Declares that a proxy template requires a backend service to be running.
247
+ Example: local LiteLLM proxies require LiteLLM backend on port 4000.
248
+ """
249
+
250
+ adapter: str # e.g., "litellm"
251
+ port: int
252
+ required_env_vars: list[str] = field(default_factory=list)
253
+
254
+
255
+ @dataclass
256
+ class ProxyConfig:
257
+ """Proxy server configuration."""
258
+
259
+ gemini: ProviderConfig = field(default_factory=ProviderConfig)
260
+ openai: ProviderConfig = field(default_factory=ProviderConfig)
261
+ litellm: ProviderConfig = field(default_factory=ProviderConfig)
262
+ openrouter: ProviderConfig = field(default_factory=ProviderConfig)
263
+
264
+ family: str = "" # model family (e.g., "openai", "anthropic", "gemini")
265
+ preferred_provider: str = "" # set by --template flag
266
+ active_template: str = ""
267
+ default_tier: str = "sonnet"
268
+ backend_dependency: BackendDependency | None = None
269
+ default_port: int = 8082
270
+ host: str = "127.0.0.1"
271
+ tool_prefixes_to_ignore: list[str] = field(default_factory=list)
272
+ costs: CostConfig = field(default_factory=CostConfig)
273
+
274
+ def get_provider(self, name: str | None = None) -> ProviderConfig:
275
+ """Get provider config by name, defaulting to preferred_provider."""
276
+ provider = name or self.preferred_provider or "litellm"
277
+ return getattr(self, provider, self.litellm)
278
+
279
+ def get_model_for_tier(self, tier: str) -> str:
280
+ """Get the configured model for a tier based on preferred_provider."""
281
+ provider = self.get_provider()
282
+ return provider.tiers.get(tier)
283
+
284
+
285
+ @dataclass
286
+ class SessionConfig:
287
+ """Session management configuration."""
288
+
289
+ default_tier: str = "sonnet"
290
+ manifest_filename: str = "forge.session.json"
291
+ forge_home: str = "" # default: ~/.forge
292
+
293
+
294
+ @dataclass
295
+ class ProxyInstanceConfig:
296
+ """Complete proxy instance configuration owned by the user.
297
+
298
+ Unlike the previous overlay model where proxies only stored tier_overrides
299
+ and merged with templates at runtime, this dataclass contains the full
300
+ configuration. The user owns the entire file and can edit it directly.
301
+
302
+ Flow:
303
+ 1. User runs `forge proxy create litellm-gemini`
304
+ 2. Template is copied to ~/.forge/proxies/{id}/proxy.yaml
305
+ 3. User can edit the file with `forge proxy edit {id}`
306
+ 4. Proxy reads this file directly at startup (no merge logic)
307
+
308
+ The template and template_digest fields are informational only —
309
+ they enable future `forge proxy rebase` functionality.
310
+ """
311
+
312
+ proxy_format: int
313
+
314
+ template: str # e.g., "litellm-gemini"
315
+ template_digest: str # SHA256 at creation time
316
+
317
+ provider: str # litellm | openai | gemini
318
+ proxy_endpoint: str # e.g., http://localhost:8085
319
+ port: int
320
+ upstream_base_url: str # e.g., https://litellm.corp.com
321
+
322
+ tiers: TierModels
323
+ family: str = "" # model family (e.g., "openai", "anthropic", "gemini")
324
+ tier_overrides: TierOverrides = field(default_factory=TierOverrides)
325
+ model_alternatives: dict[str, dict[str, str]] = field(default_factory=dict)
326
+ default_tier: str = "sonnet"
327
+
328
+ provider_settings: dict[str, Any] = field(default_factory=dict)
329
+
330
+ # Copied from template into proxy.yaml; controls Anthropic/Bedrock prompt caching via LiteLLM.
331
+ prompt_caching: str = "passthrough"
332
+ auto_cache_min_tokens: int = 1024
333
+
334
+ costs: CostConfig = field(default_factory=CostConfig)
335
+
336
+ created_at: str | None = None
337
+ updated_at: str | None = None
338
+
339
+ def __post_init__(self) -> None:
340
+ """Validate proxy instance configuration fields."""
341
+ if self.proxy_format != 1:
342
+ raise ValueError(f"Unsupported proxy_format: {self.proxy_format} (expected 1)")
343
+
344
+ valid_providers = {"litellm", "openai", "gemini", "openrouter"}
345
+ if self.provider not in valid_providers:
346
+ raise ValueError(
347
+ f"Invalid provider: '{self.provider}' (must be one of: {', '.join(sorted(valid_providers))})"
348
+ )
349
+
350
+ if not self.proxy_endpoint:
351
+ raise ValueError("proxy_endpoint is required (e.g., 'http://localhost:8085')")
352
+ if not self.upstream_base_url:
353
+ raise ValueError("upstream_base_url is required (e.g., 'https://litellm.corp.com')")
354
+
355
+ if not 1 <= self.port <= 65535:
356
+ raise ValueError(f"Invalid port: {self.port} (must be 1-65535)")
357
+
358
+ if not self.tiers.sonnet:
359
+ raise ValueError("Tiers must define at least 'sonnet' model")
360
+
361
+ valid_tiers = {"haiku", "sonnet", "opus"}
362
+ if self.default_tier not in valid_tiers:
363
+ raise ValueError(
364
+ f"Invalid default_tier: '{self.default_tier}' (must be one of: {', '.join(sorted(valid_tiers))})"
365
+ )
366
+
367
+ self.costs = _coerce_cost_config(self.costs)
368
+ _validate_static_tier_override_constraints(self.tiers, self.tier_overrides)
369
+
370
+
371
+ def _validate_static_tier_override_constraints(tiers: TierModels, overrides: TierOverrides) -> None:
372
+ """Reject Forge-owned config overrides that known models do not support."""
373
+ try:
374
+ from forge.core.models.catalog import (
375
+ ModelCatalogError,
376
+ get_model_spec,
377
+ resolve_model_id,
378
+ )
379
+ except Exception:
380
+ # Catalog import can fail during early bootstrap; provider APIs still
381
+ # reject unsupported overrides at request time as a safety net.
382
+ return
383
+
384
+ for tier in ("haiku", "sonnet", "opus"):
385
+ override = overrides.get(tier)
386
+ if override is None:
387
+ continue
388
+
389
+ model_name = tiers.get(tier)
390
+ if not model_name:
391
+ continue
392
+
393
+ lookup_name = model_name.removesuffix("[1m]")
394
+ try:
395
+ canonical_model = resolve_model_id(lookup_name)
396
+ spec = get_model_spec(canonical_model)
397
+ except ModelCatalogError:
398
+ continue
399
+
400
+ if spec.supports_sampling_overrides is False and override.temperature is not None:
401
+ raise ValueError(
402
+ f"tier_overrides.{tier}.temperature is not supported by {canonical_model}; "
403
+ "remove the override or choose a model that supports sampling overrides"
404
+ )
405
+
406
+ if spec.thinking_modes == ("adaptive",) and override.thinking_budget_tokens is not None:
407
+ raise ValueError(
408
+ f"tier_overrides.{tier}.thinking_budget_tokens is not supported by {canonical_model}; "
409
+ "this model only supports adaptive thinking"
410
+ )
411
+
412
+ if (
413
+ override.reasoning_effort is not None
414
+ and spec.litellm_reasoning_efforts is not None
415
+ and override.reasoning_effort not in spec.litellm_reasoning_efforts
416
+ ):
417
+ supported = ", ".join(spec.litellm_reasoning_efforts)
418
+ raise ValueError(
419
+ f"tier_overrides.{tier}.reasoning_effort={override.reasoning_effort!r} is not supported by "
420
+ f"{canonical_model}; supported values: {supported}"
421
+ )
422
+
423
+
424
+ @dataclass
425
+ class ForgeConfig:
426
+ """Root configuration for all Forge components.
427
+
428
+ This is the top-level config that aggregates all component configs.
429
+ Access via the singleton: `from forge.config import config`
430
+ """
431
+
432
+ proxy: ProxyConfig = field(default_factory=ProxyConfig)
433
+ session: SessionConfig = field(default_factory=SessionConfig)
434
+
435
+ # Future: mcp, guard, status
436
+
437
+ def to_dict(self) -> dict[str, Any]:
438
+ """Convert config to nested dict (for serialization)."""
439
+ from dataclasses import asdict
440
+
441
+ return asdict(self)
442
+
443
+ @classmethod
444
+ def from_dict(cls, data: dict[str, Any]) -> "ForgeConfig":
445
+ """Create config from nested dict."""
446
+ from forge.config.dataclass_utils import dict_to_dataclass
447
+
448
+ return dict_to_dataclass(cls, data)
forge/core/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Shared core libraries for Forge components."""
2
+
3
+ from .paths import FORGE_DIR, get_forge_home
4
+
5
+ __all__ = ["FORGE_DIR", "get_forge_home"]
@@ -0,0 +1,67 @@
1
+ """Consolidated authentication module for Multi-Forge.
2
+
3
+ This package provides:
4
+ 1. SecretsProvider - unified interface for accessing secrets from env/config
5
+ 2. Error types - re-exported from core.llm.errors for convenience
6
+
7
+ Usage:
8
+ from forge.core.auth import (
9
+ EnvSecretsProvider,
10
+ ChainSecretsProvider,
11
+ NoApiKeyError,
12
+ )
13
+
14
+ # Simple env-only secrets
15
+ secrets = EnvSecretsProvider()
16
+ api_key = secrets.require("ANTHROPIC_API_KEY")
17
+ """
18
+
19
+ from forge.core.auth.capabilities import (
20
+ CREDENTIALS,
21
+ RETIRED_NAMES,
22
+ Credential,
23
+ EnvVar,
24
+ credential_for_env_var,
25
+ credentials_for_template,
26
+ format_missing_credential_error,
27
+ )
28
+ from forge.core.auth.credentials_file import CredentialVersionError
29
+ from forge.core.auth.protocols import SecretsProvider
30
+ from forge.core.auth.secrets import (
31
+ ChainSecretsProvider,
32
+ ConfigSecretsProvider,
33
+ EnvSecretsProvider,
34
+ FileSecretsProvider,
35
+ )
36
+ from forge.core.auth.template_secrets import (
37
+ TEMPLATE_SECRETS,
38
+ resolve_env_or_credential,
39
+ )
40
+
41
+ # Re-export errors from core.llm.errors (no new types)
42
+ from forge.core.llm.errors import AuthenticationError, NoApiKeyError
43
+
44
+ __all__ = [
45
+ # Credential registry (capabilities.py)
46
+ "CREDENTIALS",
47
+ "RETIRED_NAMES",
48
+ "Credential",
49
+ "EnvVar",
50
+ "credential_for_env_var",
51
+ "credentials_for_template",
52
+ "format_missing_credential_error",
53
+ # SecretsProvider protocol and implementations
54
+ "SecretsProvider",
55
+ "EnvSecretsProvider",
56
+ "ConfigSecretsProvider",
57
+ "FileSecretsProvider",
58
+ "ChainSecretsProvider",
59
+ # Template credential resolution
60
+ "TEMPLATE_SECRETS",
61
+ "resolve_env_or_credential",
62
+ # Credential file errors
63
+ "CredentialVersionError",
64
+ # Re-exported errors (canonical source: core.llm.errors)
65
+ "AuthenticationError",
66
+ "NoApiKeyError",
67
+ ]