codespy-ai 2.0.1__tar.gz → 2.1.0__tar.gz
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.
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/PKG-INFO +2 -2
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/pyproject.toml +2 -2
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/__init__.py +1 -1
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/memory/cerebral/cerebral.py +237 -50
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/memory/cerebral/cost.py +12 -6
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/memory/cerebral/structured_output.py +7 -199
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/memory/prefrontal/format.py +1 -5
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/memory/prefrontal/prefrontal.py +3 -3
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/memory/prefrontal/reach.py +46 -10
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/review/scope/agent.py +15 -29
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/config_memory.py +128 -284
- codespy_ai-2.0.1/src/codespy/agents/memory/cerebral/delta_ops_compat.py +0 -197
- codespy_ai-2.0.1/src/codespy/agents/memory/cerebral/routines.py +0 -230
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/LICENSE +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/README.md +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/context_safe.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/cost_tracker.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/diagnostic_adapter.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/dspy_config.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/memory/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/memory/cerebral/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/memory/cerebral/orphans.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/memory/hippocampus/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/memory/hippocampus/budget.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/memory/hippocampus/cartographer.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/memory/hippocampus/context_memory.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/memory/hippocampus/distiller.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/memory/hippocampus/episode.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/memory/hippocampus/hippocampus.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/memory/pg0_manager.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/memory/postgres.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/memory/prefrontal/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/memory/prefrontal/facets.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/memory/prefrontal/query.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/memory/recall.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/review/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/review/audit/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/review/audit/agent.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/review/code_review/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/review/code_review/agent.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/review/doc/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/review/doc/agent.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/review/doc/doc_extractor.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/review/helpers.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/review/models.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/review/scope/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/review/scope/manifest_parser.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/review/scope/models.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/review/summary/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/review/summary/agent.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/review/supply_chain/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/review/supply_chain/agent.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/agents/rlm_tools.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/cli.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/cli_local.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/cli_mcp_server.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/cli_remote.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/config.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/config_dspy.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/config_git.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/config_io.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/config_llm.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/config_utils.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/cyber/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/cyber/osv/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/cyber/osv/client.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/cyber/osv/models.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/cyber/osv/server.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/git/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/git/base.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/git/client.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/git/github_client.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/git/gitlab_client.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/git/local_diff.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/git/models.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/git/server.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/mcp_utils.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/parsers/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/parsers/ripgrep/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/parsers/ripgrep/client.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/parsers/ripgrep/server.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/parsers/treesitter/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/parsers/treesitter/base_extractor.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/parsers/treesitter/extractors/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/parsers/treesitter/extractors/bash.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/parsers/treesitter/extractors/cpp.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/parsers/treesitter/extractors/csharp.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/parsers/treesitter/extractors/go.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/parsers/treesitter/extractors/java.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/parsers/treesitter/extractors/javascript.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/parsers/treesitter/extractors/kotlin.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/parsers/treesitter/extractors/objc.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/parsers/treesitter/extractors/php.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/parsers/treesitter/extractors/python.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/parsers/treesitter/extractors/ripgrep_fallback.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/parsers/treesitter/extractors/ruby.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/parsers/treesitter/extractors/rust.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/parsers/treesitter/extractors/swift.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/parsers/treesitter/extractors/terraform.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/parsers/treesitter/models.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/parsers/treesitter/parser.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/parsers/treesitter/server.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/storage/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/storage/base.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/storage/filesystem/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/storage/filesystem/client.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/storage/filesystem/server.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/storage/models.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/storage/s3/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/storage/s3/client.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/storage/s3/server.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/web/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/web/client.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/web/models.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/tools/web/server.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/workflows/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/workflows/review/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/workflows/review/models.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/workflows/review/pipeline.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/workflows/review/reporters/__init__.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/workflows/review/reporters/base.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/workflows/review/reporters/git.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/workflows/review/reporters/stdout.py +0 -0
- {codespy_ai-2.0.1 → codespy_ai-2.1.0}/src/codespy/workflows/review/server.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codespy-ai
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Code review agent powered by DSPy
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -23,7 +23,7 @@ Requires-Dist: cloudpickle (>=3.1.2,<4.0.0)
|
|
|
23
23
|
Requires-Dist: ddgs (>=8.0.0)
|
|
24
24
|
Requires-Dist: dspy[mcp] (>=3.3.0,<4.0.0)
|
|
25
25
|
Requires-Dist: gitpython (>=3.1.42)
|
|
26
|
-
Requires-Dist: hindsight-api-slim (>=0.
|
|
26
|
+
Requires-Dist: hindsight-api-slim (>=1.0.0,<2)
|
|
27
27
|
Requires-Dist: httpx (>=0.28.0)
|
|
28
28
|
Requires-Dist: json-repair (>=0.60.1)
|
|
29
29
|
Requires-Dist: litellm (>=1.84.0,<2.0.0)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "codespy-ai"
|
|
3
|
-
version = "2.0
|
|
3
|
+
version = "2.1.0"
|
|
4
4
|
description = "Code review agent powered by DSPy"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -53,7 +53,7 @@ beautifulsoup4 = ">=4.12.0"
|
|
|
53
53
|
markdownify = ">=0.15.0"
|
|
54
54
|
ddgs = ">=8.0.0"
|
|
55
55
|
pg0-embedded = ">=0.15"
|
|
56
|
-
hindsight-api-slim = ">=0.
|
|
56
|
+
hindsight-api-slim = ">=1.0.0,<2"
|
|
57
57
|
|
|
58
58
|
[tool.poetry.group.dev.dependencies]
|
|
59
59
|
pytest = ">=8.0.0"
|
|
@@ -33,7 +33,7 @@ HINDSIGHT_SCHEMA_DEFAULT = "semantic"
|
|
|
33
33
|
# needs sentence-transformers and emits a misleading startup warning
|
|
34
34
|
# (hindsight_api/config.py _validate). That config is built and cached at IMPORT
|
|
35
35
|
# time — the `from hindsight_api import MemoryEngine` below pulls in
|
|
36
|
-
# engine.llm_wrapper,
|
|
36
|
+
# engine.llm_wrapper, which builds Hindsight's cached config via
|
|
37
37
|
# HindsightConfig.from_env(). So these env vars MUST be set before that import
|
|
38
38
|
# line, not in Cerebral.__init__ (too late). We inject our own LiteLLM-SDK
|
|
39
39
|
# embeddings and an RRF-passthrough cross-encoder in Cerebral.__init__, so the
|
|
@@ -61,20 +61,12 @@ from hindsight_api.extensions.builtin.tenant import DefaultTenantExtension
|
|
|
61
61
|
from hindsight_api.models import RequestContext
|
|
62
62
|
from hindsight_api.worker.exceptions import DeferOperation
|
|
63
63
|
|
|
64
|
-
#
|
|
65
|
-
# (see delta_ops_compat.py for details)
|
|
66
|
-
from codespy.agents.memory.cerebral.delta_ops_compat import install_delta_ops_coercion
|
|
67
|
-
|
|
68
|
-
install_delta_ops_coercion()
|
|
69
|
-
|
|
70
|
-
# Install native structured output support and Bedrock schema sanitizer
|
|
64
|
+
# Register native structured output for Bedrock models litellm's map lacks the flag for
|
|
71
65
|
from codespy.agents.memory.cerebral.structured_output import (
|
|
72
|
-
install_bedrock_schema_sanitizer,
|
|
73
66
|
register_native_structured_output_overrides,
|
|
74
67
|
)
|
|
75
68
|
|
|
76
69
|
register_native_structured_output_overrides()
|
|
77
|
-
install_bedrock_schema_sanitizer()
|
|
78
70
|
|
|
79
71
|
|
|
80
72
|
class _SchemaSyncTaskBackend(SyncTaskBackend):
|
|
@@ -233,13 +225,14 @@ from codespy.agents.memory.cerebral.cost import (
|
|
|
233
225
|
MeteredLiteLLMSDKEmbeddings,
|
|
234
226
|
register_cerebral_cost_recorder,
|
|
235
227
|
)
|
|
236
|
-
from codespy.agents.memory.cerebral.routines import ensure_maintenance_routines
|
|
237
228
|
from codespy.agents.memory.prefrontal.reach import (
|
|
238
229
|
TAG_ORG,
|
|
239
230
|
TAG_PROJECT_SCOPE,
|
|
240
231
|
TAG_REPO,
|
|
232
|
+
is_root_scope,
|
|
241
233
|
mental_model_id,
|
|
242
234
|
repo_tags,
|
|
235
|
+
root_scope_id,
|
|
243
236
|
scope_tags,
|
|
244
237
|
)
|
|
245
238
|
|
|
@@ -281,17 +274,23 @@ HINDSIGHT_SCHEMA = HINDSIGHT_SCHEMA_DEFAULT
|
|
|
281
274
|
# - include_chunks=False: Observation text only (no file chunks)
|
|
282
275
|
# - reflect_search_observations_max_tokens=3000: Down from 5000 default
|
|
283
276
|
# - reflect_search_observations_include_entities=False: Exclude entity metadata
|
|
277
|
+
# - budget=low: a refresh gets reflect_max_iterations // 2 iterations, like Prefrontal
|
|
278
|
+
# reflect (Hindsight treats a null budget as MID for refreshes)
|
|
279
|
+
# - keep_trace=True: store the refresh trace with the briefing
|
|
280
|
+
# Key names must be exact: create_mental_model stores the dict without validating it.
|
|
284
281
|
_BRIEFING_TRIGGER_STATIC = {
|
|
285
282
|
"mode": "delta",
|
|
286
283
|
"exclude_mental_models": True,
|
|
287
284
|
"include_chunks": False,
|
|
288
285
|
"reflect_search_observations_max_tokens": 3000,
|
|
289
286
|
"reflect_search_observations_include_entities": False,
|
|
287
|
+
"budget": "low",
|
|
288
|
+
"keep_trace": True,
|
|
290
289
|
}
|
|
291
290
|
|
|
292
|
-
# Content Hindsight
|
|
293
|
-
#
|
|
294
|
-
|
|
291
|
+
# Content pre-1.0 Hindsight gave a mental model before its first refresh.
|
|
292
|
+
# Only used to clear it from upgraded rows (see Cerebral._clear_legacy_placeholders).
|
|
293
|
+
_LEGACY_PLACEHOLDER = "Generating content..."
|
|
295
294
|
|
|
296
295
|
# Generic (not code-review specific) question each briefing answers.
|
|
297
296
|
MENTAL_MODEL_SOURCE_QUERY = (
|
|
@@ -300,6 +299,26 @@ MENTAL_MODEL_SOURCE_QUERY = (
|
|
|
300
299
|
"dependencies and integrations, pitfalls and recurring problems, facts shown to be wrong."
|
|
301
300
|
)
|
|
302
301
|
|
|
302
|
+
# Question the repo-root briefing answers. The root scope of a monorepo sees
|
|
303
|
+
# facts from many scopes; this briefing must keep only what holds for all of them.
|
|
304
|
+
ROOT_MENTAL_MODEL_SOURCE_QUERY = (
|
|
305
|
+
"What should an agent starting work anywhere in this repository know, that holds for "
|
|
306
|
+
"every scope of it: repository layout and naming conventions, how scopes and manifests "
|
|
307
|
+
"are organized, shared tooling, build, CI and deployment, cross-scope integrations and "
|
|
308
|
+
"contracts, and repository-wide pitfalls or facts shown to be wrong. "
|
|
309
|
+
"Do not describe individual services, packages or libraries, their files, pull requests "
|
|
310
|
+
"or review findings; omit anything that is true for only one scope."
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# A delta refresh only adds content, so briefings grow past max_tokens. Past this
|
|
314
|
+
# factor of max_tokens, consolidate_run forces one full refresh (rewrite from scratch).
|
|
315
|
+
BRIEFING_FULL_REFRESH_FACTOR = 1.5
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _estimate_tokens(text: str) -> int:
|
|
319
|
+
"""Cheap token estimate (~4 chars per token), same heuristic as prefrontal.format."""
|
|
320
|
+
return (len(text or "") + 3) // 4
|
|
321
|
+
|
|
303
322
|
OBSERVATIONS_MISSION = (
|
|
304
323
|
"Merge facts about the same subject into one observation. Keep details that matter only "
|
|
305
324
|
"to one kind of work (e.g. documentation, security, dependencies) instead of generalizing "
|
|
@@ -443,19 +462,19 @@ class Cerebral:
|
|
|
443
462
|
# Install two-step structured output fallback when extraction model is configured
|
|
444
463
|
if extraction_llm_model:
|
|
445
464
|
try:
|
|
446
|
-
from hindsight_api.engine.llm_wrapper import
|
|
465
|
+
from hindsight_api.engine.llm_wrapper import LLMProvider
|
|
447
466
|
from codespy.agents.memory.cerebral.structured_output import (
|
|
448
467
|
install_two_step_structured_output,
|
|
449
468
|
)
|
|
450
469
|
|
|
451
|
-
|
|
470
|
+
extraction_llm = LLMProvider(
|
|
452
471
|
provider="litellm",
|
|
453
472
|
model=extraction_llm_model,
|
|
454
473
|
api_key=extraction_llm_api_key,
|
|
455
474
|
base_url=extraction_llm_base_url or "",
|
|
456
475
|
reasoning_effort=None,
|
|
457
476
|
)
|
|
458
|
-
extraction_impl =
|
|
477
|
+
extraction_impl = extraction_llm._provider_impl
|
|
459
478
|
install_two_step_structured_output(self._engine, extraction_impl)
|
|
460
479
|
except Exception:
|
|
461
480
|
logger.warning(
|
|
@@ -466,11 +485,6 @@ class Cerebral:
|
|
|
466
485
|
|
|
467
486
|
self._run_async(self._engine.initialize())
|
|
468
487
|
|
|
469
|
-
# One-time repair: install maintenance routines if missing.
|
|
470
|
-
# This is idempotent and only needed for databases that were migrated
|
|
471
|
-
# before HINDSIGHT_API_DATABASE_SCHEMA was set to "semantic" (upstream #2638).
|
|
472
|
-
self._run_async(ensure_maintenance_routines(self._engine, HINDSIGHT_SCHEMA))
|
|
473
|
-
|
|
474
488
|
# Repair orphaned pending operations left by inline SyncTaskBackend
|
|
475
489
|
from codespy.agents.memory.cerebral.orphans import repair_orphaned_operations
|
|
476
490
|
|
|
@@ -574,14 +588,11 @@ class Cerebral:
|
|
|
574
588
|
# pending rows from blocking scoped submissions).
|
|
575
589
|
"enable_auto_consolidation": False,
|
|
576
590
|
}
|
|
577
|
-
# Per-scope observation cap (
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
"limit": self._max_observations_per_scope,
|
|
583
|
-
}
|
|
584
|
-
]
|
|
591
|
+
# Per-scope observation cap (never the bank-wide max_observations_per_scope).
|
|
592
|
+
# Sent unconditionally so -1 (unlimited) lifts a previously stored cap.
|
|
593
|
+
updates["consolidation_strategies"] = self._consolidation_strategies()
|
|
594
|
+
# Tombstone the deprecated key (bank config merges with `config || $1`).
|
|
595
|
+
updates["observation_scope_limits"] = None
|
|
585
596
|
self._run_async(
|
|
586
597
|
self._engine.update_bank_config(
|
|
587
598
|
self._bank_id,
|
|
@@ -589,10 +600,58 @@ class Cerebral:
|
|
|
589
600
|
request_context=ctx,
|
|
590
601
|
)
|
|
591
602
|
)
|
|
603
|
+
self._clear_legacy_placeholders(ctx)
|
|
592
604
|
except Exception:
|
|
593
605
|
logger.warning("cerebral: failed to configure bank %s", self._bank_id, exc_info=True)
|
|
594
606
|
self._bank_ensured = True
|
|
595
607
|
|
|
608
|
+
def _consolidation_strategies(self) -> list[dict[str, Any]]:
|
|
609
|
+
"""``consolidation_strategies`` payload carrying the per-scope observation cap."""
|
|
610
|
+
return [
|
|
611
|
+
{
|
|
612
|
+
"scopes": [
|
|
613
|
+
{
|
|
614
|
+
"tags": [f"{TAG_ORG}*", f"{TAG_REPO}*", f"{TAG_PROJECT_SCOPE}*"],
|
|
615
|
+
"tags_match": "exact",
|
|
616
|
+
}
|
|
617
|
+
],
|
|
618
|
+
"max_observations_per_scope": self._max_observations_per_scope,
|
|
619
|
+
}
|
|
620
|
+
]
|
|
621
|
+
|
|
622
|
+
def _clear_legacy_placeholders(self, ctx: RequestContext) -> int:
|
|
623
|
+
"""Empty briefings that still hold the pre-1.0 placeholder. Returns the count.
|
|
624
|
+
|
|
625
|
+
Pages through every mental model of the bank and rewrites legacy rows
|
|
626
|
+
with ``content=""`` (what Hindsight 1.0 creates pages with).
|
|
627
|
+
"""
|
|
628
|
+
cleared = 0
|
|
629
|
+
offset = 0
|
|
630
|
+
while True:
|
|
631
|
+
page = self._run_async(
|
|
632
|
+
self._engine.list_mental_models(
|
|
633
|
+
self._bank_id,
|
|
634
|
+
detail="full",
|
|
635
|
+
limit=100,
|
|
636
|
+
offset=offset,
|
|
637
|
+
request_context=ctx,
|
|
638
|
+
)
|
|
639
|
+
)
|
|
640
|
+
for item in page.items:
|
|
641
|
+
if (item.get("content") or "").strip().startswith(_LEGACY_PLACEHOLDER):
|
|
642
|
+
self._run_async(
|
|
643
|
+
self._engine.update_mental_model(
|
|
644
|
+
self._bank_id, item["id"], content="", request_context=ctx
|
|
645
|
+
)
|
|
646
|
+
)
|
|
647
|
+
cleared += 1
|
|
648
|
+
offset += len(page.items)
|
|
649
|
+
if not page.items or offset >= page.total:
|
|
650
|
+
break
|
|
651
|
+
if cleared:
|
|
652
|
+
logger.info("cerebral: cleared legacy placeholder from %d briefing(s)", cleared)
|
|
653
|
+
return cleared
|
|
654
|
+
|
|
596
655
|
@staticmethod
|
|
597
656
|
def _mutation_lines(episode: Episode) -> list[str]:
|
|
598
657
|
"""Build observation lines from episode mutations.
|
|
@@ -823,16 +882,21 @@ class Cerebral:
|
|
|
823
882
|
if topic.type == "project_scope"
|
|
824
883
|
]
|
|
825
884
|
# Fallback to repo-root project scope instead of bare [org:, repo:]
|
|
826
|
-
return scopes or [scope_tags(repo_full_name, repo_full_name)]
|
|
885
|
+
return scopes or [scope_tags(repo_full_name, root_scope_id(repo_full_name))]
|
|
827
886
|
|
|
828
887
|
@staticmethod
|
|
829
888
|
def _mental_model_scopes(
|
|
830
889
|
scopes: Sequence[Sequence[str]], repo_full_name: str
|
|
831
890
|
) -> list[list[str]]:
|
|
832
|
-
"""Observation scopes
|
|
891
|
+
"""Observation scopes whose briefings are kept (deduped).
|
|
892
|
+
|
|
893
|
+
No bare ``[org:, repo:]`` scope: its briefing would read every fact of
|
|
894
|
+
the repo (all scopes of a monorepo). Repo-wide knowledge lives in the
|
|
895
|
+
repo-root scope ``[org:, repo:, project_scope:<repo>]`` instead.
|
|
896
|
+
"""
|
|
833
897
|
out: list[list[str]] = []
|
|
834
898
|
seen: set[tuple[str, ...]] = set()
|
|
835
|
-
for sc in
|
|
899
|
+
for sc in scopes:
|
|
836
900
|
key = tuple(sorted(sc))
|
|
837
901
|
if key not in seen:
|
|
838
902
|
seen.add(key)
|
|
@@ -840,15 +904,43 @@ class Cerebral:
|
|
|
840
904
|
return out
|
|
841
905
|
|
|
842
906
|
@staticmethod
|
|
843
|
-
def
|
|
907
|
+
def _scope_id_of(tags: Sequence[str]) -> str | None:
|
|
844
908
|
for tag in tags:
|
|
845
909
|
if tag.startswith(TAG_PROJECT_SCOPE):
|
|
846
|
-
return
|
|
910
|
+
return tag[len(TAG_PROJECT_SCOPE):]
|
|
911
|
+
return None
|
|
912
|
+
|
|
913
|
+
@staticmethod
|
|
914
|
+
def _repo_of(tags: Sequence[str]) -> str | None:
|
|
847
915
|
for tag in tags:
|
|
848
|
-
if tag.startswith(
|
|
849
|
-
return
|
|
916
|
+
if tag.startswith(TAG_REPO):
|
|
917
|
+
return tag[len(TAG_REPO):]
|
|
918
|
+
return None
|
|
919
|
+
|
|
920
|
+
@classmethod
|
|
921
|
+
def _is_root_briefing(cls, tags: Sequence[str]) -> bool:
|
|
922
|
+
scope_id = cls._scope_id_of(tags)
|
|
923
|
+
repo = cls._repo_of(tags)
|
|
924
|
+
return bool(scope_id and repo and is_root_scope(repo, scope_id))
|
|
925
|
+
|
|
926
|
+
@classmethod
|
|
927
|
+
def _mental_model_name(cls, tags: Sequence[str]) -> str:
|
|
928
|
+
scope_id = cls._scope_id_of(tags)
|
|
929
|
+
if scope_id:
|
|
930
|
+
if cls._is_root_briefing(tags):
|
|
931
|
+
return f"Briefing: {scope_id} (repo-wide)"
|
|
932
|
+
return f"Briefing: {scope_id}"
|
|
933
|
+
repo = cls._repo_of(tags)
|
|
934
|
+
if repo:
|
|
935
|
+
return f"Briefing: {repo}"
|
|
850
936
|
return "Briefing"
|
|
851
937
|
|
|
938
|
+
@classmethod
|
|
939
|
+
def _mental_model_source_query(cls, tags: Sequence[str]) -> str:
|
|
940
|
+
if cls._is_root_briefing(tags):
|
|
941
|
+
return ROOT_MENTAL_MODEL_SOURCE_QUERY
|
|
942
|
+
return MENTAL_MODEL_SOURCE_QUERY
|
|
943
|
+
|
|
852
944
|
def _sync_briefing_triggers(self, scopes: list[list[str]]) -> None:
|
|
853
945
|
"""Sync briefing triggers to match current settings before consolidation.
|
|
854
946
|
|
|
@@ -863,11 +955,7 @@ class Cerebral:
|
|
|
863
955
|
if not scopes:
|
|
864
956
|
return
|
|
865
957
|
|
|
866
|
-
desired_trigger =
|
|
867
|
-
**_BRIEFING_TRIGGER_STATIC,
|
|
868
|
-
"refresh_after_consolidation": self._mental_models,
|
|
869
|
-
"min_refresh_interval_seconds": self._min_mental_model_refresh_seconds,
|
|
870
|
-
}
|
|
958
|
+
desired_trigger = self._desired_trigger()
|
|
871
959
|
|
|
872
960
|
for tags in scopes:
|
|
873
961
|
mm_id = mental_model_id(tags)
|
|
@@ -891,8 +979,8 @@ class Cerebral:
|
|
|
891
979
|
self._engine.create_mental_model(
|
|
892
980
|
self._bank_id,
|
|
893
981
|
self._mental_model_name(tags),
|
|
894
|
-
|
|
895
|
-
|
|
982
|
+
self._mental_model_source_query(tags),
|
|
983
|
+
"",
|
|
896
984
|
mental_model_id=mm_id,
|
|
897
985
|
tags=sorted(tags),
|
|
898
986
|
max_tokens=self._max_mental_model_tokens,
|
|
@@ -914,11 +1002,12 @@ class Cerebral:
|
|
|
914
1002
|
self._engine.get_mental_model(self._bank_id, mm_id, request_context=ctx)
|
|
915
1003
|
)
|
|
916
1004
|
else:
|
|
917
|
-
# Existing briefing: update trigger if it differs
|
|
1005
|
+
# Existing briefing: update trigger if it differs. Stored triggers
|
|
1006
|
+
# are merged supersets (update_mental_model patches), so compare
|
|
1007
|
+
# only the keys codespy sends.
|
|
918
1008
|
current_trigger = existing.get("trigger") or {}
|
|
919
|
-
needs_update = (
|
|
920
|
-
current_trigger.get(
|
|
921
|
-
or current_trigger.get("exclude_mental_models") is not True
|
|
1009
|
+
needs_update = any(
|
|
1010
|
+
current_trigger.get(k) != v for k, v in desired_trigger.items()
|
|
922
1011
|
)
|
|
923
1012
|
if needs_update:
|
|
924
1013
|
self._run_async(
|
|
@@ -935,11 +1024,103 @@ class Cerebral:
|
|
|
935
1024
|
"on" if current_trigger.get("refresh_after_consolidation") else "off",
|
|
936
1025
|
"on" if self._mental_models else "off",
|
|
937
1026
|
)
|
|
1027
|
+
# Name / source query drift (e.g. the repo-root briefing moved to
|
|
1028
|
+
# its repo-wide question). A changed source query makes Hindsight's
|
|
1029
|
+
# next delta refresh fall back to a full rewrite.
|
|
1030
|
+
desired_name = self._mental_model_name(tags)
|
|
1031
|
+
desired_query = self._mental_model_source_query(tags)
|
|
1032
|
+
if (
|
|
1033
|
+
existing.get("name") != desired_name
|
|
1034
|
+
or existing.get("source_query") != desired_query
|
|
1035
|
+
):
|
|
1036
|
+
self._run_async(
|
|
1037
|
+
self._engine.update_mental_model(
|
|
1038
|
+
self._bank_id,
|
|
1039
|
+
mm_id,
|
|
1040
|
+
name=desired_name,
|
|
1041
|
+
source_query=desired_query,
|
|
1042
|
+
request_context=ctx,
|
|
1043
|
+
)
|
|
1044
|
+
)
|
|
1045
|
+
logger.info(
|
|
1046
|
+
"cerebral: briefing %s renamed/re-queried as %r (next refresh is full)",
|
|
1047
|
+
mm_id,
|
|
1048
|
+
desired_name,
|
|
1049
|
+
)
|
|
938
1050
|
|
|
939
1051
|
self._ensured_mental_models.add(mm_id)
|
|
940
1052
|
except Exception:
|
|
941
1053
|
logger.warning("cerebral: failed to sync briefing trigger for %s", mm_id, exc_info=True)
|
|
942
1054
|
|
|
1055
|
+
def _desired_trigger(self) -> dict[str, Any]:
|
|
1056
|
+
"""Briefing trigger for the current settings (delta mode)."""
|
|
1057
|
+
return {
|
|
1058
|
+
**_BRIEFING_TRIGGER_STATIC,
|
|
1059
|
+
"refresh_after_consolidation": self._mental_models,
|
|
1060
|
+
"min_refresh_interval_seconds": self._min_mental_model_refresh_seconds,
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
def _enforce_briefing_size(self, scopes: Sequence[Sequence[str]]) -> None:
|
|
1064
|
+
"""Force a full refresh of briefings grown past the size limit. Never raises.
|
|
1065
|
+
|
|
1066
|
+
Delta refreshes only add content, so a briefing drifts past
|
|
1067
|
+
``max_mental_model_tokens``. Above ``BRIEFING_FULL_REFRESH_FACTOR`` times
|
|
1068
|
+
that budget, the briefing is rewritten from scratch once: the trigger is
|
|
1069
|
+
switched to ``mode=full``, one explicit refresh runs inline (sync task
|
|
1070
|
+
backend), then the delta trigger is restored. Never deletes anything.
|
|
1071
|
+
"""
|
|
1072
|
+
if not self._mental_models or not scopes:
|
|
1073
|
+
return
|
|
1074
|
+
limit = int(self._max_mental_model_tokens * BRIEFING_FULL_REFRESH_FACTOR)
|
|
1075
|
+
for tags in scopes:
|
|
1076
|
+
mm_id = mental_model_id(tags)
|
|
1077
|
+
try:
|
|
1078
|
+
ctx = RequestContext()
|
|
1079
|
+
mm = self._run_async(
|
|
1080
|
+
self._engine.get_mental_model(self._bank_id, mm_id, request_context=ctx)
|
|
1081
|
+
)
|
|
1082
|
+
if not mm:
|
|
1083
|
+
continue
|
|
1084
|
+
before = _estimate_tokens((mm.get("content") or "").strip())
|
|
1085
|
+
if before <= limit:
|
|
1086
|
+
continue
|
|
1087
|
+
desired = self._desired_trigger()
|
|
1088
|
+
self._run_async(
|
|
1089
|
+
self._engine.update_mental_model(
|
|
1090
|
+
self._bank_id,
|
|
1091
|
+
mm_id,
|
|
1092
|
+
trigger={**desired, "mode": "full"},
|
|
1093
|
+
request_context=ctx,
|
|
1094
|
+
)
|
|
1095
|
+
)
|
|
1096
|
+
try:
|
|
1097
|
+
self._run_async(
|
|
1098
|
+
self._engine.submit_async_refresh_mental_model(
|
|
1099
|
+
self._bank_id, mm_id, request_context=ctx
|
|
1100
|
+
)
|
|
1101
|
+
)
|
|
1102
|
+
finally:
|
|
1103
|
+
self._run_async(
|
|
1104
|
+
self._engine.update_mental_model(
|
|
1105
|
+
self._bank_id, mm_id, trigger=desired, request_context=ctx
|
|
1106
|
+
)
|
|
1107
|
+
)
|
|
1108
|
+
after_mm = self._run_async(
|
|
1109
|
+
self._engine.get_mental_model(self._bank_id, mm_id, request_context=ctx)
|
|
1110
|
+
)
|
|
1111
|
+
after = _estimate_tokens(((after_mm or {}).get("content") or "").strip())
|
|
1112
|
+
logger.info(
|
|
1113
|
+
"cerebral: briefing %s (%s) over size (%d > %d tokens); "
|
|
1114
|
+
"full refresh -> %d tokens",
|
|
1115
|
+
mm_id,
|
|
1116
|
+
self._mental_model_name(tags),
|
|
1117
|
+
before,
|
|
1118
|
+
limit,
|
|
1119
|
+
after,
|
|
1120
|
+
)
|
|
1121
|
+
except Exception:
|
|
1122
|
+
logger.warning("cerebral: briefing size guard failed for %s", mm_id, exc_info=True)
|
|
1123
|
+
|
|
943
1124
|
def consolidate_run(self, run_id: str) -> None:
|
|
944
1125
|
"""Trigger one consolidation for all scopes touched during a run.
|
|
945
1126
|
|
|
@@ -986,6 +1167,8 @@ class Cerebral:
|
|
|
986
1167
|
observation_scopes=scopes,
|
|
987
1168
|
)
|
|
988
1169
|
)
|
|
1170
|
+
# Rewrite briefings that delta refreshes grew past their budget
|
|
1171
|
+
self._enforce_briefing_size(scopes)
|
|
989
1172
|
|
|
990
1173
|
delta = {b: _calls(b) - baseline[b] for b in buckets}
|
|
991
1174
|
logger.info(
|
|
@@ -1134,10 +1317,10 @@ class Cerebral:
|
|
|
1134
1317
|
summary.rewrite = True
|
|
1135
1318
|
|
|
1136
1319
|
# Count iterations from agent_<n> scopes (excluding _err variants)
|
|
1137
|
-
# +1 if there's any final scope (map, reduce, or
|
|
1320
|
+
# +1 if there's any final scope (map, reduce, rewrite, or Hindsight 1.0's closing_done)
|
|
1138
1321
|
agent_scopes = {s for s in scope_names if s.startswith("agent_") and not s.endswith("_err")}
|
|
1139
1322
|
summary.iterations = len(agent_scopes)
|
|
1140
|
-
if any(s.startswith("final") for s in scope_names):
|
|
1323
|
+
if any(s.startswith("final") or s == "closing_done" for s in scope_names):
|
|
1141
1324
|
summary.iterations += 1
|
|
1142
1325
|
|
|
1143
1326
|
# Tools from tool_trace (list of ToolCallTrace with output dict)
|
|
@@ -1218,6 +1401,10 @@ class Cerebral:
|
|
|
1218
1401
|
if topic.type == "project_scope":
|
|
1219
1402
|
tags.append(f"{topic.type}:{topic.id}")
|
|
1220
1403
|
if repo_full_name:
|
|
1404
|
+
# No scope topic: tag with the repo-root scope, matching the
|
|
1405
|
+
# _observation_scopes fallback (recall reads project_scope:, not repo:).
|
|
1406
|
+
if not tags:
|
|
1407
|
+
tags.append(f"{TAG_PROJECT_SCOPE}{root_scope_id(repo_full_name)}")
|
|
1221
1408
|
tags.extend(repo_tags(repo_full_name))
|
|
1222
1409
|
tags.append(f"task:{episode.task}")
|
|
1223
1410
|
return tags
|
|
@@ -117,6 +117,7 @@ class CerebralCostRecorder:
|
|
|
117
117
|
scope: str = "",
|
|
118
118
|
input_tokens: int = 0,
|
|
119
119
|
output_tokens: int = 0,
|
|
120
|
+
thoughts_tokens: int | None = 0,
|
|
120
121
|
error: Exception | None = None,
|
|
121
122
|
duration: float = 0.0,
|
|
122
123
|
**_: Any,
|
|
@@ -127,7 +128,10 @@ class CerebralCostRecorder:
|
|
|
127
128
|
model: Full litellm model string (e.g., "bedrock/converse/moonshotai.kimi-k2.5")
|
|
128
129
|
scope: Hindsight scope (e.g., "retain_extract_facts", "consolidation")
|
|
129
130
|
input_tokens: Input/prompt token count
|
|
130
|
-
output_tokens:
|
|
131
|
+
output_tokens: Visible output/completion token count
|
|
132
|
+
thoughts_tokens: Reasoning tokens. Hindsight >=0.10.2 reports them apart
|
|
133
|
+
from ``output_tokens``; they are billed as output, so they are
|
|
134
|
+
folded into the output count used for pricing and tracking.
|
|
131
135
|
error: Exception if the call failed
|
|
132
136
|
duration: Duration of the call in seconds
|
|
133
137
|
**_: Ignored extra kwargs for forward compatibility
|
|
@@ -137,21 +141,23 @@ class CerebralCostRecorder:
|
|
|
137
141
|
and errored calls that reached the provider (with billed usage).
|
|
138
142
|
"""
|
|
139
143
|
try:
|
|
144
|
+
billed_output = output_tokens + (thoughts_tokens or 0)
|
|
145
|
+
|
|
140
146
|
# Skip calls with no tokens (includes failed calls that never reached provider)
|
|
141
|
-
if input_tokens == 0 and
|
|
147
|
+
if input_tokens == 0 and billed_output == 0:
|
|
142
148
|
return
|
|
143
149
|
|
|
144
150
|
# Price the call using litellm
|
|
145
|
-
prompt_cost, completion_cost = self._price_call_split(model, input_tokens,
|
|
151
|
+
prompt_cost, completion_cost = self._price_call_split(model, input_tokens, billed_output)
|
|
146
152
|
cost = prompt_cost + completion_cost
|
|
147
|
-
tokens = input_tokens +
|
|
153
|
+
tokens = input_tokens + billed_output
|
|
148
154
|
|
|
149
155
|
# Determine bucket: an active recall wins over the scope/task tag,
|
|
150
156
|
# because Hindsight scopes cannot tell a reflect apart
|
|
151
157
|
# from a mental-model refresh.
|
|
152
158
|
usage = current_recall_usage.get()
|
|
153
159
|
if usage is not None:
|
|
154
|
-
usage.add_llm(model, input_tokens,
|
|
160
|
+
usage.add_llm(model, input_tokens, billed_output, prompt_cost, completion_cost)
|
|
155
161
|
bucket = BUCKET_MEMORY_RECALL
|
|
156
162
|
else:
|
|
157
163
|
bucket = _llm_bucket(scope)
|
|
@@ -166,7 +172,7 @@ class CerebralCostRecorder:
|
|
|
166
172
|
tokens=tokens,
|
|
167
173
|
calls=1,
|
|
168
174
|
input_tokens=input_tokens,
|
|
169
|
-
output_tokens=
|
|
175
|
+
output_tokens=billed_output,
|
|
170
176
|
input_cost=prompt_cost,
|
|
171
177
|
output_cost=completion_cost,
|
|
172
178
|
duration=duration,
|