@event4u/agent-config 1.14.0 → 1.16.0
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.
- package/.agent-src/commands/agent-handoff.md +1 -1
- package/.agent-src/commands/bug-fix.md +3 -3
- package/.agent-src/commands/bug-investigate.md +2 -2
- package/.agent-src/commands/chat-history-checkpoint.md +3 -3
- package/.agent-src/commands/chat-history-clear.md +2 -2
- package/.agent-src/commands/chat-history-resume.md +2 -2
- package/.agent-src/commands/chat-history.md +3 -3
- package/.agent-src/commands/check-current-md.md +44 -33
- package/.agent-src/commands/commit-in-chunks.md +43 -23
- package/.agent-src/commands/compress.md +34 -2
- package/.agent-src/commands/council-design.md +96 -0
- package/.agent-src/commands/council-optimize.md +115 -0
- package/.agent-src/commands/council-pr.md +123 -0
- package/.agent-src/commands/council.md +219 -0
- package/.agent-src/commands/create-pr.md +23 -0
- package/.agent-src/commands/do-and-judge.md +3 -3
- package/.agent-src/commands/do-in-steps.md +4 -4
- package/.agent-src/commands/e2e-heal.md +1 -1
- package/.agent-src/commands/e2e-plan.md +1 -1
- package/.agent-src/commands/feature-dev.md +8 -0
- package/.agent-src/commands/feature-explore.md +6 -1
- package/.agent-src/commands/feature-plan.md +33 -2
- package/.agent-src/commands/feature-refactor.md +5 -0
- package/.agent-src/commands/feature-roadmap.md +8 -3
- package/.agent-src/commands/feature.md +58 -0
- package/.agent-src/commands/fix-ci.md +5 -0
- package/.agent-src/commands/fix-portability.md +7 -2
- package/.agent-src/commands/fix-pr-bot-comments.md +5 -0
- package/.agent-src/commands/fix-pr-comments.md +5 -0
- package/.agent-src/commands/fix-pr-developer-comments.md +5 -0
- package/.agent-src/commands/fix-references.md +5 -0
- package/.agent-src/commands/fix-seeder.md +5 -0
- package/.agent-src/commands/fix.md +60 -0
- package/.agent-src/commands/jira-ticket.md +1 -1
- package/.agent-src/commands/judge.md +1 -1
- package/.agent-src/commands/memory-add.md +3 -3
- package/.agent-src/commands/memory-full.md +2 -2
- package/.agent-src/commands/memory-promote.md +2 -2
- package/.agent-src/commands/mode.md +5 -5
- package/.agent-src/commands/onboard.md +17 -8
- package/.agent-src/commands/optimize-agents.md +6 -1
- package/.agent-src/commands/optimize-augmentignore.md +14 -0
- package/.agent-src/commands/optimize-rtk-filters.md +5 -0
- package/.agent-src/commands/optimize-skills.md +6 -1
- package/.agent-src/commands/optimize.md +54 -0
- package/.agent-src/commands/propose-memory.md +2 -2
- package/.agent-src/commands/refine-ticket.md +9 -7
- package/.agent-src/commands/review-changes.md +61 -9
- package/.agent-src/commands/review-routing.md +1 -1
- package/.agent-src/commands/roadmap-create.md +42 -4
- package/.agent-src/commands/roadmap-execute.md +9 -7
- package/.agent-src/commands/set-cost-profile.md +11 -3
- package/.agent-src/commands/sync-agent-settings.md +11 -2
- package/.agent-src/commands/tests-create.md +1 -1
- package/.agent-src/commands/tests-execute.md +2 -3
- package/.agent-src/commands/upstream-contribute.md +1 -1
- package/.agent-src/contexts/authority/commit-mechanics.md +57 -0
- package/.agent-src/contexts/authority/destructive-mechanics.md +66 -0
- package/.agent-src/contexts/authority/scope-mechanics.md +87 -0
- package/.agent-src/contexts/execution/autonomy-detection.md +54 -0
- package/.agent-src/contexts/execution/autonomy-examples.md +90 -0
- package/.agent-src/contexts/execution/autonomy-mechanics.md +29 -0
- package/.agent-src/contexts/execution/verification-mechanics.md +80 -0
- package/.agent-src/personas/README.md +1 -1
- package/.agent-src/rules/agent-authority.md +24 -0
- package/.agent-src/rules/architecture.md +1 -1
- package/.agent-src/rules/artifact-drafting-protocol.md +1 -1
- package/.agent-src/rules/artifact-engagement-recording.md +2 -2
- package/.agent-src/rules/ask-when-uncertain.md +1 -1
- package/.agent-src/rules/augment-portability.md +56 -37
- package/.agent-src/rules/autonomous-execution.md +78 -114
- package/.agent-src/rules/capture-learnings.md +1 -1
- package/.agent-src/rules/chat-history-cadence.md +109 -0
- package/.agent-src/rules/chat-history-ownership.md +123 -0
- package/.agent-src/rules/chat-history-visibility.md +96 -0
- package/.agent-src/rules/cli-output-handling.md +1 -1
- package/.agent-src/rules/{command-suggestion.md → command-suggestion-policy.md} +10 -9
- package/.agent-src/rules/commit-conventions.md +1 -1
- package/.agent-src/rules/commit-policy.md +43 -61
- package/.agent-src/rules/context-hygiene.md +3 -3
- package/.agent-src/rules/direct-answers.md +2 -2
- package/.agent-src/rules/docs-sync.md +1 -1
- package/.agent-src/rules/e2e-testing.md +1 -1
- package/.agent-src/rules/guidelines.md +4 -4
- package/.agent-src/rules/improve-before-implement.md +2 -2
- package/.agent-src/rules/language-and-tone.md +41 -96
- package/.agent-src/rules/minimal-safe-diff.md +3 -3
- package/.agent-src/rules/model-recommendation.md +4 -4
- package/.agent-src/rules/no-cheap-questions.md +89 -0
- package/.agent-src/rules/non-destructive-by-default.md +25 -59
- package/.agent-src/rules/onboarding-gate.md +5 -5
- package/.agent-src/rules/review-routing-awareness.md +9 -9
- package/.agent-src/rules/roadmap-progress-sync.md +132 -80
- package/.agent-src/rules/role-mode-adherence.md +3 -3
- package/.agent-src/rules/scope-control.md +65 -46
- package/.agent-src/rules/security-sensitive-stop.md +2 -2
- package/.agent-src/rules/size-enforcement.md +3 -2
- package/.agent-src/rules/think-before-action.md +5 -5
- package/.agent-src/rules/token-efficiency.md +4 -4
- package/.agent-src/rules/{ui-audit-before-build.md → ui-audit-gate.md} +3 -3
- package/.agent-src/rules/user-interaction.md +31 -7
- package/.agent-src/rules/verify-before-complete.md +12 -67
- package/.agent-src/scripts/update_roadmap_progress.py +65 -8
- package/.agent-src/skills/ai-council/SKILL.md +333 -0
- package/.agent-src/skills/api-endpoint/SKILL.md +2 -2
- package/.agent-src/skills/blade-ui/SKILL.md +30 -11
- package/.agent-src/skills/blast-radius-analyzer/SKILL.md +1 -1
- package/.agent-src/skills/bug-analyzer/SKILL.md +1 -1
- package/.agent-src/skills/command-routing/SKILL.md +1 -1
- package/.agent-src/skills/command-writing/SKILL.md +16 -5
- package/.agent-src/skills/conventional-commits-writing/SKILL.md +1 -1
- package/.agent-src/skills/copilot-agents-optimization/SKILL.md +2 -2
- package/.agent-src/skills/developer-like-execution/SKILL.md +2 -2
- package/.agent-src/skills/existing-ui-audit/SKILL.md +24 -9
- package/.agent-src/skills/fe-design/SKILL.md +20 -15
- package/.agent-src/skills/file-editor/SKILL.md +9 -0
- package/.agent-src/skills/flux/SKILL.md +1 -1
- package/.agent-src/skills/git-workflow/SKILL.md +1 -1
- package/.agent-src/skills/guideline-writing/SKILL.md +11 -11
- package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +4 -4
- package/.agent-src/skills/livewire/SKILL.md +27 -8
- package/.agent-src/skills/override-management/SKILL.md +2 -2
- package/.agent-src/skills/php-coder/SKILL.md +1 -1
- package/.agent-src/skills/playwright-testing/SKILL.md +2 -2
- package/.agent-src/skills/readme-reviewer/SKILL.md +1 -1
- package/.agent-src/skills/readme-writing/SKILL.md +1 -1
- package/.agent-src/skills/readme-writing-package/SKILL.md +1 -1
- package/.agent-src/skills/receiving-code-review/SKILL.md +1 -1
- package/.agent-src/skills/refine-ticket/SKILL.md +30 -24
- package/.agent-src/skills/review-routing/SKILL.md +2 -2
- package/.agent-src/skills/roadmap-management/SKILL.md +22 -16
- package/.agent-src/skills/rule-writing/SKILL.md +1 -1
- package/.agent-src/skills/skill-reviewer/SKILL.md +1 -1
- package/.agent-src/skills/skill-writing/SKILL.md +6 -6
- package/.agent-src/skills/subagent-orchestration/SKILL.md +1 -0
- package/.agent-src/skills/systematic-debugging/SKILL.md +1 -1
- package/.agent-src/skills/upstream-contribute/SKILL.md +3 -3
- package/.agent-src/skills/validate-feature-fit/SKILL.md +2 -2
- package/.agent-src/skills/{verify-before-complete → verify-completion-evidence}/SKILL.md +2 -2
- package/.agent-src/templates/agent-settings.md +9 -9
- package/.agent-src/templates/contexts/auth-model.md +1 -1
- package/.agent-src/templates/roadmaps.md +9 -8
- package/.agent-src/templates/scripts/README.md +2 -2
- package/.agent-src/templates/scripts/memory_lookup.py +1 -1
- package/.agent-src/templates/scripts/telemetry/aggregator.py +16 -1
- package/.agent-src/templates/scripts/telemetry/engagement.py +59 -0
- package/.agent-src/templates/scripts/telemetry/report_renderer.py +28 -1
- package/.agent-src/templates/scripts/telemetry_record.py +14 -1
- package/.agent-src/templates/scripts/work_engine/__init__.py +2 -2
- package/.agent-src/templates/scripts/work_engine/cli.py +64 -461
- package/.agent-src/templates/scripts/work_engine/cli_args.py +116 -0
- package/.agent-src/templates/scripts/work_engine/delivery_state.py +3 -3
- package/.agent-src/templates/scripts/work_engine/directives/backend/__init__.py +1 -1
- package/.agent-src/templates/scripts/work_engine/directives/backend/implement.py +1 -1
- package/.agent-src/templates/scripts/work_engine/directives/backend/memory.py +1 -1
- package/.agent-src/templates/scripts/work_engine/directives/backend/plan.py +1 -1
- package/.agent-src/templates/scripts/work_engine/directives/backend/report.py +1 -1
- package/.agent-src/templates/scripts/work_engine/dispatcher.py +1 -1
- package/.agent-src/templates/scripts/work_engine/emitters.py +43 -0
- package/.agent-src/templates/scripts/work_engine/errors.py +19 -0
- package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +76 -0
- package/.agent-src/templates/scripts/work_engine/input_builders.py +163 -0
- package/.agent-src/templates/scripts/work_engine/migration/v0_to_v1.py +34 -2
- package/.agent-src/templates/scripts/work_engine/persona_policy.py +1 -1
- package/.agent-src/templates/scripts/work_engine/resolvers/prompt.py +1 -1
- package/.agent-src/templates/scripts/work_engine/state_io.py +202 -0
- package/.claude-plugin/marketplace.json +10 -2
- package/AGENTS.md +16 -12
- package/CHANGELOG.md +206 -9
- package/README.md +51 -52
- package/config/agent-settings.template.yml +58 -1
- package/config/gitignore-block.txt +3 -0
- package/docs/MIGRATION.md +122 -0
- package/docs/architecture.md +83 -34
- package/docs/catalog.md +331 -0
- package/docs/contracts/STABILITY.md +134 -0
- package/docs/contracts/adr-chat-history-split.md +132 -0
- package/docs/contracts/adr-command-suggestion.md +146 -0
- package/docs/contracts/adr-implement-ticket-runtime.md +122 -0
- package/docs/contracts/adr-product-ui-track.md +384 -0
- package/docs/contracts/adr-prompt-driven-execution.md +187 -0
- package/docs/contracts/agent-memory-contract.md +149 -0
- package/docs/contracts/artifact-engagement-flow.md +262 -0
- package/docs/contracts/command-clusters.md +126 -0
- package/docs/contracts/command-suggestion-flow.md +148 -0
- package/docs/contracts/implement-ticket-flow.md +628 -0
- package/docs/contracts/linear-ai-rules-inclusion.md +143 -0
- package/docs/contracts/linear-ai-three-layers.md +131 -0
- package/docs/contracts/load-context-schema.md +186 -0
- package/docs/contracts/rule-interactions.md +107 -0
- package/docs/contracts/rule-interactions.yml +238 -0
- package/docs/contracts/rule-priority-hierarchy.md +87 -0
- package/docs/contracts/ui-stack-extension.md +236 -0
- package/docs/contracts/ui-track-flow.md +338 -0
- package/docs/customization.md +14 -0
- package/docs/end-to-end-walkthroughs.md +165 -0
- package/docs/getting-started.md +27 -9
- package/docs/github-topics.md +12 -3
- package/docs/guidelines/agent-infra/language-and-tone-examples.md +79 -0
- package/{.agent-src → docs}/guidelines/docs/readme-size-and-splitting.md +26 -25
- package/docs/guidelines/php/git.md +164 -0
- package/docs/installation.md +42 -6
- package/docs/migrations/commands-1.15.0.md +112 -0
- package/docs/showcase.md +9 -4
- package/docs/skills-catalog.md +14 -8
- package/docs/ui-track-mental-model.md +121 -0
- package/llms.txt +13 -7
- package/package.json +1 -1
- package/scripts/agent-config +23 -0
- package/scripts/ai_council/__init__.py +39 -0
- package/scripts/ai_council/_default_prices.py +41 -0
- package/scripts/ai_council/_one_off_rebalancing_audit.py +149 -0
- package/scripts/ai_council/_one_off_roundtrip.py +106 -0
- package/scripts/ai_council/budget_guard.py +172 -0
- package/scripts/ai_council/bundler.py +261 -0
- package/scripts/ai_council/clients.py +381 -0
- package/scripts/ai_council/modes.py +127 -0
- package/scripts/ai_council/orchestrator.py +350 -0
- package/scripts/ai_council/pricing.py +213 -0
- package/scripts/ai_council/project_context.py +159 -0
- package/scripts/ai_council/prompts.py +232 -0
- package/scripts/ai_council/session.py +144 -0
- package/scripts/build_linear_digest.py +4 -4
- package/scripts/check_always_budget.py +126 -0
- package/scripts/check_augmentignore.py +69 -0
- package/scripts/check_command_count_messaging.py +120 -0
- package/scripts/check_portability.py +57 -0
- package/scripts/check_public_catalog_links.py +122 -0
- package/scripts/check_public_links.py +185 -0
- package/scripts/check_references.py +5 -1
- package/scripts/check_roadmap_trackable.py +111 -0
- package/scripts/command_suggester/cooldown.py +1 -1
- package/scripts/generate_index.py +266 -0
- package/scripts/install_anthropic_key.sh +5 -0
- package/scripts/install_openai_key.sh +106 -0
- package/scripts/lint_load_context.py +163 -0
- package/scripts/lint_no_new_atomic_commands.py +179 -0
- package/scripts/lint_rule_interactions.py +149 -0
- package/scripts/memory_lookup.py +1 -1
- package/scripts/release.py +297 -64
- package/scripts/schemas/command.schema.json +20 -0
- package/scripts/schemas/rule.schema.json +10 -0
- package/scripts/skill_linter.py +26 -4
- package/scripts/sync_agent_settings.py +1 -1
- package/scripts/update_counts.py +19 -4
- package/scripts/update_prices.py +124 -0
- package/.agent-src/guidelines/php/git.md +0 -96
- package/.agent-src/rules/chat-history.md +0 -200
- /package/.agent-src/rules/{slash-commands.md → slash-command-routing-policy.md} +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/agent-interaction-and-decision-quality.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/break-glass-usage.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/developer-judgment.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/engineering-memory-data-format.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/layered-settings.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/memory-access.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/naming.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/output-patterns.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/review-routing-data-format.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/role-contracts.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/role-mode-router.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/runtime-layer.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/self-improvement-pipeline.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/size-and-scope.md +0 -0
- /package/{.agent-src → docs}/guidelines/agent-infra/tool-integration.md +0 -0
- /package/{.agent-src → docs}/guidelines/e2e/playwright.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/api-design.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/artisan-commands.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/blade-ui.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/controllers.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/database.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/eloquent.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/flux.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/general.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/jobs.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/livewire.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/logging.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/naming.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns/dependency-injection.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns/dtos.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns/events.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns/factory.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns/pipelines.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns/policies.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns/repositories.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns/service-layer.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns/strategy.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/patterns.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/performance.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/resources.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/security.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/sql.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/validations.md +0 -0
- /package/{.agent-src → docs}/guidelines/php/websocket.md +0 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""Council orchestrator — fan out one question to multiple members.
|
|
2
|
+
|
|
3
|
+
v2 contract (sequential + interactive overrun prompt):
|
|
4
|
+
|
|
5
|
+
- Members are called **sequentially** in input order. The previous
|
|
6
|
+
parallel ThreadPoolExecutor was traded for predictable mid-flow
|
|
7
|
+
user prompts; with 2-3 council members the latency cost is small.
|
|
8
|
+
- `estimate(question, members, table)` returns a pre-call cost preview
|
|
9
|
+
(input tokens + max-output ceiling + USD per member). The host
|
|
10
|
+
agent shows this before invoking `consult()`.
|
|
11
|
+
- `consult(..., on_overrun=...)` invokes the callback BEFORE each
|
|
12
|
+
member's actual API call when the projected total cost would push
|
|
13
|
+
past the cost budget. The callback decides whether to proceed for
|
|
14
|
+
this single member; the next member triggers the callback again.
|
|
15
|
+
|
|
16
|
+
Failure normalisation (one member's exception → `error`-set
|
|
17
|
+
CouncilResponse, never raise) is unchanged.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import Callable
|
|
24
|
+
|
|
25
|
+
from scripts.ai_council.budget_guard import (
|
|
26
|
+
record_spend as _record_daily_spend,
|
|
27
|
+
today_spend_usd as _today_spend_usd,
|
|
28
|
+
would_exceed as _would_exceed_daily,
|
|
29
|
+
)
|
|
30
|
+
from scripts.ai_council.clients import CouncilResponse, ExternalAIClient
|
|
31
|
+
from scripts.ai_council.pricing import (
|
|
32
|
+
CostEstimate,
|
|
33
|
+
PriceTable,
|
|
34
|
+
estimate_cost,
|
|
35
|
+
estimate_input_tokens,
|
|
36
|
+
)
|
|
37
|
+
from scripts.ai_council.project_context import ProjectContext
|
|
38
|
+
from scripts.ai_council.prompts import system_prompt_for
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class CostBudget:
|
|
43
|
+
max_input_tokens: int = 50_000
|
|
44
|
+
max_output_tokens: int = 20_000
|
|
45
|
+
max_calls: int = 10
|
|
46
|
+
max_total_usd: float = 0.0 # 0 = USD ceiling disabled (token caps still apply)
|
|
47
|
+
daily_limit_usd: float = 0.0 # 0 = rolling 24h cap disabled (D3)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class CouncilQuestion:
|
|
52
|
+
mode: str # one of: prompt, roadmap, diff, files
|
|
53
|
+
user_prompt: str # bundled artefact text
|
|
54
|
+
max_tokens: int = 1024
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class OverrunEvent:
|
|
59
|
+
"""Passed to `on_overrun` when projected spend exceeds the budget."""
|
|
60
|
+
|
|
61
|
+
member_index: int
|
|
62
|
+
member: ExternalAIClient
|
|
63
|
+
next_estimate: CostEstimate # this member's projected cost
|
|
64
|
+
spent_input_tokens: int # already-billed totals BEFORE this member
|
|
65
|
+
spent_output_tokens: int
|
|
66
|
+
spent_usd: float
|
|
67
|
+
projected_total_usd: float # spent_usd + next_estimate.total_usd
|
|
68
|
+
daily_spent_usd: float = 0.0 # rolling 24h spend BEFORE this member (D3)
|
|
69
|
+
daily_limit_usd: float = 0.0 # the configured daily cap (0 = disabled)
|
|
70
|
+
breach_kind: str = "session" # "session" | "daily" | "tokens"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Callback signature: receive event → return True (proceed) or False (skip + tag error).
|
|
74
|
+
OnOverrunCallback = Callable[[OverrunEvent], bool]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def estimate(
|
|
78
|
+
question: CouncilQuestion,
|
|
79
|
+
members: list[ExternalAIClient],
|
|
80
|
+
table: PriceTable,
|
|
81
|
+
*,
|
|
82
|
+
project: ProjectContext | None = None,
|
|
83
|
+
original_ask: str = "",
|
|
84
|
+
) -> list[CostEstimate]:
|
|
85
|
+
"""Return a pre-call cost estimate per member, in input order.
|
|
86
|
+
|
|
87
|
+
`project` and `original_ask` are passed through to
|
|
88
|
+
`system_prompt_for()` so the estimate covers the handoff preamble
|
|
89
|
+
bytes too. Both default to v1-shape (no preamble extension).
|
|
90
|
+
"""
|
|
91
|
+
sys_prompt = system_prompt_for(
|
|
92
|
+
question.mode, project=project, original_ask=original_ask,
|
|
93
|
+
)
|
|
94
|
+
input_tokens = estimate_input_tokens(question.user_prompt) + estimate_input_tokens(sys_prompt)
|
|
95
|
+
return [
|
|
96
|
+
estimate_cost(m.name, m.model, input_tokens, question.max_tokens, table)
|
|
97
|
+
for m in members
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def consult(
|
|
102
|
+
members: list[ExternalAIClient],
|
|
103
|
+
question: CouncilQuestion,
|
|
104
|
+
budget: CostBudget | None = None,
|
|
105
|
+
*,
|
|
106
|
+
table: PriceTable | None = None,
|
|
107
|
+
on_overrun: OnOverrunCallback | None = None,
|
|
108
|
+
project: ProjectContext | None = None,
|
|
109
|
+
original_ask: str = "",
|
|
110
|
+
rounds: int = 1,
|
|
111
|
+
on_round_complete: Callable[[int, list[CouncilResponse]], None] | None = None,
|
|
112
|
+
) -> list[CouncilResponse]:
|
|
113
|
+
"""Sequentially fan out `question` to every enabled member.
|
|
114
|
+
|
|
115
|
+
- If `table` is provided, USD spend is tracked against
|
|
116
|
+
`budget.max_total_usd` (when > 0). Without `table`, only the
|
|
117
|
+
token caps apply (back-compat with v1 callers).
|
|
118
|
+
- When the projected next-member spend would breach any cap,
|
|
119
|
+
`on_overrun` is consulted. Returning False marks that member as
|
|
120
|
+
`cost_budget_exceeded`; True proceeds with the call.
|
|
121
|
+
- Without `on_overrun`, breaching caps short-circuits remaining
|
|
122
|
+
members with `cost_budget_exceeded` (v1 behaviour preserved).
|
|
123
|
+
- `project` + `original_ask` flow into `handoff_preamble()` so the
|
|
124
|
+
council member receives a neutral context-handoff alongside the
|
|
125
|
+
artefact. Both default to v1 shape (no preamble extension).
|
|
126
|
+
- `rounds >= 2` enables multi-round debate (D1). Each subsequent
|
|
127
|
+
round augments the user prompt with anonymised prior-round
|
|
128
|
+
responses (provider/model identity stripped). Token + USD caps
|
|
129
|
+
accumulate across rounds. Returns the FINAL round's responses;
|
|
130
|
+
use `on_round_complete(round_idx, responses)` to capture
|
|
131
|
+
intermediate rounds.
|
|
132
|
+
"""
|
|
133
|
+
if rounds < 1:
|
|
134
|
+
raise ValueError(f"rounds must be >= 1 (got {rounds})")
|
|
135
|
+
if not members:
|
|
136
|
+
return []
|
|
137
|
+
budget = budget or CostBudget()
|
|
138
|
+
if len(members) > budget.max_calls:
|
|
139
|
+
raise ValueError(
|
|
140
|
+
f"Council has {len(members)} members but budget caps at "
|
|
141
|
+
f"{budget.max_calls} calls."
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
spent: dict[str, float] = {"input": 0, "output": 0, "usd": 0.0}
|
|
145
|
+
last_results: list[CouncilResponse] = []
|
|
146
|
+
current_user_prompt = question.user_prompt
|
|
147
|
+
|
|
148
|
+
for round_idx in range(rounds):
|
|
149
|
+
round_question = (
|
|
150
|
+
question if round_idx == 0
|
|
151
|
+
else CouncilQuestion(
|
|
152
|
+
mode=question.mode,
|
|
153
|
+
user_prompt=current_user_prompt,
|
|
154
|
+
max_tokens=question.max_tokens,
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
last_results = _run_round(
|
|
158
|
+
members, round_question, budget, spent,
|
|
159
|
+
table=table, on_overrun=on_overrun,
|
|
160
|
+
project=project, original_ask=original_ask,
|
|
161
|
+
)
|
|
162
|
+
if on_round_complete is not None:
|
|
163
|
+
on_round_complete(round_idx, last_results)
|
|
164
|
+
if round_idx + 1 < rounds:
|
|
165
|
+
current_user_prompt = _augment_for_next_round(
|
|
166
|
+
question.user_prompt, last_results, round_idx + 2,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return last_results
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _run_round(
|
|
173
|
+
members: list[ExternalAIClient],
|
|
174
|
+
question: CouncilQuestion,
|
|
175
|
+
budget: CostBudget,
|
|
176
|
+
spent: dict[str, float],
|
|
177
|
+
*,
|
|
178
|
+
table: PriceTable | None,
|
|
179
|
+
on_overrun: OnOverrunCallback | None,
|
|
180
|
+
project: ProjectContext | None,
|
|
181
|
+
original_ask: str,
|
|
182
|
+
) -> list[CouncilResponse]:
|
|
183
|
+
"""Run a single round; mutate `spent` with cumulative totals."""
|
|
184
|
+
system_prompt = system_prompt_for(
|
|
185
|
+
question.mode, project=project, original_ask=original_ask,
|
|
186
|
+
)
|
|
187
|
+
results: list[CouncilResponse] = []
|
|
188
|
+
estimates = (
|
|
189
|
+
estimate(question, members, table, project=project, original_ask=original_ask)
|
|
190
|
+
if table is not None
|
|
191
|
+
else None
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
for idx, member in enumerate(members):
|
|
195
|
+
# ── non-billable members skip the cost gate entirely ─────────
|
|
196
|
+
# ManualClient (and future PlaywrightClient) cost us $0; their
|
|
197
|
+
# token counts are still tracked from the response below for
|
|
198
|
+
# observability, but no projection / budget breach can apply.
|
|
199
|
+
if not getattr(member, "billable", True):
|
|
200
|
+
try:
|
|
201
|
+
response = member.ask(system_prompt, question.user_prompt, question.max_tokens)
|
|
202
|
+
except Exception as exc: # noqa: BLE001 - last-resort safety net
|
|
203
|
+
response = CouncilResponse(
|
|
204
|
+
provider=member.name, model=member.model, text="",
|
|
205
|
+
error=f"{type(exc).__name__}: {exc}",
|
|
206
|
+
)
|
|
207
|
+
results.append(response)
|
|
208
|
+
spent["input"] += response.input_tokens
|
|
209
|
+
spent["output"] += response.output_tokens
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
# ── projected spend check ────────────────────────────────────
|
|
213
|
+
proj_input = spent["input"] + (estimates[idx].input_tokens if estimates else 0)
|
|
214
|
+
proj_output = spent["output"] + (estimates[idx].output_tokens if estimates else 0)
|
|
215
|
+
proj_usd = spent["usd"] + (estimates[idx].total_usd if estimates else 0.0)
|
|
216
|
+
next_call_usd = estimates[idx].total_usd if estimates else 0.0
|
|
217
|
+
|
|
218
|
+
breaches_tokens = (
|
|
219
|
+
proj_input > budget.max_input_tokens
|
|
220
|
+
or proj_output > budget.max_output_tokens
|
|
221
|
+
)
|
|
222
|
+
breaches_usd = budget.max_total_usd > 0 and proj_usd > budget.max_total_usd
|
|
223
|
+
breaches_daily = (
|
|
224
|
+
budget.daily_limit_usd > 0
|
|
225
|
+
and _would_exceed_daily(budget.daily_limit_usd, next_call_usd)
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if breaches_tokens or breaches_usd or breaches_daily:
|
|
229
|
+
breach_kind = (
|
|
230
|
+
"tokens" if breaches_tokens
|
|
231
|
+
else "daily" if breaches_daily
|
|
232
|
+
else "session"
|
|
233
|
+
)
|
|
234
|
+
error_tag = (
|
|
235
|
+
"daily_budget_exceeded" if breach_kind == "daily"
|
|
236
|
+
else "cost_budget_exceeded"
|
|
237
|
+
)
|
|
238
|
+
if on_overrun is not None and estimates is not None:
|
|
239
|
+
event = OverrunEvent(
|
|
240
|
+
member_index=idx,
|
|
241
|
+
member=member,
|
|
242
|
+
next_estimate=estimates[idx],
|
|
243
|
+
spent_input_tokens=int(spent["input"]),
|
|
244
|
+
spent_output_tokens=int(spent["output"]),
|
|
245
|
+
spent_usd=spent["usd"],
|
|
246
|
+
projected_total_usd=proj_usd,
|
|
247
|
+
daily_spent_usd=(
|
|
248
|
+
_today_spend_usd() if budget.daily_limit_usd > 0 else 0.0
|
|
249
|
+
),
|
|
250
|
+
daily_limit_usd=budget.daily_limit_usd,
|
|
251
|
+
breach_kind=breach_kind,
|
|
252
|
+
)
|
|
253
|
+
if not on_overrun(event):
|
|
254
|
+
results.append(_aborted(member, error_tag))
|
|
255
|
+
continue
|
|
256
|
+
else:
|
|
257
|
+
# v1 behaviour: short-circuit all remaining members.
|
|
258
|
+
for left in members[idx:]:
|
|
259
|
+
results.append(_aborted(left, error_tag))
|
|
260
|
+
return results
|
|
261
|
+
|
|
262
|
+
# ── actual call ──────────────────────────────────────────────
|
|
263
|
+
try:
|
|
264
|
+
response = member.ask(system_prompt, question.user_prompt, question.max_tokens)
|
|
265
|
+
except Exception as exc: # noqa: BLE001 - last-resort safety net
|
|
266
|
+
response = CouncilResponse(
|
|
267
|
+
provider=member.name, model=member.model, text="",
|
|
268
|
+
error=f"{type(exc).__name__}: {exc}",
|
|
269
|
+
)
|
|
270
|
+
results.append(response)
|
|
271
|
+
spent["input"] += response.input_tokens
|
|
272
|
+
spent["output"] += response.output_tokens
|
|
273
|
+
if estimates is not None and table is not None:
|
|
274
|
+
# Bill the actual output against the budget using the
|
|
275
|
+
# member's per-1M output rate. Re-use estimate_cost with
|
|
276
|
+
# the *real* token count.
|
|
277
|
+
actual = estimate_cost(
|
|
278
|
+
member.name, member.model,
|
|
279
|
+
response.input_tokens, response.output_tokens, table,
|
|
280
|
+
)
|
|
281
|
+
spent["usd"] += actual.total_usd
|
|
282
|
+
# Persist to the rolling 24h ledger when the daily cap is
|
|
283
|
+
# active. Errors are swallowed inside record_spend.
|
|
284
|
+
if budget.daily_limit_usd > 0 and not response.error:
|
|
285
|
+
_record_daily_spend(
|
|
286
|
+
actual.total_usd, member.name, member.model,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
return results
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _aborted(member: ExternalAIClient, reason: str) -> CouncilResponse:
|
|
293
|
+
return CouncilResponse(
|
|
294
|
+
provider=member.name, model=member.model, text="", error=reason,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _augment_for_next_round(
|
|
299
|
+
original_prompt: str,
|
|
300
|
+
prior_responses: list[CouncilResponse],
|
|
301
|
+
next_round_number: int,
|
|
302
|
+
) -> str:
|
|
303
|
+
"""Build the round-N user prompt: original artefact + anonymised prior round.
|
|
304
|
+
|
|
305
|
+
Provider/model identifiers are stripped (Iron Law of Neutrality §
|
|
306
|
+
multi-round). Reviewers are labelled "Reviewer A / B / C…" in the
|
|
307
|
+
order they appeared. Errors are skipped — they reveal nothing
|
|
308
|
+
useful and can leak provider error formats.
|
|
309
|
+
"""
|
|
310
|
+
blocks: list[str] = []
|
|
311
|
+
label_idx = 0
|
|
312
|
+
for r in prior_responses:
|
|
313
|
+
if r.error or not r.text.strip():
|
|
314
|
+
continue
|
|
315
|
+
label = chr(ord("A") + label_idx)
|
|
316
|
+
label_idx += 1
|
|
317
|
+
blocks.append(f"### Reviewer {label}\n\n{r.text.strip()}")
|
|
318
|
+
if not blocks:
|
|
319
|
+
return original_prompt
|
|
320
|
+
prior_block = "\n\n".join(blocks)
|
|
321
|
+
return (
|
|
322
|
+
f"{original_prompt}\n\n"
|
|
323
|
+
f"---\n\n"
|
|
324
|
+
f"## Prior round critiques (round {next_round_number - 1})\n\n"
|
|
325
|
+
f"You are now in round {next_round_number}. Below are anonymised\n"
|
|
326
|
+
f"critiques from independent reviewers in the previous round.\n"
|
|
327
|
+
f"You do NOT know which model produced which critique. Read them,\n"
|
|
328
|
+
f"then respond with:\n\n"
|
|
329
|
+
f"1. Which prior points you agree with (cite reviewer label).\n"
|
|
330
|
+
f"2. Which you disagree with and why.\n"
|
|
331
|
+
f"3. New points or refinements not raised in round 1.\n\n"
|
|
332
|
+
f"{prior_block}"
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def render(responses: list[CouncilResponse]) -> str:
|
|
337
|
+
"""Render stacked sections + a Convergence/Divergence summary slot."""
|
|
338
|
+
blocks: list[str] = []
|
|
339
|
+
for r in responses:
|
|
340
|
+
header = f"## {r.provider} · {r.model}"
|
|
341
|
+
if r.error:
|
|
342
|
+
blocks.append(f"{header}\n\n*ERROR:* `{r.error}`")
|
|
343
|
+
continue
|
|
344
|
+
meta = (
|
|
345
|
+
f"*tokens: {r.input_tokens} in / {r.output_tokens} out · "
|
|
346
|
+
f"{r.latency_ms} ms*"
|
|
347
|
+
)
|
|
348
|
+
blocks.append(f"{header}\n\n{meta}\n\n{r.text}")
|
|
349
|
+
blocks.append("## Convergence / Divergence\n\n*to be summarised by the host agent*")
|
|
350
|
+
return "\n\n---\n\n".join(blocks)
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Runtime pricing layer for the AI Council.
|
|
2
|
+
|
|
3
|
+
Reads `.agent-prices.md` from the repo root, parses YAML frontmatter
|
|
4
|
+
and the Markdown table, and exposes:
|
|
5
|
+
|
|
6
|
+
- `load_prices()` — parse `.agent-prices.md` (bootstraps if missing)
|
|
7
|
+
- `estimate_input_tokens()` — chars / 4 heuristic
|
|
8
|
+
- `estimate_cost()` — input + output USD for a single member
|
|
9
|
+
- `is_stale()` — True if `last_updated` is older than the
|
|
10
|
+
most recent UTC Monday 00:00
|
|
11
|
+
- `bootstrap_from_defaults()` — write a fresh `.agent-prices.md` from
|
|
12
|
+
`_default_prices.DEFAULT_PRICES`
|
|
13
|
+
|
|
14
|
+
The orchestrator never reads `_default_prices` directly. It always
|
|
15
|
+
goes through `load_prices()` so user edits to `.agent-prices.md` win.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import datetime as _dt
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from scripts.ai_council._default_prices import DEFAULT_PRICES, LAST_UPDATED, as_rows
|
|
25
|
+
|
|
26
|
+
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
27
|
+
PRICES_FILE = REPO_ROOT / ".agent-prices.md"
|
|
28
|
+
|
|
29
|
+
# Heuristic: 1 token ≈ 4 characters of English text. OpenAI's tiktoken
|
|
30
|
+
# is more accurate but pulls in a heavy dep we explicitly avoid.
|
|
31
|
+
_CHARS_PER_TOKEN = 4
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Price:
|
|
36
|
+
provider: str
|
|
37
|
+
model: str
|
|
38
|
+
input_per_1m_usd: float
|
|
39
|
+
output_per_1m_usd: float
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class PriceTable:
|
|
44
|
+
last_updated: str # YYYY-MM-DD
|
|
45
|
+
currency: str
|
|
46
|
+
unit: str # "per_1M_tokens"
|
|
47
|
+
source: str
|
|
48
|
+
prices: dict[tuple[str, str], Price]
|
|
49
|
+
|
|
50
|
+
def lookup(self, provider: str, model: str) -> Price | None:
|
|
51
|
+
return self.prices.get((provider, model))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class CostEstimate:
|
|
56
|
+
provider: str
|
|
57
|
+
model: str
|
|
58
|
+
input_tokens: int
|
|
59
|
+
output_tokens: int # max_tokens budget — worst-case ceiling
|
|
60
|
+
input_usd: float
|
|
61
|
+
output_usd: float
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def total_usd(self) -> float:
|
|
65
|
+
return self.input_usd + self.output_usd
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ── token + cost arithmetic ────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def estimate_input_tokens(text: str) -> int:
|
|
72
|
+
"""chars / 4 heuristic. Always returns ≥ 1 for non-empty strings."""
|
|
73
|
+
if not text:
|
|
74
|
+
return 0
|
|
75
|
+
return max(1, len(text) // _CHARS_PER_TOKEN)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def estimate_cost(
|
|
79
|
+
provider: str,
|
|
80
|
+
model: str,
|
|
81
|
+
input_tokens: int,
|
|
82
|
+
max_output_tokens: int,
|
|
83
|
+
table: PriceTable,
|
|
84
|
+
) -> CostEstimate:
|
|
85
|
+
price = table.lookup(provider, model)
|
|
86
|
+
if price is None:
|
|
87
|
+
# Unknown model — return zero-cost estimate; caller decides what
|
|
88
|
+
# to do (warn user, skip, ...). Never silently invent a price.
|
|
89
|
+
return CostEstimate(provider, model, input_tokens, max_output_tokens, 0.0, 0.0)
|
|
90
|
+
input_usd = (input_tokens / 1_000_000) * price.input_per_1m_usd
|
|
91
|
+
output_usd = (max_output_tokens / 1_000_000) * price.output_per_1m_usd
|
|
92
|
+
return CostEstimate(provider, model, input_tokens, max_output_tokens, input_usd, output_usd)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ── staleness ──────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def last_monday_utc(now: _dt.datetime | None = None) -> _dt.date:
|
|
99
|
+
"""Return the most recent Monday 00:00 UTC as a date."""
|
|
100
|
+
now = now or _dt.datetime.now(_dt.timezone.utc)
|
|
101
|
+
weekday = now.weekday() # Mon=0 ... Sun=6
|
|
102
|
+
return (now - _dt.timedelta(days=weekday)).date()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def is_stale(table: PriceTable, now: _dt.datetime | None = None) -> bool:
|
|
106
|
+
"""True if `last_updated` is older than the most recent UTC Monday."""
|
|
107
|
+
try:
|
|
108
|
+
last = _dt.date.fromisoformat(table.last_updated)
|
|
109
|
+
except ValueError:
|
|
110
|
+
return True
|
|
111
|
+
return last < last_monday_utc(now)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ── parser + bootstrap ─────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def load_prices(path: Path = PRICES_FILE) -> PriceTable:
|
|
118
|
+
"""Parse `.agent-prices.md`; bootstrap from defaults if missing."""
|
|
119
|
+
if not path.exists():
|
|
120
|
+
bootstrap_from_defaults(path)
|
|
121
|
+
return _parse(path.read_text(encoding="utf-8"))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def bootstrap_from_defaults(path: Path = PRICES_FILE) -> None:
|
|
125
|
+
"""Write a fresh `.agent-prices.md` from `_default_prices.py`."""
|
|
126
|
+
rows = as_rows()
|
|
127
|
+
body = _render_markdown(LAST_UPDATED, "shipped-default", rows)
|
|
128
|
+
path.write_text(body, encoding="utf-8")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _render_markdown(
|
|
132
|
+
last_updated: str,
|
|
133
|
+
source: str,
|
|
134
|
+
rows: list[tuple[str, str, float, float]],
|
|
135
|
+
) -> str:
|
|
136
|
+
lines = [
|
|
137
|
+
"---",
|
|
138
|
+
f"last_updated: {last_updated}",
|
|
139
|
+
"currency: USD",
|
|
140
|
+
"unit: per_1M_tokens",
|
|
141
|
+
f"source: {source}",
|
|
142
|
+
"---",
|
|
143
|
+
"",
|
|
144
|
+
"# Agent prices",
|
|
145
|
+
"",
|
|
146
|
+
"| provider | model | input | output |",
|
|
147
|
+
"|-----------|---------------------|--------|--------|",
|
|
148
|
+
]
|
|
149
|
+
for provider, model, inp, outp in rows:
|
|
150
|
+
lines.append(f"| {provider:<9} | {model:<19} | {inp:>6.2f} | {outp:>6.2f} |")
|
|
151
|
+
lines.append("")
|
|
152
|
+
return "\n".join(lines)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _parse(text: str) -> PriceTable:
|
|
156
|
+
front, body = _split_frontmatter(text)
|
|
157
|
+
meta = _parse_frontmatter(front)
|
|
158
|
+
prices = _parse_table(body)
|
|
159
|
+
return PriceTable(
|
|
160
|
+
last_updated=meta.get("last_updated", "1970-01-01"),
|
|
161
|
+
currency=meta.get("currency", "USD"),
|
|
162
|
+
unit=meta.get("unit", "per_1M_tokens"),
|
|
163
|
+
source=meta.get("source", "unknown"),
|
|
164
|
+
prices=prices,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _split_frontmatter(text: str) -> tuple[str, str]:
|
|
169
|
+
if not text.startswith("---"):
|
|
170
|
+
return "", text
|
|
171
|
+
parts = text.split("---", 2)
|
|
172
|
+
if len(parts) < 3:
|
|
173
|
+
return "", text
|
|
174
|
+
return parts[1], parts[2]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _parse_frontmatter(front: str) -> dict[str, str]:
|
|
178
|
+
out: dict[str, str] = {}
|
|
179
|
+
for line in front.splitlines():
|
|
180
|
+
line = line.strip()
|
|
181
|
+
if not line or ":" not in line:
|
|
182
|
+
continue
|
|
183
|
+
k, _, v = line.partition(":")
|
|
184
|
+
out[k.strip()] = v.strip()
|
|
185
|
+
return out
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _parse_table(body: str) -> dict[tuple[str, str], Price]:
|
|
189
|
+
out: dict[tuple[str, str], Price] = {}
|
|
190
|
+
for line in body.splitlines():
|
|
191
|
+
line = line.strip()
|
|
192
|
+
if not line.startswith("|") or line.startswith("|--") or line.startswith("|-"):
|
|
193
|
+
continue
|
|
194
|
+
cells = [c.strip() for c in line.strip("|").split("|")]
|
|
195
|
+
if len(cells) != 4:
|
|
196
|
+
continue
|
|
197
|
+
provider, model, inp, outp = cells
|
|
198
|
+
if provider == "provider": # header row
|
|
199
|
+
continue
|
|
200
|
+
try:
|
|
201
|
+
out[(provider, model)] = Price(provider, model, float(inp), float(outp))
|
|
202
|
+
except ValueError:
|
|
203
|
+
continue
|
|
204
|
+
return out
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
__all__ = [
|
|
208
|
+
"Price", "PriceTable", "CostEstimate",
|
|
209
|
+
"PRICES_FILE", "DEFAULT_PRICES",
|
|
210
|
+
"load_prices", "bootstrap_from_defaults",
|
|
211
|
+
"estimate_input_tokens", "estimate_cost",
|
|
212
|
+
"last_monday_utc", "is_stale",
|
|
213
|
+
]
|