deepy-cli 0.2.10__tar.gz → 0.2.12__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.
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/PKG-INFO +1 -1
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/pyproject.toml +1 -1
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/__init__.py +1 -1
- deepy_cli-0.2.12/src/deepy/data/tools/apply_patch.md +68 -0
- deepy_cli-0.2.12/src/deepy/data/tools/edit_text.md +21 -0
- deepy_cli-0.2.12/src/deepy/data/tools/read_file.md +16 -0
- deepy_cli-0.2.12/src/deepy/data/tools/write_file.md +13 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/prompts/system.py +3 -3
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/prompts/tool_docs.py +4 -2
- deepy_cli-0.2.12/src/deepy/session_cost.py +183 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/sessions/jsonl.py +36 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/tools/agents.py +193 -79
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/tools/builtin.py +1465 -76
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/tools/file_state.py +61 -6
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/tui/app.py +58 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/tui/diff.py +66 -14
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/ui/exit_summary.py +13 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/ui/message_view.py +92 -10
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/ui/terminal.py +156 -46
- deepy_cli-0.2.10/src/deepy/data/tools/edit.md +0 -17
- deepy_cli-0.2.10/src/deepy/data/tools/modify.md +0 -26
- deepy_cli-0.2.10/src/deepy/data/tools/read.md +0 -8
- deepy_cli-0.2.10/src/deepy/data/tools/write.md +0 -16
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/README.md +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/__main__.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/cli.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/config/__init__.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/config/settings.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/data/__init__.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/data/skills/skill-creator/SKILL.md +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/data/skills/skill-installer/SKILL.md +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/data/tools/AskUserQuestion.md +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/data/tools/WebFetch.md +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/data/tools/WebSearch.md +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/data/tools/__init__.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/data/tools/shell.md +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/data/tools/todo_write.md +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/errors.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/input_suggestions.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/llm/__init__.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/llm/agent.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/llm/compaction.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/llm/context.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/llm/events.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/llm/model_capabilities.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/llm/provider.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/llm/replay.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/llm/runner.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/llm/thinking.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/mcp.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/prompts/__init__.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/prompts/compact.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/prompts/init_agents.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/prompts/rules.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/prompts/runtime_context.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/sessions/__init__.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/sessions/manager.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/skill_market.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/skills.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/status.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/todos.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/tools/__init__.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/tools/result.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/tools/shell_output.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/tools/shell_utils.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/tui/__init__.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/tui/commands.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/tui/compat.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/tui/runner.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/tui/screens.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/tui/state.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/tui/widgets.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/types/__init__.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/types/sdk.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/types/tool_payloads.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/ui/__init__.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/ui/app.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/ui/ask_user_question.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/ui/file_mentions.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/ui/loading_text.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/ui/local_command.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/ui/markdown.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/ui/model_picker.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/ui/prompt_buffer.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/ui/prompt_input.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/ui/session_list.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/ui/session_picker.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/ui/skill_picker.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/ui/slash_commands.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/ui/status_footer.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/ui/styles.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/ui/theme_picker.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/ui/thinking_state.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/ui/welcome.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/update_check.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/usage.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/utils/__init__.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/utils/debug_logger.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/utils/error_logger.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/utils/json.py +0 -0
- {deepy_cli-0.2.10 → deepy_cli-0.2.12}/src/deepy/utils/notify.py +0 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
## apply_patch
|
|
2
|
+
|
|
3
|
+
Batch structured file operations in one call.
|
|
4
|
+
|
|
5
|
+
Args: `operations`.
|
|
6
|
+
|
|
7
|
+
Use this when a change has multiple edits in one file, touches multiple files,
|
|
8
|
+
creates/deletes/moves files, replaces a larger block, or needs one all-or-nothing
|
|
9
|
+
preflight before writing. Use `edit_text` only for one small single-file exact
|
|
10
|
+
edit where `old_string` and `new_string` are straightforward.
|
|
11
|
+
|
|
12
|
+
Each operation is an object with a `type` and the fields relevant to that type.
|
|
13
|
+
Set unrelated nullable fields to `null` when required by the schema.
|
|
14
|
+
|
|
15
|
+
Supported operation types:
|
|
16
|
+
|
|
17
|
+
- `create_file`: create a new text file with `file_path` and `content`.
|
|
18
|
+
- `replace_file`: explicitly replace a whole existing file with `file_path`,
|
|
19
|
+
`content`, `overwrite=true`, and either `snapshot_id` or `expected_hash`.
|
|
20
|
+
- `delete_file`: delete `file_path`.
|
|
21
|
+
- `move_file`: move `file_path` to `destination_path`.
|
|
22
|
+
- `replace_block`: replace exact `old_text` with `new_text`.
|
|
23
|
+
- `insert_before`: insert `content` before exact `anchor`.
|
|
24
|
+
- `insert_after`: insert `content` after exact `anchor`.
|
|
25
|
+
- `replace_all`: replace every exact `old_text` match with `new_text`.
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"operations": [
|
|
32
|
+
{
|
|
33
|
+
"type": "replace_block",
|
|
34
|
+
"file_path": "portfolio/index.html",
|
|
35
|
+
"old_text": "<p>Old bio</p>",
|
|
36
|
+
"new_text": "<p>New bio</p>",
|
|
37
|
+
"expected_occurrences": 1,
|
|
38
|
+
"destination_path": null,
|
|
39
|
+
"content": null,
|
|
40
|
+
"anchor": null,
|
|
41
|
+
"replace_all": null,
|
|
42
|
+
"overwrite": null,
|
|
43
|
+
"snapshot_id": null,
|
|
44
|
+
"expected_hash": null
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"type": "insert_after",
|
|
48
|
+
"file_path": "portfolio/styles.css",
|
|
49
|
+
"anchor": ".about {\\n display: grid;\\n}\\n",
|
|
50
|
+
"content": "\\n.about-tags {\\n display: flex;\\n}\\n",
|
|
51
|
+
"expected_occurrences": 1,
|
|
52
|
+
"destination_path": null,
|
|
53
|
+
"old_text": null,
|
|
54
|
+
"new_text": null,
|
|
55
|
+
"replace_all": null,
|
|
56
|
+
"overwrite": null,
|
|
57
|
+
"snapshot_id": null,
|
|
58
|
+
"expected_hash": null
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Deepy preflights all operations before committing file side effects, preserves
|
|
65
|
+
existing encodings and line endings on updates, writes new text files as UTF-8
|
|
66
|
+
without BOM, and returns per-operation, changed-file, diff, and diff-preview
|
|
67
|
+
metadata. Exact text and anchor operations reject absent, ambiguous, no-op, and
|
|
68
|
+
unexpected-count matches with structured diagnostics.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
## edit_text
|
|
2
|
+
|
|
3
|
+
Make a small exact/string edit to an existing text file.
|
|
4
|
+
|
|
5
|
+
Args: `file_path`, `old_string`, `new_string`, `replace_all`,
|
|
6
|
+
`expected_occurrences`, optional `snippet_id`.
|
|
7
|
+
|
|
8
|
+
Use this for focused edits where the current text is known. Prefer
|
|
9
|
+
`expected_occurrences` as a safety check, especially with `replace_all`. If a
|
|
10
|
+
partial `read_file` returned a `snippet_id`, pass it only when you intentionally
|
|
11
|
+
want to restrict the replacement to that snippet. For ordinary single-file exact
|
|
12
|
+
edits, pass `file_path` and omit `snippet_id`; Deepy can internally promote a
|
|
13
|
+
fresh partial read to a full-file exact edit when needed. Do not pass
|
|
14
|
+
`snapshot_id` as `snippet_id`; snapshots are for stale protection, while
|
|
15
|
+
snippets are only returned by partial reads.
|
|
16
|
+
|
|
17
|
+
Deepy preserves the existing file encoding and line endings, rejects stale files,
|
|
18
|
+
rejects no-op edits, and returns structured error metadata for missing,
|
|
19
|
+
ambiguous, or count-mismatched replacements. Use `apply_patch` when there are
|
|
20
|
+
multiple edits in one file, multiple files, create/delete/move operations, or a
|
|
21
|
+
larger block replacement.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
## read_file
|
|
2
|
+
|
|
3
|
+
Read files or list directories before changes.
|
|
4
|
+
|
|
5
|
+
Args: `file_path`, optional `offset`, optional `limit`, optional `pages`.
|
|
6
|
+
|
|
7
|
+
Text output includes line numbers. Full text reads record a managed snapshot with
|
|
8
|
+
encoding, line-ending, snapshot id, and content hash metadata for later
|
|
9
|
+
`edit_text`, `write_file`, or `apply_patch` calls. Partial reads return snippet
|
|
10
|
+
metadata that can scope later `edit_text` calls but do not authorize unrestricted
|
|
11
|
+
whole-file replacement. For normal single-file exact edits after a partial read,
|
|
12
|
+
prefer `edit_text` with `file_path` and no `snippet_id`; use the snippet only
|
|
13
|
+
when you need to constrain the replacement to that line range.
|
|
14
|
+
|
|
15
|
+
Non-text files such as images, notebooks, and PDFs may return descriptive
|
|
16
|
+
metadata, but they are not tracked for text mutation.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
## write_file
|
|
2
|
+
|
|
3
|
+
Create a new text file or explicitly replace a whole file.
|
|
4
|
+
|
|
5
|
+
Args: `file_path`, `content`, `overwrite`, optional `snapshot_id`, optional
|
|
6
|
+
`expected_hash`.
|
|
7
|
+
|
|
8
|
+
For new files, Deepy writes UTF-8 without BOM by default. For existing files,
|
|
9
|
+
whole-file replacement requires `overwrite=true` and a fresh `snapshot_id` or
|
|
10
|
+
`expected_hash` from `read_file`; this prevents accidental stale rewrites.
|
|
11
|
+
|
|
12
|
+
Prefer `edit_text` for small targeted edits and `apply_patch` for structured or
|
|
13
|
+
multi-file edits.
|
|
@@ -43,10 +43,10 @@ def build_system_prompt(
|
|
|
43
43
|
return f"""You are Deepy, a terminal coding agent in the user's project.
|
|
44
44
|
|
|
45
45
|
Core rules:
|
|
46
|
-
- Work in the repo with tools: inspect,
|
|
46
|
+
- Work in the repo with tools: inspect, edit, test, verify.
|
|
47
47
|
- Preserve user changes. Prefer small, verifiable edits.
|
|
48
|
-
- Read existing files when you need context; exact `
|
|
49
|
-
- Use `
|
|
48
|
+
- Read existing files when you need context; exact `edit_text` edits can establish the managed snapshot internally.
|
|
49
|
+
- Use `edit_text` for one small single-file exact edit. Use structured `apply_patch.operations` when a change has multiple edits in one file, touches multiple files, creates/deletes/moves files, or replaces a larger block. Use `write_file` for new files or explicit whole-file replacement.
|
|
50
50
|
- After project generators create scaffold files, read and edit the generated block instead of replacing the file.
|
|
51
51
|
- Run shell commands using the Runtime context's command dialect and path style: `powershell` -> PowerShell with Windows paths; `cmd` -> cmd; `posix` -> POSIX shell.
|
|
52
52
|
- Match visible thinking/reasoning language to the user's latest natural language. If the user asks in Chinese, you MUST write visible thinking/reasoning in Chinese unless they explicitly request another language. Do not switch visible thinking/reasoning to English for Chinese requests.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import urllib.parse
|
|
4
|
+
from decimal import Decimal, InvalidOperation
|
|
5
|
+
from typing import Any, Mapping
|
|
6
|
+
|
|
7
|
+
from deepy.config import Settings
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def should_track_session_cost(settings: Settings) -> bool:
|
|
11
|
+
if not settings.model.api_key:
|
|
12
|
+
return False
|
|
13
|
+
parsed = urllib.parse.urlparse(settings.model.base_url)
|
|
14
|
+
return parsed.scheme in {"http", "https"} and parsed.hostname == "api.deepseek.com"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def balance_snapshot_to_dict(balance: Any, *, captured_at_ms: int) -> dict[str, Any]:
|
|
18
|
+
unavailable_reason = _string_or_none(_field(balance, "unavailable_reason"))
|
|
19
|
+
if unavailable_reason:
|
|
20
|
+
return {
|
|
21
|
+
"capturedAt": captured_at_ms,
|
|
22
|
+
"unavailableReason": unavailable_reason,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
infos: list[dict[str, str]] = []
|
|
26
|
+
for item in _field(balance, "balance_infos") or ():
|
|
27
|
+
currency = _string_or_none(_field(item, "currency"))
|
|
28
|
+
total = _string_or_none(_field(item, "total_balance"))
|
|
29
|
+
granted = _string_or_none(_field(item, "granted_balance"))
|
|
30
|
+
topped_up = _string_or_none(_field(item, "topped_up_balance"))
|
|
31
|
+
if None in (currency, total, granted, topped_up):
|
|
32
|
+
return {
|
|
33
|
+
"capturedAt": captured_at_ms,
|
|
34
|
+
"unavailableReason": "invalid balance snapshot",
|
|
35
|
+
}
|
|
36
|
+
infos.append(
|
|
37
|
+
{
|
|
38
|
+
"currency": currency or "",
|
|
39
|
+
"totalBalance": total or "",
|
|
40
|
+
"grantedBalance": granted or "",
|
|
41
|
+
"toppedUpBalance": topped_up or "",
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
is_available = _field(balance, "is_available")
|
|
45
|
+
return {
|
|
46
|
+
"capturedAt": captured_at_ms,
|
|
47
|
+
"isAvailable": is_available if isinstance(is_available, bool) else None,
|
|
48
|
+
"balanceInfos": infos,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def start_session_cost(snapshot: Mapping[str, Any]) -> dict[str, Any]:
|
|
53
|
+
cost: dict[str, Any] = {
|
|
54
|
+
"attempted": True,
|
|
55
|
+
"start": dict(snapshot),
|
|
56
|
+
}
|
|
57
|
+
if reason := _snapshot_unavailable_reason(snapshot):
|
|
58
|
+
cost["unavailableReason"] = f"start {reason}"
|
|
59
|
+
return cost
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def complete_session_cost(
|
|
63
|
+
existing: Mapping[str, Any] | None,
|
|
64
|
+
end_snapshot: Mapping[str, Any],
|
|
65
|
+
) -> dict[str, Any]:
|
|
66
|
+
cost: dict[str, Any] = dict(existing or {})
|
|
67
|
+
cost["attempted"] = True
|
|
68
|
+
cost["end"] = dict(end_snapshot)
|
|
69
|
+
start = cost.get("start")
|
|
70
|
+
if not isinstance(start, Mapping):
|
|
71
|
+
cost["amounts"] = []
|
|
72
|
+
cost["unavailableReason"] = "start snapshot missing"
|
|
73
|
+
return cost
|
|
74
|
+
amounts, reason = compute_session_cost_amounts(start, end_snapshot)
|
|
75
|
+
cost["amounts"] = amounts
|
|
76
|
+
if reason:
|
|
77
|
+
cost["unavailableReason"] = reason
|
|
78
|
+
else:
|
|
79
|
+
cost.pop("unavailableReason", None)
|
|
80
|
+
return cost
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def compute_session_cost_amounts(
|
|
84
|
+
start_snapshot: Mapping[str, Any],
|
|
85
|
+
end_snapshot: Mapping[str, Any],
|
|
86
|
+
) -> tuple[list[dict[str, str]], str | None]:
|
|
87
|
+
if reason := _snapshot_unavailable_reason(start_snapshot):
|
|
88
|
+
return [], f"start {reason}"
|
|
89
|
+
if reason := _snapshot_unavailable_reason(end_snapshot):
|
|
90
|
+
return [], f"end {reason}"
|
|
91
|
+
|
|
92
|
+
start_infos = _snapshot_infos_by_currency(start_snapshot)
|
|
93
|
+
end_infos = _snapshot_infos_by_currency(end_snapshot)
|
|
94
|
+
if start_infos is None or end_infos is None:
|
|
95
|
+
return [], "invalid balance snapshot"
|
|
96
|
+
shared = [currency for currency in start_infos if currency in end_infos]
|
|
97
|
+
if not shared:
|
|
98
|
+
return [], "currency mismatch"
|
|
99
|
+
|
|
100
|
+
amounts: list[dict[str, str]] = []
|
|
101
|
+
for currency in shared:
|
|
102
|
+
start_total_text = start_infos[currency]
|
|
103
|
+
end_total_text = end_infos[currency]
|
|
104
|
+
start_total = _decimal(start_total_text)
|
|
105
|
+
end_total = _decimal(end_total_text)
|
|
106
|
+
if start_total is None or end_total is None:
|
|
107
|
+
return [], "invalid balance amount"
|
|
108
|
+
spent = start_total - end_total
|
|
109
|
+
if spent > 0:
|
|
110
|
+
amounts.append(
|
|
111
|
+
{
|
|
112
|
+
"currency": currency,
|
|
113
|
+
"startTotal": start_total_text,
|
|
114
|
+
"endTotal": end_total_text,
|
|
115
|
+
"spent": _format_decimal(spent),
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
if not amounts:
|
|
119
|
+
return [], "no measurable spend"
|
|
120
|
+
return amounts, None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def format_session_cost(cost: Any) -> str | None:
|
|
124
|
+
if not isinstance(cost, Mapping) or not cost.get("attempted"):
|
|
125
|
+
return None
|
|
126
|
+
amounts = cost.get("amounts")
|
|
127
|
+
if isinstance(amounts, list) and amounts:
|
|
128
|
+
parts: list[str] = []
|
|
129
|
+
for item in amounts:
|
|
130
|
+
if not isinstance(item, Mapping):
|
|
131
|
+
continue
|
|
132
|
+
currency = _string_or_none(item.get("currency"))
|
|
133
|
+
spent = _string_or_none(item.get("spent"))
|
|
134
|
+
if currency and spent:
|
|
135
|
+
parts.append(f"{currency} {spent}")
|
|
136
|
+
if parts:
|
|
137
|
+
return f"{', '.join(parts)} (DeepSeek balance delta)"
|
|
138
|
+
reason = _string_or_none(cost.get("unavailableReason")) or "unknown"
|
|
139
|
+
return f"unavailable ({reason})"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _field(value: Any, name: str) -> Any:
|
|
143
|
+
if isinstance(value, Mapping):
|
|
144
|
+
return value.get(name)
|
|
145
|
+
return getattr(value, name, None)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _string_or_none(value: Any) -> str | None:
|
|
149
|
+
return value if isinstance(value, str) and value else None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _snapshot_unavailable_reason(snapshot: Mapping[str, Any]) -> str | None:
|
|
153
|
+
return _string_or_none(snapshot.get("unavailableReason"))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _snapshot_infos_by_currency(snapshot: Mapping[str, Any]) -> dict[str, str] | None:
|
|
157
|
+
raw_infos = snapshot.get("balanceInfos")
|
|
158
|
+
if not isinstance(raw_infos, list):
|
|
159
|
+
return None
|
|
160
|
+
infos: dict[str, str] = {}
|
|
161
|
+
for item in raw_infos:
|
|
162
|
+
if not isinstance(item, Mapping):
|
|
163
|
+
return None
|
|
164
|
+
currency = _string_or_none(item.get("currency"))
|
|
165
|
+
total = _string_or_none(item.get("totalBalance"))
|
|
166
|
+
if not currency or total is None:
|
|
167
|
+
return None
|
|
168
|
+
infos[currency] = total
|
|
169
|
+
return infos
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _decimal(value: str) -> Decimal | None:
|
|
173
|
+
try:
|
|
174
|
+
return Decimal(value)
|
|
175
|
+
except (InvalidOperation, ValueError):
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _format_decimal(value: Decimal) -> str:
|
|
180
|
+
text = format(value.normalize(), "f")
|
|
181
|
+
if "." not in text:
|
|
182
|
+
return text
|
|
183
|
+
return text.rstrip("0").rstrip(".") or "0"
|
|
@@ -40,6 +40,7 @@ class SessionEntry:
|
|
|
40
40
|
pending_tokens: int = 0
|
|
41
41
|
last_usage_record_count: int | None = None
|
|
42
42
|
todo_state: list[dict[str, str]] | None = None
|
|
43
|
+
session_cost: dict[str, Any] | None = None
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
def project_code(project_root: Path) -> str:
|
|
@@ -160,6 +161,7 @@ class DeepyJsonlSession:
|
|
|
160
161
|
pending_tokens=0,
|
|
161
162
|
last_usage_record_count=0,
|
|
162
163
|
todo_state=[],
|
|
164
|
+
session_cost=None,
|
|
163
165
|
)
|
|
164
166
|
|
|
165
167
|
def record_usage(self, usage: TokenUsage | dict[str, Any] | None) -> None:
|
|
@@ -207,6 +209,32 @@ class DeepyJsonlSession:
|
|
|
207
209
|
) + max(elapsed_ms, 0)
|
|
208
210
|
self._touch_index(input_suggestion_usage=accumulated)
|
|
209
211
|
|
|
212
|
+
def record_session_cost_start(self, snapshot: dict[str, Any]) -> None:
|
|
213
|
+
from deepy.session_cost import start_session_cost
|
|
214
|
+
|
|
215
|
+
previous = _entry_for_session(self.path.parent / "sessions-index.json", self.session_id)
|
|
216
|
+
previous_cost = previous.get("sessionCost") if previous else None
|
|
217
|
+
if isinstance(previous_cost, dict) and isinstance(previous_cost.get("start"), dict):
|
|
218
|
+
return
|
|
219
|
+
self._touch_index(session_cost=start_session_cost(snapshot))
|
|
220
|
+
|
|
221
|
+
def record_session_cost_end(self, snapshot: dict[str, Any]) -> None:
|
|
222
|
+
from deepy.session_cost import complete_session_cost
|
|
223
|
+
|
|
224
|
+
previous = _entry_for_session(self.path.parent / "sessions-index.json", self.session_id)
|
|
225
|
+
previous_cost = previous.get("sessionCost") if previous else None
|
|
226
|
+
self._touch_index(
|
|
227
|
+
session_cost=complete_session_cost(
|
|
228
|
+
previous_cost if isinstance(previous_cost, dict) else None,
|
|
229
|
+
snapshot,
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def session_cost(self) -> dict[str, Any] | None:
|
|
234
|
+
previous = _entry_for_session(self.path.parent / "sessions-index.json", self.session_id)
|
|
235
|
+
cost = previous.get("sessionCost") if previous else None
|
|
236
|
+
return cost if isinstance(cost, dict) else None
|
|
237
|
+
|
|
210
238
|
def context_token_state(
|
|
211
239
|
self,
|
|
212
240
|
records: list[dict[str, Any]] | None = None,
|
|
@@ -361,6 +389,7 @@ class DeepyJsonlSession:
|
|
|
361
389
|
last_usage_record_count: int | None = None,
|
|
362
390
|
todo_state: object = _UNSET,
|
|
363
391
|
input_suggestion_usage: dict[str, Any] | None = None,
|
|
392
|
+
session_cost: object = _UNSET,
|
|
364
393
|
) -> None:
|
|
365
394
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
366
395
|
index_path = self.path.parent / "sessions-index.json"
|
|
@@ -432,6 +461,11 @@ class DeepyJsonlSession:
|
|
|
432
461
|
),
|
|
433
462
|
**({"processes": previous["processes"]} if "processes" in previous else {}),
|
|
434
463
|
}
|
|
464
|
+
if session_cost is _UNSET:
|
|
465
|
+
if isinstance(previous.get("sessionCost"), dict):
|
|
466
|
+
entry["sessionCost"] = previous["sessionCost"]
|
|
467
|
+
elif isinstance(session_cost, dict):
|
|
468
|
+
entry["sessionCost"] = session_cost
|
|
435
469
|
if todo_state is _UNSET:
|
|
436
470
|
if "todoState" in previous:
|
|
437
471
|
entry["todoState"] = previous["todoState"]
|
|
@@ -498,6 +532,7 @@ def list_session_entries(project_root: Path, deepy_home: Path | None = None) ->
|
|
|
498
532
|
path = f"{session_id}.jsonl"
|
|
499
533
|
usage = item.get("usage")
|
|
500
534
|
input_suggestion_usage = item.get("inputSuggestionUsage")
|
|
535
|
+
session_cost = item.get("sessionCost")
|
|
501
536
|
entries.append(
|
|
502
537
|
SessionEntry(
|
|
503
538
|
id=session_id,
|
|
@@ -515,6 +550,7 @@ def list_session_entries(project_root: Path, deepy_home: Path | None = None) ->
|
|
|
515
550
|
pending_tokens=_coerce_int(item.get("pendingTokens"), 0),
|
|
516
551
|
last_usage_record_count=_optional_int(item.get("lastUsageRecordCount")),
|
|
517
552
|
todo_state=normalize_persisted_todo_state(item.get("todoState")),
|
|
553
|
+
session_cost=session_cost if isinstance(session_cost, dict) else None,
|
|
518
554
|
)
|
|
519
555
|
)
|
|
520
556
|
return entries
|