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