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.
- forge/__init__.py +3 -0
- forge/_extensions/agents/.gitkeep +0 -0
- forge/_extensions/commands/.gitkeep +0 -0
- forge/_extensions/skills/analyze/SKILL.md +87 -0
- forge/_extensions/skills/challenge/SKILL.md +91 -0
- forge/_extensions/skills/consensus/SKILL.md +120 -0
- forge/_extensions/skills/consensus/resources/code_consensus_evaluation.md +94 -0
- forge/_extensions/skills/consensus/resources/consensus_evaluation.md +70 -0
- forge/_extensions/skills/consensus/resources/synthesis.md +101 -0
- forge/_extensions/skills/debate/SKILL.md +116 -0
- forge/_extensions/skills/debate/resources/code_debate_evaluation.md +101 -0
- forge/_extensions/skills/debate/resources/debate_evaluation.md +90 -0
- forge/_extensions/skills/panel/SKILL.md +141 -0
- forge/_extensions/skills/panel/resources/synthesis.md +103 -0
- forge/_extensions/skills/qa/SKILL.md +704 -0
- forge/_extensions/skills/qa/resources/checklist/0-enable.md +78 -0
- forge/_extensions/skills/qa/resources/checklist/1-preflight.md +24 -0
- forge/_extensions/skills/qa/resources/checklist/10-resume.md +143 -0
- forge/_extensions/skills/qa/resources/checklist/11-config.md +150 -0
- forge/_extensions/skills/qa/resources/checklist/12-search.md +58 -0
- forge/_extensions/skills/qa/resources/checklist/13-guard.md +237 -0
- forge/_extensions/skills/qa/resources/checklist/14-workflow.md +305 -0
- forge/_extensions/skills/qa/resources/checklist/15-skills.md +155 -0
- forge/_extensions/skills/qa/resources/checklist/16-handoff.md +224 -0
- forge/_extensions/skills/qa/resources/checklist/17-info.md +50 -0
- forge/_extensions/skills/qa/resources/checklist/18-disable.md +84 -0
- forge/_extensions/skills/qa/resources/checklist/19-uninstall.md +146 -0
- forge/_extensions/skills/qa/resources/checklist/2-extensions.md +188 -0
- forge/_extensions/skills/qa/resources/checklist/20-cleanup.md +36 -0
- forge/_extensions/skills/qa/resources/checklist/3-auth.md +234 -0
- forge/_extensions/skills/qa/resources/checklist/4-proxy.md +481 -0
- forge/_extensions/skills/qa/resources/checklist/5-session.md +541 -0
- forge/_extensions/skills/qa/resources/checklist/6-hooks.md +275 -0
- forge/_extensions/skills/qa/resources/checklist/7-costs.md +309 -0
- forge/_extensions/skills/qa/resources/checklist/8-status-line.md +174 -0
- forge/_extensions/skills/qa/resources/checklist/9-direct-commands.md +146 -0
- forge/_extensions/skills/qa/resources/checklist.md +103 -0
- forge/_extensions/skills/qa/resources/report-template.md +62 -0
- forge/_extensions/skills/qa/scripts/start-container.sh +529 -0
- forge/_extensions/skills/qa/scripts/walkthrough-state.py +1137 -0
- forge/_extensions/skills/review/SKILL.md +125 -0
- forge/_extensions/skills/review/references/claude-4.6.md +474 -0
- forge/_extensions/skills/review/references/claude-4.7.md +710 -0
- forge/_extensions/skills/review/references/gemini-3.1.md +546 -0
- forge/_extensions/skills/review/references/gpt-5.5.md +490 -0
- forge/_extensions/skills/review/references/skills-writing-guide.md +1588 -0
- forge/_extensions/skills/review/resources/code-anthropic.md +160 -0
- forge/_extensions/skills/review/resources/code-gemini.md +184 -0
- forge/_extensions/skills/review/resources/code-openai.md +203 -0
- forge/_extensions/skills/review/resources/code.md +160 -0
- forge/_extensions/skills/review-docs/SKILL.md +121 -0
- forge/_extensions/skills/review-docs/resources/docs-anthropic.md +170 -0
- forge/_extensions/skills/review-docs/resources/docs-gemini.md +204 -0
- forge/_extensions/skills/review-docs/resources/docs-openai.md +231 -0
- forge/_extensions/skills/review-docs/resources/docs.md +170 -0
- forge/_extensions/skills/smoke-test/SKILL.md +27 -0
- forge/_extensions/skills/smoke-test/scripts/smoke-test.sh +118 -0
- forge/_extensions/skills/understand/SKILL.md +148 -0
- forge/_extensions/skills/understand/resources/code-anthropic.md +163 -0
- forge/_extensions/skills/understand/resources/code-gemini.md +194 -0
- forge/_extensions/skills/understand/resources/code-openai.md +181 -0
- forge/_extensions/skills/understand/resources/code.md +163 -0
- forge/_extensions/skills/understand/resources/docs-anthropic.md +177 -0
- forge/_extensions/skills/understand/resources/docs-gemini.md +202 -0
- forge/_extensions/skills/understand/resources/docs-openai.md +191 -0
- forge/_extensions/skills/understand/resources/docs.md +177 -0
- forge/_extensions/skills/walkthrough/SKILL.md +599 -0
- forge/_extensions/skills/walkthrough/resources/checklist.md +765 -0
- forge/_extensions/skills/walkthrough/scripts/run-in-repo.sh +118 -0
- forge/_extensions/skills/walkthrough/scripts/setup-test-repo.sh +198 -0
- forge/_extensions/skills/walkthrough/scripts/walkthrough-state.py +1137 -0
- forge/backend/__init__.py +174 -0
- forge/backend/adapters/__init__.py +38 -0
- forge/backend/adapters/litellm.py +158 -0
- forge/backend/creation.py +89 -0
- forge/backend/registry.py +178 -0
- forge/cli/__init__.py +16 -0
- forge/cli/auth.py +483 -0
- forge/cli/backend.py +298 -0
- forge/cli/claude.py +411 -0
- forge/cli/config_cmd.py +303 -0
- forge/cli/extensions.py +1001 -0
- forge/cli/gc.py +165 -0
- forge/cli/guard.py +1018 -0
- forge/cli/guards.py +106 -0
- forge/cli/handoff.py +110 -0
- forge/cli/hooks/__init__.py +36 -0
- forge/cli/hooks/_group.py +20 -0
- forge/cli/hooks/_helpers.py +149 -0
- forge/cli/hooks/commands.py +1677 -0
- forge/cli/hooks/direct_commands.py +1304 -0
- forge/cli/hooks/install.py +232 -0
- forge/cli/hooks/policy.py +151 -0
- forge/cli/hooks/read_hygiene.py +74 -0
- forge/cli/hooks/verification.py +370 -0
- forge/cli/logs.py +406 -0
- forge/cli/main.py +292 -0
- forge/cli/proxy.py +1821 -0
- forge/cli/proxy_costs.py +313 -0
- forge/cli/search.py +416 -0
- forge/cli/session.py +892 -0
- forge/cli/session_addendum.py +81 -0
- forge/cli/session_fork.py +750 -0
- forge/cli/session_handoff.py +141 -0
- forge/cli/session_lifecycle.py +2053 -0
- forge/cli/session_manage.py +1336 -0
- forge/cli/session_memory.py +201 -0
- forge/cli/status_line.py +1398 -0
- forge/cli/workflow.py +1964 -0
- forge/config/__init__.py +110 -0
- forge/config/dataclass_utils.py +88 -0
- forge/config/defaults/__init__.py +0 -0
- forge/config/defaults/backends/__init__.py +0 -0
- forge/config/defaults/backends/litellm.yaml +196 -0
- forge/config/defaults/templates/__init__.py +0 -0
- forge/config/defaults/templates/litellm-anthropic-local.yaml +33 -0
- forge/config/defaults/templates/litellm-anthropic.yaml +24 -0
- forge/config/defaults/templates/litellm-gemini-flash-local.yaml +37 -0
- forge/config/defaults/templates/litellm-gemini-local.yaml +32 -0
- forge/config/defaults/templates/litellm-gemini-test.yaml +34 -0
- forge/config/defaults/templates/litellm-gemini.yaml +21 -0
- forge/config/defaults/templates/litellm-openai-codex-local.yaml +36 -0
- forge/config/defaults/templates/litellm-openai-local.yaml +38 -0
- forge/config/defaults/templates/litellm-openai.yaml +28 -0
- forge/config/defaults/templates/openrouter-anthropic.yaml +23 -0
- forge/config/defaults/templates/openrouter-deepseek.yaml +26 -0
- forge/config/defaults/templates/openrouter-gemini-flash.yaml +26 -0
- forge/config/defaults/templates/openrouter-gemini.yaml +23 -0
- forge/config/defaults/templates/openrouter-glm.yaml +23 -0
- forge/config/defaults/templates/openrouter-kimi.yaml +30 -0
- forge/config/defaults/templates/openrouter-minimax.yaml +26 -0
- forge/config/defaults/templates/openrouter-openai-codex.yaml +23 -0
- forge/config/defaults/templates/openrouter-openai.yaml +28 -0
- forge/config/defaults/templates/openrouter-qwen.yaml +25 -0
- forge/config/loader.py +675 -0
- forge/config/schema.py +448 -0
- forge/core/__init__.py +5 -0
- forge/core/auth/__init__.py +67 -0
- forge/core/auth/capabilities.py +219 -0
- forge/core/auth/credentials_file.py +244 -0
- forge/core/auth/protocols.py +18 -0
- forge/core/auth/secrets.py +243 -0
- forge/core/auth/template_secrets.py +112 -0
- forge/core/data/__init__.py +5 -0
- forge/core/data/model_catalog.yaml +1522 -0
- forge/core/data/pricing.yaml +140 -0
- forge/core/data/system_prompt_addendums/__init__.py +0 -0
- forge/core/data/system_prompt_addendums/gemini.md +330 -0
- forge/core/data/system_prompt_addendums/openai.md +328 -0
- forge/core/llm/__init__.py +231 -0
- forge/core/llm/clients/__init__.py +14 -0
- forge/core/llm/clients/base.py +115 -0
- forge/core/llm/clients/litellm.py +619 -0
- forge/core/llm/clients/openai_compat.py +244 -0
- forge/core/llm/clients/openrouter.py +234 -0
- forge/core/llm/credentials.py +439 -0
- forge/core/llm/detection.py +86 -0
- forge/core/llm/errors.py +44 -0
- forge/core/llm/protocols.py +80 -0
- forge/core/llm/types.py +176 -0
- forge/core/logging.py +146 -0
- forge/core/models/__init__.py +91 -0
- forge/core/models/catalog.py +467 -0
- forge/core/models/pricing.py +165 -0
- forge/core/models/types.py +167 -0
- forge/core/naming.py +212 -0
- forge/core/ops/__init__.py +73 -0
- forge/core/ops/context.py +141 -0
- forge/core/ops/gc.py +802 -0
- forge/core/ops/proxy.py +146 -0
- forge/core/ops/resolution.py +135 -0
- forge/core/ops/session.py +344 -0
- forge/core/ops/session_context.py +548 -0
- forge/core/paths.py +38 -0
- forge/core/process.py +54 -0
- forge/core/reactive/__init__.py +38 -0
- forge/core/reactive/cost_tracking.py +300 -0
- forge/core/reactive/env.py +180 -0
- forge/core/reactive/proxy.py +78 -0
- forge/core/reactive/routing.py +622 -0
- forge/core/reactive/session_runner.py +185 -0
- forge/core/reactive/structured_output.py +62 -0
- forge/core/reactive/tagger.py +94 -0
- forge/core/reactive/throttle.py +132 -0
- forge/core/state/__init__.py +59 -0
- forge/core/state/exceptions.py +59 -0
- forge/core/state/io.py +140 -0
- forge/core/state/lock.py +99 -0
- forge/core/state/timestamps.py +60 -0
- forge/core/transcript.py +78 -0
- forge/core/typing_helpers.py +24 -0
- forge/core/workqueue/__init__.py +67 -0
- forge/core/workqueue/queue.py +552 -0
- forge/core/workqueue/types.py +63 -0
- forge/guard/__init__.py +26 -0
- forge/guard/deterministic/__init__.py +26 -0
- forge/guard/deterministic/base.py +158 -0
- forge/guard/deterministic/coding_standards.py +256 -0
- forge/guard/deterministic/registry.py +148 -0
- forge/guard/deterministic/tdd.py +171 -0
- forge/guard/engine.py +216 -0
- forge/guard/protocols.py +91 -0
- forge/guard/queries.py +96 -0
- forge/guard/semantic/__init__.py +34 -0
- forge/guard/semantic/promotion.py +18 -0
- forge/guard/semantic/supervisor.py +813 -0
- forge/guard/semantic/verdict.py +183 -0
- forge/guard/store.py +124 -0
- forge/guard/team/__init__.py +6 -0
- forge/guard/team/config.py +24 -0
- forge/guard/team/handlers.py +209 -0
- forge/guard/team/prompts.py +41 -0
- forge/guard/types.py +125 -0
- forge/guard/workflow/__init__.py +17 -0
- forge/guard/workflow/branches.py +67 -0
- forge/guard/workflow/config.py +63 -0
- forge/guard/workflow/divergence.py +113 -0
- forge/guard/workflow/policy.py +87 -0
- forge/guard/workflow/stages.py +205 -0
- forge/install/__init__.py +55 -0
- forge/install/cli.py +281 -0
- forge/install/exceptions.py +163 -0
- forge/install/hooks.py +109 -0
- forge/install/installer.py +1037 -0
- forge/install/models.py +321 -0
- forge/install/preset.py +272 -0
- forge/install/settings_merge.py +831 -0
- forge/install/tracking.py +238 -0
- forge/install/version.py +141 -0
- forge/proxy/__init__.py +0 -0
- forge/proxy/base_client.py +181 -0
- forge/proxy/client_adapter.py +476 -0
- forge/proxy/client_factory.py +531 -0
- forge/proxy/converters.py +1206 -0
- forge/proxy/cost_logger.py +132 -0
- forge/proxy/cost_tracker.py +242 -0
- forge/proxy/data_models.py +338 -0
- forge/proxy/error_hints.py +92 -0
- forge/proxy/metrics.py +222 -0
- forge/proxy/model_spec.py +158 -0
- forge/proxy/proxies.py +333 -0
- forge/proxy/proxy_identity.py +134 -0
- forge/proxy/proxy_orchestrator.py +1018 -0
- forge/proxy/proxy_startup.py +54 -0
- forge/proxy/server.py +1561 -0
- forge/proxy/utils.py +537 -0
- forge/review/__init__.py +6 -0
- forge/review/adversarial.py +111 -0
- forge/review/consensus.py +236 -0
- forge/review/engine.py +356 -0
- forge/review/models.py +437 -0
- forge/review/resources/__init__.py +5 -0
- forge/review/resources/codereview-performance.md +85 -0
- forge/review/resources/codereview-quick.md +75 -0
- forge/review/resources/codereview-security.md +92 -0
- forge/review/resources/codereview.md +85 -0
- forge/review/resources/docreview-quick.md +75 -0
- forge/review/resources/docreview.md +86 -0
- forge/review/resources/thinkdeep.md +89 -0
- forge/review/routing.py +368 -0
- forge/review/synthesis.py +73 -0
- forge/runtime_config.py +438 -0
- forge/search/__init__.py +55 -0
- forge/search/bm25_store.py +264 -0
- forge/search/content_store.py +197 -0
- forge/search/engine.py +352 -0
- forge/search/exceptions.py +51 -0
- forge/search/extractor.py +234 -0
- forge/search/index_state.py +295 -0
- forge/search/store.py +215 -0
- forge/search/tokenizer.py +24 -0
- forge/session/__init__.py +130 -0
- forge/session/active.py +339 -0
- forge/session/artifacts.py +202 -0
- forge/session/claude/__init__.py +50 -0
- forge/session/claude/cleanup.py +105 -0
- forge/session/claude/invoke.py +236 -0
- forge/session/claude/paths.py +200 -0
- forge/session/cleanup.py +216 -0
- forge/session/config.py +34 -0
- forge/session/direct_model.py +107 -0
- forge/session/effective.py +169 -0
- forge/session/exceptions.py +255 -0
- forge/session/handoff.py +881 -0
- forge/session/handoff_agent.py +544 -0
- forge/session/hooks/__init__.py +35 -0
- forge/session/hooks/models.py +73 -0
- forge/session/hooks/session_start.py +507 -0
- forge/session/identity.py +84 -0
- forge/session/index.py +553 -0
- forge/session/manager.py +1506 -0
- forge/session/models.py +572 -0
- forge/session/overrides.py +344 -0
- forge/session/plan_resolution.py +286 -0
- forge/session/prev_sessions.py +128 -0
- forge/session/store.py +431 -0
- forge/session/validation.py +47 -0
- forge/session/worktree/__init__.py +65 -0
- forge/session/worktree/cleanup.py +262 -0
- forge/session/worktree/config_copy.py +203 -0
- forge/session/worktree/create.py +332 -0
- forge/sidecar/__init__.py +29 -0
- forge/sidecar/container.py +161 -0
- forge/sidecar/docker.py +86 -0
- forge/sidecar/secrets.py +19 -0
- multi_forge-0.2.0.dist-info/METADATA +242 -0
- multi_forge-0.2.0.dist-info/RECORD +311 -0
- multi_forge-0.2.0.dist-info/WHEEL +4 -0
- multi_forge-0.2.0.dist-info/entry_points.txt +2 -0
- multi_forge-0.2.0.dist-info/licenses/LICENSE +203 -0
- 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
|