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,328 @@
1
+ # Tool Parameter Guidance
2
+
3
+ You are working inside Claude Code, which provides tools with specific parameter contracts.
4
+
5
+ Follow these rules exactly. Most tool-call failures come from including optional parameters that are not needed. Before
6
+ every tool call, construct the parameter object from scratch using the smallest valid object for that specific call. Do
7
+ not reuse a previous failed tool-call object.
8
+
9
+ ## Universal tool-call rule
10
+
11
+ For every tool call:
12
+
13
+ 1. Start with only the required parameters.
14
+ 2. Add an optional parameter only if this exact call needs it.
15
+ 3. Never include optional parameters with placeholder values.
16
+ 4. Never pass `""`, `null`, `[]`, or `{}` just to satisfy a schema.
17
+ 5. If a tool call fails because of an invalid optional parameter, the next retry MUST remove that parameter entirely
18
+ unless it is required.
19
+ 6. Prefer omitting optional fields over passing empty or default-looking values.
20
+ 7. A field that is “not applicable” must be absent from the JSON object, not present with an empty value.
21
+
22
+ Bad:
23
+
24
+ ```json
25
+ {"file_path":"/workspace/README.md","pages":""}
26
+ ```
27
+
28
+ Good:
29
+
30
+ ```json
31
+ {"file_path":"/workspace/README.md"}
32
+ ```
33
+
34
+ ## Read
35
+
36
+ Default call shape for ordinary files:
37
+
38
+ ```json
39
+ {"file_path":"/absolute/path/to/file"}
40
+ ```
41
+
42
+ Use this default for normal-sized non-PDF files, including:
43
+
44
+ - .md
45
+ - .txt
46
+ - source code files
47
+ - .json
48
+ - .yaml / .yml
49
+ - config files
50
+ - notebooks, unless notebook-specific handling is explicitly needed
51
+
52
+ Do not add offset, limit, or pages unless this exact read requires them.
53
+
54
+ ### `pages`
55
+
56
+ `pages` is only for PDF files.
57
+
58
+ If `file_path` does not end in `.pdf`, the `pages` key is forbidden.
59
+
60
+ For non-PDF files:
61
+
62
+ - Do not include `pages`.
63
+ - Do not include `"pages": ""`.
64
+ - Do not include `"pages": null`.
65
+ - Do not include `"pages": "1"`.
66
+ - Do not include `pages` together with `offset`/`limit`.
67
+ - The correct non-PDF retry after any `pages` error is usually only:
68
+
69
+ ```json
70
+ {"file_path":"/absolute/path/to/file"}
71
+ ```
72
+
73
+ Correct non-PDF examples:
74
+
75
+ ```json
76
+ {"file_path":"/workspace/README.md"}
77
+ ```
78
+
79
+ ```json
80
+ {"file_path":"/workspace/src/app.ts"}
81
+ ```
82
+
83
+ ```json
84
+ {"file_path":"/workspace/package.json"}
85
+ ```
86
+
87
+ Correct PDF examples:
88
+
89
+ ```json
90
+ {"file_path":"/workspace/spec.pdf","pages":"1-5"}
91
+ ```
92
+
93
+ ```json
94
+ {"file_path":"/workspace/spec.pdf","pages":"3"}
95
+ ```
96
+
97
+ Incorrect:
98
+
99
+ ```json
100
+ {"file_path":"/workspace/README.md","pages":""}
101
+ ```
102
+
103
+ ```json
104
+ {"file_path":"/workspace/README.md","pages":"1"}
105
+ ```
106
+
107
+ ```json
108
+ {"file_path":"/workspace/README.md","offset":1,"limit":2000,"pages":""}
109
+ ```
110
+
111
+ ```json
112
+ {"file_path":"/workspace/src/app.ts","pages":null}
113
+ ```
114
+
115
+ ### `offset` and `limit`
116
+
117
+ `offset` and `limit` are optional.
118
+
119
+ For normal-sized files, omit both and let the tool return the full content.
120
+
121
+ Default:
122
+
123
+ ```json
124
+ {"file_path":"/absolute/path/to/file"}
125
+ ```
126
+
127
+ Only include `offset` and/or `limit` when:
128
+
129
+ - the file is known to be large,
130
+ - you need a specific section,
131
+ - a previous successful read showed that the relevant content is outside the default range.
132
+
133
+ Correct large-file section example:
134
+
135
+ ```json
136
+ {"file_path":"/absolute/path/to/file","offset":2000,"limit":200}
137
+ ```
138
+
139
+ Incorrect for ordinary files:
140
+
141
+ ```json
142
+ {"file_path":"/workspace/README.md","offset":1,"limit":2000}
143
+ ```
144
+
145
+ The above is not invalid, but it is unnecessary. Prefer:
146
+
147
+ ```json
148
+ {"file_path":"/workspace/README.md"}
149
+ ```
150
+
151
+ ### Read retry rule
152
+
153
+ If a Read call fails because of an invalid optional parameter, retry with the smallest valid object.
154
+
155
+ For a non-PDF file, that means:
156
+
157
+ ```json
158
+ {"file_path":"/absolute/path/to/file"}
159
+ ```
160
+
161
+ Do not use Bash as a workaround for a failed Read call until you have retried once with the minimal valid Read object.
162
+
163
+ ## Edit
164
+
165
+ Use Edit for modifying existing files whenever possible.
166
+
167
+ `old_string` must be an exact substring of the current file content.
168
+
169
+ Rules:
170
+
171
+ - Copy `old_string` character-for-character from the file content.
172
+ - Preserve exact whitespace and indentation.
173
+ - Never include line number prefixes from Read.
174
+ - The line number prefix shown by Read is display-only and is not part of the file.
175
+ - If `old_string` is not unique, include more surrounding context.
176
+ - Do not make broad replacements when a narrow exact replacement is possible.
177
+ - Prefer Edit over Write for existing files.
178
+
179
+ Correct:
180
+
181
+ ```json
182
+ {
183
+ "file_path":"/workspace/src/app.py",
184
+ "old_string":"def greet(name):\n return f\"Hello {name}\"",
185
+ "new_string":"def greet(name):\n return f\"Hello, {name}!\""
186
+ }
187
+ ```
188
+
189
+ Incorrect because it includes a line number prefix:
190
+
191
+ ```json
192
+ {
193
+ "file_path":"/workspace/src/app.py",
194
+ "old_string":"12\tdef greet(name):\n13\t return f\"Hello {name}\"",
195
+ "new_string":"def greet(name):\n return f\"Hello, {name}!\""
196
+ }
197
+ ```
198
+
199
+ ### Edit retry rule
200
+
201
+ If Edit fails because `old_string` is not found:
202
+
203
+ 1. Re-read the relevant file or section.
204
+ 2. Copy the exact current text.
205
+ 3. Retry with a more precise `old_string`.
206
+
207
+ If Edit fails because `old_string` is not unique:
208
+
209
+ 1. Include more surrounding lines in `old_string`.
210
+ 2. Do not use `replace_all` unless every occurrence should actually change.
211
+
212
+ ## Write
213
+
214
+ Use Write only for:
215
+
216
+ - creating a new file,
217
+ - intentionally replacing an entire existing file after reading it first.
218
+
219
+ Rules:
220
+
221
+ - Prefer Edit for modifying existing files.
222
+ - Do not use Write to make a small change to an existing file.
223
+ - If writing an existing file, read it first.
224
+ - Do not create documentation files unless explicitly requested by the user.
225
+ - Do not use emojis in files unless explicitly requested by the user.
226
+
227
+ Correct use for a new file:
228
+
229
+ ```json
230
+ {
231
+ "file_path":"/workspace/src/new_module.py",
232
+ "content":"def main():\n return None\n"
233
+ }
234
+ ```
235
+
236
+ Incorrect use for a small edit to an existing file:
237
+
238
+ ```json
239
+ {
240
+ "file_path":"/workspace/src/app.py",
241
+ "content":"<entire rewritten file just to change one line>"
242
+ }
243
+ ```
244
+
245
+ Use Edit instead.
246
+
247
+ ## Bash
248
+
249
+ Prefer dedicated tools over Bash when a dedicated tool fits.
250
+
251
+ Use:
252
+
253
+ - Read instead of `cat`, `head`, `tail`, or `sed -n`.
254
+ - Edit instead of `sed -i`, `perl -pi`, or shell redirection.
255
+ - Write instead of `cat > file` or heredoc file creation.
256
+
257
+ Do not use Bash as a workaround for a failed dedicated tool call until you have retried once with the minimal valid
258
+ parameter object.
259
+
260
+ Example:
261
+
262
+ If this fails:
263
+
264
+ ```json
265
+ {"file_path":"/workspace/README.md","pages":""}
266
+ ```
267
+
268
+ Do not immediately use Bash. Retry:
269
+
270
+ ```json
271
+ {"file_path":"/workspace/README.md"}
272
+ ```
273
+
274
+ Only use Bash if the dedicated tool still cannot accomplish the task.
275
+
276
+ ## Common valid tool-call shapes
277
+
278
+ ### Read an ordinary file
279
+
280
+ ```json
281
+ {"file_path":"/workspace/README.md"}
282
+ ```
283
+
284
+ ### Read a section of a large ordinary file
285
+
286
+ ```json
287
+ {"file_path":"/workspace/large.log","offset":1000,"limit":200}
288
+ ```
289
+
290
+ ### Read a PDF page range
291
+
292
+ ```json
293
+ {"file_path":"/workspace/spec.pdf","pages":"1-5"}
294
+ ```
295
+
296
+ ### Edit an existing file
297
+
298
+ ```json
299
+ {
300
+ "file_path":"/workspace/src/app.py",
301
+ "old_string":"old exact text",
302
+ "new_string":"new exact text"
303
+ }
304
+ ```
305
+
306
+ ### Create a new file
307
+
308
+ ```json
309
+ {
310
+ "file_path":"/workspace/src/new_file.py",
311
+ "content":"file contents\n"
312
+ }
313
+ ```
314
+
315
+ ## Final preflight checklist
316
+
317
+ Before sending any tool call, ask:
318
+
319
+ 1. Are all required fields present?
320
+ 2. Did I omit every optional field that is not needed?
321
+ 3. Did I avoid empty-string, null, empty-list, and empty-object placeholders?
322
+ 4. If this is Read, is `pages` absent unless the file is a PDF?
323
+ 5. If this is Read for a normal file, am I using only `file_path`?
324
+ 6. If this is a retry after a parameter error, did I remove the invalid optional parameter entirely?
325
+ 7. If this is Edit, did I copy `old_string` exactly from the current file and exclude line numbers?
326
+ 8. If this is Write, am I creating a new file or intentionally replacing the whole file?
327
+
328
+ When in doubt, use the smallest valid object.
@@ -0,0 +1,231 @@
1
+ """Shared LLM client abstraction for Forge components.
2
+
3
+ This module provides a unified, async-first interface for calling LLMs
4
+ across different providers (LiteLLM, Anthropic).
5
+
6
+ Usage:
7
+ # Async usage (Proxy)
8
+ from forge.core.llm import get_client, Message
9
+
10
+ client = get_client("openai/gpt-5.2")
11
+ response = await client.complete([Message(role="user", content="Hello")])
12
+
13
+ # Streaming
14
+ async for event in client.stream(messages):
15
+ if event.type == "text_delta":
16
+ print(event.text, end="")
17
+
18
+ # Sync usage (Guard)
19
+ from forge.core.llm import get_client, SyncAdapter
20
+
21
+ client = SyncAdapter(get_client("openai/gpt-5.2"))
22
+ response = client.ask("Analyze this code...")
23
+ """
24
+
25
+ import asyncio
26
+ from typing import Any
27
+
28
+ from .clients.litellm import LiteLLMClient
29
+ from .credentials import CredentialManager
30
+ from .detection import ProviderType, detect_provider, is_implemented
31
+ from .errors import (
32
+ AuthenticationError,
33
+ LLMError,
34
+ NoApiKeyError,
35
+ ProviderError,
36
+ UnsupportedParamError,
37
+ )
38
+ from .protocols import LLMClient
39
+ from .types import (
40
+ CompletionResponse,
41
+ InjectionPoint,
42
+ Message,
43
+ ModelHyperparameters,
44
+ PromptCachingConfig,
45
+ PromptCachingPolicy,
46
+ StreamEvent,
47
+ ThinkingConfig,
48
+ ToolCall,
49
+ ToolCallDelta,
50
+ )
51
+
52
+ __all__ = [
53
+ # Factory
54
+ "get_client",
55
+ "SyncAdapter",
56
+ # Types
57
+ "Message",
58
+ "CompletionResponse",
59
+ "StreamEvent",
60
+ "ToolCall",
61
+ "ToolCallDelta",
62
+ "ModelHyperparameters",
63
+ "ThinkingConfig",
64
+ "PromptCachingConfig",
65
+ "PromptCachingPolicy",
66
+ "InjectionPoint",
67
+ # Protocol
68
+ "LLMClient",
69
+ # Errors
70
+ "LLMError",
71
+ "NoApiKeyError",
72
+ "AuthenticationError",
73
+ "ProviderError",
74
+ "UnsupportedParamError",
75
+ # Detection
76
+ "ProviderType",
77
+ "detect_provider",
78
+ # Credentials
79
+ "CredentialManager",
80
+ ]
81
+
82
+
83
+ def get_client(
84
+ model: str,
85
+ *,
86
+ provider: ProviderType | None = None,
87
+ credentials: CredentialManager | None = None,
88
+ default_hyperparams: ModelHyperparameters | None = None,
89
+ ) -> LLMClient:
90
+ """Get an LLM client for the given model.
91
+
92
+ The factory is sync; credential fetching is deferred to first
93
+ complete()/stream() call. This allows sync code to construct
94
+ clients without needing an event loop.
95
+
96
+ Args:
97
+ model: Model identifier with provider prefix (e.g., "openai/gpt-5.2").
98
+ provider: Explicit provider override (detected from prefix if not provided).
99
+ credentials: Custom credential manager (uses default if not provided).
100
+ default_hyperparams: Default hyperparameters for all calls on this client.
101
+
102
+ Returns:
103
+ LLMClient instance for the appropriate provider.
104
+
105
+ Raises:
106
+ ValueError: If model ID is not prefixed (unprefixed models not supported).
107
+ NotImplementedError: If the detected provider is not yet implemented.
108
+
109
+ Examples:
110
+ >>> client = get_client("openai/gpt-5.2")
111
+ >>> client = get_client("vertex_ai/gemini-3.1-pro-preview")
112
+ >>> client = get_client("gemini/gemini-2.0-flash") # Local LiteLLM
113
+ """
114
+ resolved_provider = provider or detect_provider(model)
115
+ creds_manager = credentials or CredentialManager.default()
116
+
117
+ # Check if provider is implemented
118
+ if not is_implemented(resolved_provider):
119
+ if resolved_provider == "anthropic":
120
+ raise NotImplementedError(
121
+ f"Direct Anthropic client not yet implemented. "
122
+ f"Use 'anthropic/{model.split('/')[-1] if '/' in model else model}' "
123
+ f"prefix to route via LiteLLM."
124
+ )
125
+ else:
126
+ raise NotImplementedError(f"Provider '{resolved_provider}' not yet implemented.")
127
+
128
+ if resolved_provider == "openrouter":
129
+ from .clients.openrouter import OpenRouterClient
130
+
131
+ return OpenRouterClient(
132
+ model=model,
133
+ provider=resolved_provider,
134
+ credentials=creds_manager,
135
+ default_hyperparams=default_hyperparams,
136
+ )
137
+
138
+ return LiteLLMClient(
139
+ model=model,
140
+ provider=resolved_provider,
141
+ credentials=creds_manager,
142
+ default_hyperparams=default_hyperparams,
143
+ )
144
+
145
+
146
+ class SyncAdapter:
147
+ """Wraps async LLMClient for synchronous usage.
148
+
149
+ CONSTRAINT: Cannot be used inside an event loop.
150
+ asyncio.run() raises RuntimeError if a loop is running.
151
+ Use the async client directly in async contexts.
152
+
153
+ Usage:
154
+ client = SyncAdapter(get_client("openai/gpt-5.2"))
155
+ response = client.ask("Analyze this code...")
156
+ """
157
+
158
+ def __init__(self, client: LLMClient) -> None:
159
+ """Initialize sync adapter.
160
+
161
+ Args:
162
+ client: Async LLMClient to wrap.
163
+ """
164
+ self._client = client
165
+
166
+ @property
167
+ def model(self) -> str:
168
+ """The model this client is configured for."""
169
+ return self._client.model
170
+
171
+ def _check_no_running_loop(self) -> None:
172
+ """Ensure we're not inside an event loop.
173
+
174
+ Raises:
175
+ RuntimeError: If called from inside an event loop.
176
+ """
177
+ try:
178
+ asyncio.get_running_loop()
179
+ raise RuntimeError(
180
+ "SyncAdapter cannot be used inside an event loop. " "Use the async client directly in async contexts."
181
+ )
182
+ except RuntimeError as e:
183
+ # RuntimeError is raised when no loop is running - that's what we want
184
+ if "no running event loop" not in str(e).lower():
185
+ raise
186
+
187
+ def ask(
188
+ self,
189
+ prompt: str,
190
+ *,
191
+ system: str | None = None,
192
+ hyperparams: ModelHyperparameters | None = None,
193
+ ) -> str:
194
+ """Simple prompt-to-response interface.
195
+
196
+ Args:
197
+ prompt: User prompt.
198
+ system: Optional system prompt.
199
+ hyperparams: Optional hyperparameters.
200
+
201
+ Returns:
202
+ Response text from the model.
203
+ """
204
+ self._check_no_running_loop()
205
+
206
+ messages = [Message(role="user", content=prompt)]
207
+ if system:
208
+ messages.insert(0, Message(role="system", content=system))
209
+
210
+ response = asyncio.run(self._client.complete(messages, hyperparams=hyperparams))
211
+ return response.text
212
+
213
+ def complete(
214
+ self,
215
+ messages: list[Message],
216
+ *,
217
+ tools: list[dict[str, Any]] | None = None,
218
+ hyperparams: ModelHyperparameters | None = None,
219
+ ) -> CompletionResponse:
220
+ """Synchronous completion with full control.
221
+
222
+ Args:
223
+ messages: List of messages in the conversation.
224
+ tools: Optional list of tool definitions.
225
+ hyperparams: Optional hyperparameters.
226
+
227
+ Returns:
228
+ CompletionResponse with text, optional tool_calls, and usage.
229
+ """
230
+ self._check_no_running_loop()
231
+ return asyncio.run(self._client.complete(messages, tools=tools, hyperparams=hyperparams))
@@ -0,0 +1,14 @@
1
+ """LLM client implementations.
2
+
3
+ Currently implemented:
4
+ - LiteLLMClient: For both remote and local LiteLLM instances
5
+
6
+ Deferred (not yet implemented):
7
+ - AnthropicClient: Direct Anthropic API
8
+ """
9
+
10
+ from .litellm import LiteLLMClient
11
+ from .openai_compat import ToolCallAccumulator
12
+ from .openrouter import OpenRouterClient
13
+
14
+ __all__ = ["LiteLLMClient", "OpenRouterClient", "ToolCallAccumulator"]
@@ -0,0 +1,115 @@
1
+ """Base helpers for LLM client implementations."""
2
+
3
+ import logging
4
+
5
+ from ..errors import UnsupportedParamError
6
+ from ..types import ModelHyperparameters
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def merge_hyperparams(
12
+ defaults: ModelHyperparameters | None,
13
+ call_time: ModelHyperparameters | None,
14
+ ) -> ModelHyperparameters:
15
+ """Merge default hyperparameters with call-time overrides.
16
+
17
+ Call-time values take precedence, but only for fields explicitly set by
18
+ the caller. Default values on ModelHyperparameters do not override client
19
+ defaults (uses exclude_unset, not exclude_none).
20
+
21
+ Args:
22
+ defaults: Default hyperparameters (from client initialization).
23
+ call_time: Call-time hyperparameters (from complete/stream call).
24
+
25
+ Returns:
26
+ Merged hyperparameters.
27
+ """
28
+ if defaults is None and call_time is None:
29
+ return ModelHyperparameters()
30
+ if defaults is None:
31
+ return call_time or ModelHyperparameters()
32
+ if call_time is None:
33
+ return defaults
34
+
35
+ # Merge: only explicitly-set call_time values override defaults
36
+ merged_data = defaults.model_dump()
37
+ call_data = call_time.model_dump(exclude_unset=True)
38
+
39
+ # Special handling for nested dicts (extra)
40
+ if "extra" in call_data:
41
+ merged_extra = merged_data.get("extra", {}).copy()
42
+ for namespace, params in call_data["extra"].items():
43
+ if namespace in merged_extra:
44
+ merged_extra[namespace] = {**merged_extra[namespace], **params}
45
+ else:
46
+ merged_extra[namespace] = params
47
+ call_data["extra"] = merged_extra
48
+
49
+ merged_data.update(call_data)
50
+ return ModelHyperparameters(**merged_data)
51
+
52
+
53
+ def handle_unsupported_param(
54
+ param: str,
55
+ value: object,
56
+ provider: str,
57
+ strict: bool,
58
+ ) -> None:
59
+ """Handle an unsupported parameter.
60
+
61
+ In strict mode, raises UnsupportedParamError.
62
+ Otherwise, logs a warning and continues.
63
+
64
+ Args:
65
+ param: Parameter name that is not supported.
66
+ value: The value that was provided.
67
+ provider: Provider that doesn't support this parameter.
68
+ strict: Whether to raise an error (True) or warn (False).
69
+
70
+ Raises:
71
+ UnsupportedParamError: If strict mode is enabled.
72
+ """
73
+ if strict:
74
+ raise UnsupportedParamError(param, provider)
75
+ else:
76
+ logger.warning(f"Parameter '{param}' with value '{value}' not supported by {provider}, ignoring")
77
+
78
+
79
+ def estimate_tokens_simple(text: str) -> int:
80
+ """Simple token estimation (4 chars per token).
81
+
82
+ This is a conservative estimate for when provider-specific
83
+ tokenization is not available.
84
+
85
+ Args:
86
+ text: Text to estimate tokens for.
87
+
88
+ Returns:
89
+ Estimated token count.
90
+ """
91
+ return len(text) // 4 + 1
92
+
93
+
94
+ def estimate_message_tokens(messages: list[dict[str, object]]) -> int:
95
+ """Estimate tokens for a list of messages.
96
+
97
+ Args:
98
+ messages: List of message dicts with 'content' field.
99
+
100
+ Returns:
101
+ Estimated total token count.
102
+ """
103
+ total = 0
104
+ for msg in messages:
105
+ content = msg.get("content", "")
106
+ if isinstance(content, str):
107
+ total += estimate_tokens_simple(content)
108
+ elif isinstance(content, list):
109
+ # Multimodal content - estimate text parts only
110
+ for part in content:
111
+ if isinstance(part, dict) and part.get("type") == "text":
112
+ total += estimate_tokens_simple(str(part.get("text", "")))
113
+ # Add overhead per message
114
+ total += 10
115
+ return total