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