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
@@ -0,0 +1,176 @@
1
+ """Canonical types for LLM client abstraction.
2
+
3
+ These types provide a provider-agnostic interface for LLM interactions.
4
+ All client implementations convert to/from these canonical types.
5
+ """
6
+
7
+ from typing import Any, Literal, Self
8
+
9
+ from pydantic import BaseModel, Field, model_validator
10
+
11
+ ReasoningEffort = Literal["none", "low", "medium", "high", "xhigh"]
12
+ Verbosity = Literal["low", "medium", "high", "xhigh", "max"]
13
+ MessageRole = Literal["system", "user", "assistant", "tool"]
14
+ StreamEventType = Literal["text_delta", "tool_call_delta", "response_end", "usage", "error"]
15
+ # Client-side prompt caching policy (NOT provider mechanism)
16
+ # Provider mechanisms (auto, explicit, context_cache_api) are in model_catalog.yaml
17
+ PromptCachingPolicy = Literal["passthrough", "auto_inject"]
18
+
19
+
20
+ class ThinkingConfig(BaseModel):
21
+ """Direct thinking mode control (Gemini/Claude extended thinking)."""
22
+
23
+ type: Literal["enabled", "disabled", "adaptive"] = "enabled"
24
+ budget_tokens: int = 8192
25
+
26
+ @model_validator(mode="after")
27
+ def validate_budget_tokens(self) -> Self:
28
+ """Validate budget_tokens is positive when thinking is enabled/adaptive."""
29
+ if self.type in ("enabled", "adaptive") and self.budget_tokens <= 0:
30
+ raise ValueError("budget_tokens must be positive when thinking is enabled")
31
+ return self
32
+
33
+
34
+ class InjectionPoint(BaseModel):
35
+ """Typed injection point for auto_inject cache control.
36
+
37
+ Specifies where to add cache_control directives in messages.
38
+ """
39
+
40
+ location: Literal["message"] = "message"
41
+ role: Literal["system", "user", "assistant"] | None = None
42
+ index: int | None = None # Target by index (-1 = last message)
43
+
44
+
45
+ class PromptCachingConfig(BaseModel):
46
+ """Client-side prompt caching configuration for LLM calls.
47
+
48
+ Policies (client behavior):
49
+ - passthrough: Honor caller's cache_control if provided (default)
50
+ - auto_inject: Force cache_control injection even if caller didn't specify
51
+ (uses LiteLLM's cache_control_injection_points)
52
+
53
+ Note: Provider mechanisms (auto, explicit, context_cache_api) are defined
54
+ in model_catalog.yaml under prompt_caching.mechanism, not here.
55
+ """
56
+
57
+ policy: PromptCachingPolicy = "passthrough"
58
+ # For auto_inject policy: where to inject cache_control
59
+ injection_points: list[InjectionPoint] | None = None
60
+
61
+ @model_validator(mode="after")
62
+ def validate_injection_points(self) -> Self:
63
+ """Validate injection_points is provided when policy is auto_inject."""
64
+ if self.policy == "auto_inject" and not self.injection_points:
65
+ # Default: cache system messages
66
+ self.injection_points = [InjectionPoint(location="message", role="system")]
67
+ return self
68
+
69
+
70
+ class ModelHyperparameters(BaseModel):
71
+ """Provider-agnostic parameters for LLM calls.
72
+
73
+ Timeout and prompt caching are operational parameters that vary by model:
74
+ - Reasoning models (GPT-5, o3) need longer timeouts (180-300s)
75
+ - Fast models (GPT-4o-mini, Haiku) can use shorter timeouts (30-60s)
76
+ - Prompt caching mode controls whether cache_control is passed through or auto-injected
77
+ """
78
+
79
+ max_tokens: int = 4096
80
+ temperature: float | None = None
81
+ top_p: float | None = None
82
+ reasoning_effort: ReasoningEffort | None = None
83
+ thinking: ThinkingConfig | None = None
84
+ verbosity: Verbosity | None = None
85
+ timeout: int | None = None # Request timeout in seconds (None = use model default)
86
+ prompt_caching: PromptCachingConfig | None = None
87
+ strict: bool = False # Raise UnsupportedParamError instead of warn+ignore
88
+ # Provider-specific extras, namespaced: {"openai": {...}, "anthropic": {...}}
89
+ extra: dict[str, dict[str, Any]] = Field(default_factory=dict)
90
+
91
+ @model_validator(mode="after")
92
+ def validate_hyperparameters(self) -> Self:
93
+ """Validate numeric hyperparameters are within valid ranges.
94
+
95
+ Catches invalid values early rather than failing at the LLM provider.
96
+ """
97
+ if self.max_tokens <= 0:
98
+ raise ValueError(f"max_tokens must be positive, got {self.max_tokens}")
99
+ if self.max_tokens > 1_000_000:
100
+ raise ValueError(f"max_tokens exceeds maximum (1M), got {self.max_tokens}")
101
+
102
+ if self.temperature is not None:
103
+ if not 0.0 <= self.temperature <= 2.0:
104
+ raise ValueError(f"temperature must be between 0.0 and 2.0, got {self.temperature}")
105
+
106
+ if self.top_p is not None:
107
+ if not 0.0 <= self.top_p <= 1.0:
108
+ raise ValueError(f"top_p must be between 0.0 and 1.0, got {self.top_p}")
109
+
110
+ if self.timeout is not None:
111
+ if self.timeout <= 0:
112
+ raise ValueError(f"timeout must be positive, got {self.timeout}")
113
+ if self.timeout > 600:
114
+ raise ValueError(f"timeout exceeds maximum (600s), got {self.timeout}")
115
+
116
+ return self
117
+
118
+
119
+ class ToolCall(BaseModel):
120
+ """Canonical tool call representation (stable across providers).
121
+
122
+ Represents a complete, parsed tool call ready for execution.
123
+ """
124
+
125
+ id: str
126
+ name: str
127
+ arguments: dict[str, Any] # Parsed arguments (not raw JSON string)
128
+
129
+
130
+ class ToolCallDelta(BaseModel):
131
+ """Partial tool call for streaming (accumulate until complete).
132
+
133
+ During streaming, tool calls arrive in fragments. Accumulate these
134
+ deltas until the stream completes, then parse into ToolCall.
135
+
136
+ OpenAI streaming sends `id` only on the first chunk for each tool call.
137
+ Subsequent argument chunks use `index` (integer) to correlate.
138
+ """
139
+
140
+ index: int | None = None # OpenAI tool call index (stable across chunks)
141
+ id: str | None = None
142
+ name: str | None = None
143
+ arguments_json: str = "" # Raw JSON fragment, parse when complete
144
+
145
+
146
+ class Message(BaseModel):
147
+ """Canonical message format."""
148
+
149
+ role: MessageRole
150
+ content: str | list[dict[str, Any]] # text or content blocks
151
+ tool_call_id: str | None = None # For role="tool" responses
152
+ tool_calls: list[ToolCall] | None = None # For role="assistant" with tool use
153
+
154
+
155
+ class CompletionResponse(BaseModel):
156
+ """Canonical completion response."""
157
+
158
+ text: str
159
+ tool_calls: list[ToolCall] | None = None
160
+ usage: dict[str, int] | None = None # {prompt_tokens, completion_tokens, total_tokens}
161
+ raw: dict[str, Any] | None = None # Original provider response (debugging only)
162
+
163
+
164
+ class StreamEvent(BaseModel):
165
+ """Canonical streaming event.
166
+
167
+ For type="response_end", tool_calls contains the finalized list of
168
+ complete ToolCall objects accumulated from tool_call_delta events.
169
+ """
170
+
171
+ type: StreamEventType
172
+ text: str | None = None
173
+ tool_call_delta: ToolCallDelta | None = None
174
+ tool_calls: list[ToolCall] | None = None # Finalized tool calls at response_end
175
+ usage: dict[str, int] | None = None
176
+ error: str | None = None
forge/core/logging.py ADDED
@@ -0,0 +1,146 @@
1
+ """File logging for Forge.
2
+
3
+ Activated by ``log_level`` config setting or ``FORGE_DEBUG=1`` env var.
4
+ Attaches a RotatingFileHandler to the ``forge`` logger namespace so all child
5
+ loggers (forge.cli.*, forge.session.*, forge.core.*, etc.) emit to disk.
6
+
7
+ No file I/O occurs when the effective log_level is "off".
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ import os
14
+ from logging.handlers import RotatingFileHandler
15
+ from pathlib import Path
16
+
17
+ _LEVEL_MAP = {
18
+ "debug": logging.DEBUG,
19
+ "info": logging.INFO,
20
+ "warning": logging.WARNING,
21
+ }
22
+
23
+
24
+ def get_effective_log_level() -> str:
25
+ """Resolve effective log level (env var overrides are applied in RuntimeConfig).
26
+
27
+ Returns:
28
+ "off", "debug", "info", or "warning".
29
+ """
30
+ try:
31
+ from forge.runtime_config import get_runtime_config
32
+
33
+ return get_runtime_config().log_level
34
+ except Exception:
35
+ return "off"
36
+
37
+
38
+ def configure_debug_logging(component: str, subdirectory: str) -> None:
39
+ """Attach a RotatingFileHandler to the 'forge' namespace.
40
+
41
+ Activates when log_level config is not "off" or FORGE_DEBUG=1.
42
+ Per-PID log files avoid multi-process rotation conflicts.
43
+
44
+ Fail-open: permission errors or missing dirs don't crash the command.
45
+
46
+ Args:
47
+ component: Log filename stem (e.g., "session-start", "session").
48
+ subdirectory: Directory under $FORGE_HOME/logs/ (e.g., "hooks", "cli").
49
+ """
50
+ level = get_effective_log_level()
51
+ if level == "off":
52
+ return
53
+
54
+ forge_logger = logging.getLogger("forge")
55
+
56
+ try:
57
+ from forge.core.paths import get_forge_home
58
+
59
+ logs_dir = get_forge_home() / "logs" / subdirectory
60
+ logs_dir.mkdir(exist_ok=True, parents=True)
61
+
62
+ pid = os.getpid()
63
+ log_file = logs_dir / f"{component}.{pid}.log"
64
+
65
+ # Idempotency: skip if THIS exact file handler is already attached.
66
+ for h in forge_logger.handlers:
67
+ if isinstance(h, RotatingFileHandler) and getattr(h, "baseFilename", None) == str(log_file):
68
+ return
69
+
70
+ py_level = _LEVEL_MAP.get(level, logging.DEBUG)
71
+ forge_logger.setLevel(py_level)
72
+ forge_logger.propagate = False
73
+
74
+ fmt = logging.Formatter(
75
+ "%(asctime)s.%(msecs)03d | %(levelname)-8s | %(process)d | "
76
+ "%(name)s:%(funcName)s:%(lineno)d - %(message)s",
77
+ datefmt="%Y-%m-%d %H:%M:%S",
78
+ )
79
+ handler = RotatingFileHandler(str(log_file), maxBytes=10 * 1024 * 1024, backupCount=5)
80
+ handler.setLevel(py_level)
81
+ handler.setFormatter(fmt)
82
+ forge_logger.addHandler(handler)
83
+
84
+ # Set 0600 perms — log files may contain payload fragments / hostnames.
85
+ try:
86
+ os.chmod(str(log_file), 0o600)
87
+ except OSError:
88
+ pass
89
+ except Exception:
90
+ pass # Fail-open: don't crash the command because logging couldn't start
91
+
92
+
93
+ def configure_console_logging() -> None:
94
+ """Attach a stderr StreamHandler to the 'forge' namespace.
95
+
96
+ For long-running processes (proxy server) that need visible console output
97
+ in addition to file logging. Idempotent: skips if a stderr handler exists.
98
+
99
+ No-op when log_level is "off".
100
+ """
101
+ level = get_effective_log_level()
102
+ if level == "off":
103
+ return
104
+
105
+ py_level = _LEVEL_MAP.get(level, logging.DEBUG)
106
+ forge_logger = logging.getLogger("forge")
107
+
108
+ import sys
109
+
110
+ has_stderr = any(
111
+ isinstance(h, logging.StreamHandler)
112
+ and not isinstance(h, logging.FileHandler)
113
+ and getattr(h, "stream", None) is sys.stderr
114
+ for h in forge_logger.handlers
115
+ )
116
+ if has_stderr:
117
+ return
118
+
119
+ handler = logging.StreamHandler(sys.stderr)
120
+ handler.setLevel(py_level)
121
+ handler.setFormatter(
122
+ logging.Formatter(
123
+ "%(asctime)s | %(levelname)-8s | %(process)d | %(name)s:%(funcName)s:%(lineno)d - %(message)s",
124
+ datefmt="%Y-%m-%d %H:%M:%S",
125
+ )
126
+ )
127
+ forge_logger.addHandler(handler)
128
+
129
+
130
+ def find_latest_log(subdirectory: str, glob_pattern: str) -> Path | None:
131
+ """Find the most recently modified log file in a subdirectory.
132
+
133
+ Args:
134
+ subdirectory: Directory under $FORGE_HOME/logs/ (e.g., "proxy").
135
+ glob_pattern: Glob pattern to match (e.g., "proxy.*.log").
136
+ """
137
+ try:
138
+ from forge.core.paths import get_forge_home
139
+
140
+ logs_dir = get_forge_home() / "logs" / subdirectory
141
+ if not logs_dir.exists():
142
+ return None
143
+ log_files = sorted(logs_dir.glob(glob_pattern), key=lambda p: p.stat().st_mtime, reverse=True)
144
+ return log_files[0] if log_files else None
145
+ except Exception:
146
+ return None
@@ -0,0 +1,91 @@
1
+ """Central model catalog for intrinsic model properties.
2
+
3
+ This module is the single source of truth for model capabilities:
4
+ - Context window sizes
5
+ - Maximum output tokens
6
+ - Thinking/reasoning support and configuration
7
+ - Temperature constraints
8
+ - Verbosity support
9
+ - API requirements (responses API, etc.)
10
+
11
+ Usage:
12
+ from forge.core.models import (
13
+ get_model_spec,
14
+ get_context_window_tokens,
15
+ get_max_output_tokens,
16
+ resolve_model_id,
17
+ model_exists,
18
+ )
19
+
20
+ # Get full spec for a model
21
+ spec = get_model_spec("gpt-5.5")
22
+ print(spec.context_window_tokens) # 400000
23
+ print(spec.litellm_reasoning_efforts) # ('none', 'low', 'medium', 'high', 'xhigh')
24
+ print(spec.supports_verbosity) # True
25
+
26
+ # Aliases work transparently
27
+ spec = get_model_spec("openai/gpt-5.5") # Same result
28
+
29
+ # Convenience functions
30
+ ctx = get_context_window_tokens("gemini-3.1-pro-preview") # 1048576
31
+ max_out = get_max_output_tokens("gpt-5.5") # 128000
32
+
33
+ # Check existence without raising
34
+ if model_exists("unknown-model"):
35
+ ...
36
+ """
37
+
38
+ from forge.core.models.catalog import (
39
+ ModelCatalogError,
40
+ get_compact_name,
41
+ get_context_window_tokens,
42
+ get_default_model,
43
+ get_max_output_tokens,
44
+ get_model_spec,
45
+ get_provider_defaults,
46
+ get_system_prompt_addendum,
47
+ load_model_catalog,
48
+ model_exists,
49
+ resolve_model_id,
50
+ )
51
+ from forge.core.models.pricing import (
52
+ ModelPricing,
53
+ calculate_cost,
54
+ get_pricing,
55
+ micros_to_usd,
56
+ )
57
+ from forge.core.models.types import (
58
+ ModelCatalog,
59
+ ModelSpec,
60
+ TemperatureSpec,
61
+ )
62
+
63
+ __all__ = [
64
+ # Catalog loader
65
+ "load_model_catalog",
66
+ # Lookup functions (strict)
67
+ "resolve_model_id",
68
+ "get_model_spec",
69
+ "get_context_window_tokens",
70
+ "get_max_output_tokens",
71
+ # Non-strict check
72
+ "model_exists",
73
+ # Defaults
74
+ "get_default_model",
75
+ "get_provider_defaults",
76
+ # Display
77
+ "get_compact_name",
78
+ # System prompt addendum
79
+ "get_system_prompt_addendum",
80
+ # Error type
81
+ "ModelCatalogError",
82
+ # Pricing
83
+ "ModelPricing",
84
+ "get_pricing",
85
+ "calculate_cost",
86
+ "micros_to_usd",
87
+ # Types
88
+ "ModelCatalog",
89
+ "ModelSpec",
90
+ "TemperatureSpec",
91
+ ]