tsugite-cli 0.17.0__tar.gz → 0.17.2__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.
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/PKG-INFO +1 -1
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/pyproject.toml +1 -1
- tsugite_cli-0.17.2/tsugite/builtin_agents/job_verifier.md +71 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/cli/daemon.py +23 -6
- tsugite_cli-0.17.2/tsugite/core/record_store.py +316 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/providers/anthropic.py +48 -28
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/providers/base.py +5 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/providers/openai_compat.py +28 -57
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/tools/http.py +37 -9
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/tools/jobs.py +7 -1
- tsugite_cli-0.17.0/tsugite/builtin_agents/job_verifier.md +0 -58
- tsugite_cli-0.17.0/tsugite/core/record_store.py +0 -146
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/.gitignore +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/AGENTS.md +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/CONTRIBUTING.md +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/LICENSE +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/README.md +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/__init__.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/agent_inheritance.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/agent_preparation.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/agent_runner/__init__.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/agent_runner/exec_directives.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/agent_runner/exec_runner.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/agent_runner/helpers.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/agent_runner/history_integration.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/agent_runner/metrics.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/agent_runner/models.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/agent_runner/runner.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/agent_runner/validation.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/agent_utils.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/attachments/__init__.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/attachments/auto_context.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/attachments/base.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/attachments/file.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/attachments/inline.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/attachments/storage.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/attachments/url.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/builtin_agents/.gitkeep +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/builtin_agents/code_searcher.md +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/builtin_agents/default.md +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/builtin_agents/file_searcher.md +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/builtin_agents/job_worker.md +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/builtin_agents/onboard.md +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/builtin_skills/.gitkeep +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/builtin_skills/codebase-exploration/SKILL.md +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/builtin_skills/external-agent-bridge/SKILL.md +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/builtin_skills/python-math/SKILL.md +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/builtin_skills/response-patterns/SKILL.md +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/builtin_skills/scheduling/SKILL.md +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/builtin_skills/skill-authoring/SKILL.md +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/builtin_skills/tsugite-agent-basics/SKILL.md +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/builtin_skills/tsugite-jinja-reference/SKILL.md +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/cache.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/cancellation.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/cli/__init__.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/cli/agents.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/cli/attachments.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/cli/cache.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/cli/chat.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/cli/config.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/cli/exec.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/cli/helpers.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/cli/history.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/cli/init.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/cli/models.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/cli/plugins.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/cli/render.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/cli/run.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/cli/secrets.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/cli/skills.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/cli/tools.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/cli/usage.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/cli/validate.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/cli/workspace.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/config.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/console.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/constants.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/core/__init__.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/core/agent.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/core/content_blocks.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/core/executor.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/core/executor_registry.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/core/memory.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/core/proxy.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/core/sandbox.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/core/state.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/core/subprocess_executor.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/core/tools.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/events/__init__.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/events/base.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/events/bus.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/events/events.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/events/helpers.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/exceptions.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/history/__init__.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/history/base.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/history/models.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/history/reconstruction.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/history/registry.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/history/sqlite_backend.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/history/sqlite_conn.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/history/sqlite_schema.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/history/storage.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/history/ui_events.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/hooks.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/interaction.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/md_agents.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/models.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/options.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/plugins.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/providers/__init__.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/providers/model_cache.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/providers/model_registry.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/providers/ollama.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/providers/openrouter.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/py.typed +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/renderer.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/schemas/__init__.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/schemas/agent.schema.json +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/secrets/__init__.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/secrets/backend.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/secrets/env.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/secrets/exec.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/secrets/file.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/secrets/masking.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/secrets/registry.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/secrets/sqlite.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/shell_tool_config.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/skill_discovery.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/templates/AGENTS.md +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/templates/IDENTITY.md +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/templates/MEMORY.md +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/templates/USER.md +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/templates/personas/casual-technical.md +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/templates/personas/marvin.md +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/templates/personas/minimal.md +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/tools/__init__.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/tools/agents.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/tools/fs.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/tools/history.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/tools/interactive.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/tools/notify.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/tools/schedule.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/tools/scratchpad.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/tools/secrets.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/tools/sessions.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/tools/shell.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/tools/shell_tools.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/tools/skills.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/tools/time.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/tsugite.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/ui/__init__.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/ui/base.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/ui/chat.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/ui/helpers.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/ui/jsonl.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/ui/plain.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/ui/repl_chat.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/ui/repl_commands.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/ui/repl_completer.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/ui/repl_handler.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/ui_context.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/usage/__init__.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/usage/store.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/user_agent.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/utils.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/workspace/__init__.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/workspace/context.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/workspace/models.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/workspace/session.py +0 -0
- {tsugite_cli-0.17.0 → tsugite_cli-0.17.2}/tsugite/workspace/templates.py +0 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: job_verifier
|
|
3
|
+
description: Reasoning-blind verifier — judges a Job's worker output against its acceptance criteria, inspecting workspace file artifacts before deciding. Returns strict JSON. Spawned fresh per round with no parent context.
|
|
4
|
+
extends: none
|
|
5
|
+
max_turns: 10
|
|
6
|
+
tools: [read_file, run]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Job Verifier
|
|
10
|
+
|
|
11
|
+
You are a reasoning-blind verifier. You receive a list of acceptance criteria
|
|
12
|
+
and a work-output blob (the worker's structured summary, optionally with a
|
|
13
|
+
`git diff`). You run in the same working directory the worker ran in, so the
|
|
14
|
+
files it produced are directly inspectable.
|
|
15
|
+
|
|
16
|
+
You do not see the worker's reasoning. You do not see the parent chat. Claims
|
|
17
|
+
are not evidence: judge each criterion against the visible artifacts, or by
|
|
18
|
+
inspecting the workspace yourself.
|
|
19
|
+
|
|
20
|
+
## Task
|
|
21
|
+
|
|
22
|
+
{{ user_prompt }}
|
|
23
|
+
|
|
24
|
+
## How to verify
|
|
25
|
+
|
|
26
|
+
- Read the worker's `## Summary`, `## Output` (if present), `## Acceptance
|
|
27
|
+
criteria`, and `## Artifacts` sections.
|
|
28
|
+
- When the deliverable is inline text (poem, snippet, written answer), it
|
|
29
|
+
lives in `## Output`. Judge ACs about the content (length, format, contains
|
|
30
|
+
word X, syllable count, etc.) against the verbatim Output text.
|
|
31
|
+
- When a criterion concerns a file's existence, contents, or structure, read
|
|
32
|
+
the actual file before deciding: `read_file(path=...)` resolves against your
|
|
33
|
+
working directory; use `run` for listings or diffs. Only fail such a
|
|
34
|
+
criterion after inspection confirms the miss or the file is genuinely
|
|
35
|
+
absent/unreadable - never just because the worker's summary doesn't inline
|
|
36
|
+
the contents.
|
|
37
|
+
- If a PR URL or commit SHA is provided, you may inspect it via `run` (e.g.
|
|
38
|
+
`git show <sha>`, `gh pr view <url>`) when that materially affects the
|
|
39
|
+
verdict.
|
|
40
|
+
- Do not run long-running commands or perform setup; you have a turn budget.
|
|
41
|
+
- Be skeptical, not pedantic. A criterion like "tests pass" is met if the
|
|
42
|
+
worker reports a passing test run; you don't need to re-run unless evidence
|
|
43
|
+
is contradictory.
|
|
44
|
+
|
|
45
|
+
## Tool use
|
|
46
|
+
|
|
47
|
+
To inspect, reply with a single ```python code block calling a tool:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
read_file(path="docs/page.md")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The result comes back as an observation; keep inspecting or emit the verdict.
|
|
54
|
+
Never mix the final verdict into a tool-call turn.
|
|
55
|
+
|
|
56
|
+
## Output contract
|
|
57
|
+
|
|
58
|
+
When you have enough evidence, reply with ONLY a strictly-valid JSON object as
|
|
59
|
+
plain text - no code block, no markdown fences, no prose before or after:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
{
|
|
63
|
+
"ac_results": [
|
|
64
|
+
{"ac_text": "<verbatim AC>", "pass": true|false, "reason": "<one sentence>"}
|
|
65
|
+
],
|
|
66
|
+
"overall_pass": true|false
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Include one entry per acceptance criterion, in the given order. `overall_pass`
|
|
71
|
+
is `true` only if every `ac_results[i].pass` is `true`.
|
|
@@ -716,20 +716,36 @@ token_app = typer.Typer(help="Manage API authentication tokens")
|
|
|
716
716
|
daemon_app.add_typer(token_app, name="token")
|
|
717
717
|
|
|
718
718
|
|
|
719
|
-
def _get_tokens_path() -> Path:
|
|
719
|
+
def _get_tokens_path(config_path: Optional[Path] = None) -> Path:
|
|
720
|
+
"""Resolve the daemon's token storage from its config so the CLI writes
|
|
721
|
+
where the daemon reads. Previously the CLI always used the XDG default,
|
|
722
|
+
so a daemon with a custom `state_dir` never saw CLI-created tokens.
|
|
723
|
+
Falls back to the default state dir when no daemon config exists yet.
|
|
724
|
+
"""
|
|
720
725
|
from tsugite_daemon.auth import TOKENS_FILENAME
|
|
726
|
+
from tsugite_daemon.config import load_daemon_config
|
|
727
|
+
|
|
728
|
+
try:
|
|
729
|
+
state_dir = load_daemon_config(config_path).state_dir
|
|
730
|
+
except ValueError:
|
|
731
|
+
state_dir = get_xdg_data_path("daemon")
|
|
732
|
+
return state_dir / TOKENS_FILENAME
|
|
733
|
+
|
|
721
734
|
|
|
722
|
-
|
|
735
|
+
_TOKEN_CONFIG_OPTION = typer.Option(
|
|
736
|
+
None, "--config", "-c", help="Daemon config to resolve state_dir from (default: ~/.config/tsugite/daemon.yaml)"
|
|
737
|
+
)
|
|
723
738
|
|
|
724
739
|
|
|
725
740
|
@token_app.command("create")
|
|
726
741
|
def token_create(
|
|
727
742
|
name: str = typer.Option("", "--name", "-n", help="Name for the token (for identification)"),
|
|
743
|
+
config: Optional[Path] = _TOKEN_CONFIG_OPTION,
|
|
728
744
|
):
|
|
729
745
|
"""Create a new admin API token."""
|
|
730
746
|
from tsugite_daemon.auth import TokenStore
|
|
731
747
|
|
|
732
|
-
store = TokenStore(_get_tokens_path())
|
|
748
|
+
store = TokenStore(_get_tokens_path(config))
|
|
733
749
|
t, raw = store.create_admin_token(name=name)
|
|
734
750
|
print(raw)
|
|
735
751
|
err = Console(stderr=True)
|
|
@@ -740,11 +756,11 @@ def token_create(
|
|
|
740
756
|
|
|
741
757
|
|
|
742
758
|
@token_app.command("list")
|
|
743
|
-
def token_list():
|
|
759
|
+
def token_list(config: Optional[Path] = _TOKEN_CONFIG_OPTION):
|
|
744
760
|
"""List all admin API tokens."""
|
|
745
761
|
from tsugite_daemon.auth import TokenStore
|
|
746
762
|
|
|
747
|
-
store = TokenStore(_get_tokens_path())
|
|
763
|
+
store = TokenStore(_get_tokens_path(config))
|
|
748
764
|
tokens = store.list_admin_tokens()
|
|
749
765
|
if not tokens:
|
|
750
766
|
console.print("[dim]No tokens configured.[/dim]")
|
|
@@ -757,11 +773,12 @@ def token_list():
|
|
|
757
773
|
@token_app.command("revoke")
|
|
758
774
|
def token_revoke(
|
|
759
775
|
name_or_prefix: str = typer.Argument(help="Token name or prefix to revoke"),
|
|
776
|
+
config: Optional[Path] = _TOKEN_CONFIG_OPTION,
|
|
760
777
|
):
|
|
761
778
|
"""Revoke an admin API token."""
|
|
762
779
|
from tsugite_daemon.auth import TokenStore
|
|
763
780
|
|
|
764
|
-
store = TokenStore(_get_tokens_path())
|
|
781
|
+
store = TokenStore(_get_tokens_path(config))
|
|
765
782
|
if store.revoke_admin_token(name_or_prefix):
|
|
766
783
|
console.print(f"[green]Token '{name_or_prefix}' revoked.[/green]")
|
|
767
784
|
else:
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""Shared base for the daemon's record stores.
|
|
2
|
+
|
|
3
|
+
One copy of the locking, persistence, and guarded state-machine logic that
|
|
4
|
+
JobStore and TerminalSessionStore both need - the two had already started to
|
|
5
|
+
drift (one locked its read accessors, the other didn't).
|
|
6
|
+
|
|
7
|
+
Persistence is a per-collection table in ``<dir>/daemon.db`` (SQLite, WAL,
|
|
8
|
+
write-through): each mutation upserts or deletes exactly the affected row, so
|
|
9
|
+
nothing is lost on a crash and a large store isn't re-serialized wholesale per
|
|
10
|
+
save. The legacy per-store ``*.json`` file remains the constructor argument:
|
|
11
|
+
it is read once as a migration source when the db collection is empty, and
|
|
12
|
+
left untouched afterwards as a backup.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import sqlite3
|
|
22
|
+
import threading
|
|
23
|
+
from dataclasses import asdict
|
|
24
|
+
from datetime import datetime, timezone
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
DAEMON_DB_FILENAME = "daemon.db"
|
|
30
|
+
|
|
31
|
+
_BUSY_TIMEOUT_MS = 5000
|
|
32
|
+
_COLLECTION_NAME_RE = re.compile(r"^[a-z_][a-z0-9_]*$")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def now_iso() -> str:
|
|
36
|
+
return datetime.now(timezone.utc).isoformat()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SqliteCollectionStorage:
|
|
40
|
+
"""One collection (table) of ``id -> JSON document`` rows in a shared SQLite db.
|
|
41
|
+
|
|
42
|
+
Rows are opaque JSON documents rather than per-field columns so dataclass
|
|
43
|
+
schema growth never needs a db migration; queries stay in the stores'
|
|
44
|
+
in-memory dicts. The connection is NOT internally synchronized - callers
|
|
45
|
+
serialize access with their own lock (every store method already runs
|
|
46
|
+
under one).
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, db_path: Path, collection: str):
|
|
50
|
+
if not _COLLECTION_NAME_RE.match(collection):
|
|
51
|
+
raise ValueError(f"Invalid collection name: {collection!r}")
|
|
52
|
+
# Reject non-paths outright: a test's MagicMock would otherwise
|
|
53
|
+
# materialize junk directories/db files named after the mock's repr in
|
|
54
|
+
# the CWD (Path(mock) "works" via the mock's auto-generated __fspath__).
|
|
55
|
+
if not isinstance(db_path, (str, Path)):
|
|
56
|
+
raise TypeError(f"db_path must be a Path, got {type(db_path).__name__}")
|
|
57
|
+
db_path = Path(db_path)
|
|
58
|
+
self._collection = collection
|
|
59
|
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
self._conn = sqlite3.connect(str(db_path), check_same_thread=False, isolation_level=None)
|
|
61
|
+
self._conn.execute("PRAGMA journal_mode=WAL")
|
|
62
|
+
self._conn.execute(f"PRAGMA busy_timeout={_BUSY_TIMEOUT_MS}")
|
|
63
|
+
self._conn.execute("PRAGMA synchronous=NORMAL")
|
|
64
|
+
self._conn.execute(f'CREATE TABLE IF NOT EXISTS "{collection}" (id TEXT PRIMARY KEY, data TEXT NOT NULL)')
|
|
65
|
+
# Tracks which collections have completed their one-time legacy-JSON
|
|
66
|
+
# import. Row-count can't stand in for this: a collection legitimately
|
|
67
|
+
# emptied at runtime must NOT re-import the stale legacy file on the
|
|
68
|
+
# next start.
|
|
69
|
+
self._conn.execute('CREATE TABLE IF NOT EXISTS "_migrated" (collection TEXT PRIMARY KEY, at TEXT NOT NULL)')
|
|
70
|
+
# Owner-only, like tokens.json was: the db holds session content and
|
|
71
|
+
# auth token hashes. SQLite propagates the mode to -wal/-shm siblings.
|
|
72
|
+
try:
|
|
73
|
+
os.chmod(db_path, 0o600)
|
|
74
|
+
except OSError:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def for_state_file(cls, legacy_path: Path, collection: str) -> "SqliteCollectionStorage":
|
|
79
|
+
"""Storage for a store whose legacy JSON lived at ``legacy_path``:
|
|
80
|
+
the shared daemon.db sits next to it."""
|
|
81
|
+
return cls(legacy_path.parent / DAEMON_DB_FILENAME, collection)
|
|
82
|
+
|
|
83
|
+
def load_or_migrate(self, legacy_path: Path, collection_key: str) -> tuple[list[dict], bool]:
|
|
84
|
+
"""Entries from the db, falling back to a one-time legacy JSON import.
|
|
85
|
+
|
|
86
|
+
The import runs at most once per collection (tracked in the _migrated
|
|
87
|
+
table), so a collection emptied at runtime stays empty across restarts
|
|
88
|
+
instead of resurrecting the stale legacy file.
|
|
89
|
+
|
|
90
|
+
Returns (entries, migrating). When migrating is True the caller must
|
|
91
|
+
persist its parsed records back (replace_all / _save) so the import
|
|
92
|
+
becomes durable; the legacy file is left untouched as a backup.
|
|
93
|
+
"""
|
|
94
|
+
entries = self.load_all()
|
|
95
|
+
if entries or self._is_migrated():
|
|
96
|
+
self._mark_migrated() # stamp dbs created before the marker existed
|
|
97
|
+
return entries, False
|
|
98
|
+
entries = load_legacy_json_entries(legacy_path, collection_key)
|
|
99
|
+
if entries:
|
|
100
|
+
logger.info(
|
|
101
|
+
"Migrating %d %s record(s) from %s into daemon.db (legacy file kept as backup)",
|
|
102
|
+
len(entries),
|
|
103
|
+
collection_key,
|
|
104
|
+
legacy_path.name,
|
|
105
|
+
)
|
|
106
|
+
self._mark_migrated()
|
|
107
|
+
return entries, bool(entries)
|
|
108
|
+
|
|
109
|
+
def _is_migrated(self) -> bool:
|
|
110
|
+
row = self._conn.execute('SELECT 1 FROM "_migrated" WHERE collection = ?', (self._collection,)).fetchone()
|
|
111
|
+
return row is not None
|
|
112
|
+
|
|
113
|
+
def _mark_migrated(self) -> None:
|
|
114
|
+
self._conn.execute(
|
|
115
|
+
'INSERT OR IGNORE INTO "_migrated" (collection, at) VALUES (?, ?)',
|
|
116
|
+
(self._collection, datetime.now(timezone.utc).isoformat()),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def exists_any(self) -> bool:
|
|
120
|
+
"""True when the collection has at least one row (no full scan)."""
|
|
121
|
+
return self._conn.execute(f'SELECT 1 FROM "{self._collection}" LIMIT 1').fetchone() is not None
|
|
122
|
+
|
|
123
|
+
def get(self, record_id: str) -> dict | None:
|
|
124
|
+
row = self._conn.execute(f'SELECT data FROM "{self._collection}" WHERE id = ?', (record_id,)).fetchone()
|
|
125
|
+
if row is None:
|
|
126
|
+
return None
|
|
127
|
+
try:
|
|
128
|
+
return json.loads(row[0])
|
|
129
|
+
except json.JSONDecodeError as e:
|
|
130
|
+
logger.error("Corrupt %s row %s: %s", self._collection, record_id, e)
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
def load_all(self) -> list[dict]:
|
|
134
|
+
rows = self._conn.execute(f'SELECT data FROM "{self._collection}"').fetchall()
|
|
135
|
+
entries = []
|
|
136
|
+
for (data,) in rows:
|
|
137
|
+
try:
|
|
138
|
+
entries.append(json.loads(data))
|
|
139
|
+
except json.JSONDecodeError as e:
|
|
140
|
+
logger.error("Skipping corrupt %s row: %s", self._collection, e)
|
|
141
|
+
return entries
|
|
142
|
+
|
|
143
|
+
def upsert(self, record_id: str, entry: dict) -> None:
|
|
144
|
+
# default=str: a stray non-JSON value in a record field must degrade to
|
|
145
|
+
# its string form, not crash the mutation that carries it.
|
|
146
|
+
self._conn.execute(
|
|
147
|
+
f'INSERT INTO "{self._collection}" (id, data) VALUES (?, ?) '
|
|
148
|
+
"ON CONFLICT(id) DO UPDATE SET data = excluded.data",
|
|
149
|
+
(record_id, json.dumps(entry, default=str)),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def delete(self, record_id: str) -> None:
|
|
153
|
+
self._conn.execute(f'DELETE FROM "{self._collection}" WHERE id = ?', (record_id,))
|
|
154
|
+
|
|
155
|
+
def replace_all(self, entries: dict[str, dict]) -> None:
|
|
156
|
+
"""Atomically make the collection equal to ``entries``."""
|
|
157
|
+
self._conn.execute("BEGIN IMMEDIATE")
|
|
158
|
+
try:
|
|
159
|
+
self._conn.execute(f'DELETE FROM "{self._collection}"')
|
|
160
|
+
self._conn.executemany(
|
|
161
|
+
f'INSERT INTO "{self._collection}" (id, data) VALUES (?, ?)',
|
|
162
|
+
[(rid, json.dumps(entry, default=str)) for rid, entry in entries.items()],
|
|
163
|
+
)
|
|
164
|
+
self._conn.execute("COMMIT")
|
|
165
|
+
except Exception:
|
|
166
|
+
self._conn.execute("ROLLBACK")
|
|
167
|
+
raise
|
|
168
|
+
|
|
169
|
+
def close(self) -> None:
|
|
170
|
+
self._conn.close()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def load_legacy_json_entries(path: Path, collection_key: str) -> list[dict]:
|
|
174
|
+
"""Entries from a legacy whole-file JSON store, tolerating the list shape
|
|
175
|
+
(``{key: [...]}``), the dict shape (``{key: {id: {...}}}``), and the bare
|
|
176
|
+
top-level array some stores used (tokens.json, push_subscriptions.json).
|
|
177
|
+
Returns [] when the file is absent or unreadable."""
|
|
178
|
+
if not path.exists():
|
|
179
|
+
return []
|
|
180
|
+
try:
|
|
181
|
+
data = json.loads(path.read_text())
|
|
182
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
183
|
+
logger.error("Failed to load legacy %s from %s: %s", collection_key, path, e)
|
|
184
|
+
return []
|
|
185
|
+
raw = data.get(collection_key, []) if isinstance(data, dict) else data
|
|
186
|
+
if isinstance(raw, dict):
|
|
187
|
+
return list(raw.values())
|
|
188
|
+
if isinstance(raw, list):
|
|
189
|
+
return [e for e in raw if isinstance(e, dict)]
|
|
190
|
+
return []
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class RecordStore:
|
|
194
|
+
"""In-memory dict of dataclass records + write-through SQLite persistence.
|
|
195
|
+
|
|
196
|
+
Records must have: id, state, updated_at, resolved_at, parent_session_id.
|
|
197
|
+
Subclasses define the class attributes below and may override `_load_entry`
|
|
198
|
+
(per-entry migrations) and `_after_load` (post-load reconciliation; return
|
|
199
|
+
True to persist the result).
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
record_cls: type
|
|
203
|
+
collection_key: str # table name / legacy top-level JSON key, e.g. "jobs"
|
|
204
|
+
record_label: str # for error messages, e.g. "job"
|
|
205
|
+
valid_transitions: dict[str, frozenset[str]]
|
|
206
|
+
terminal_states: frozenset[str]
|
|
207
|
+
transition_error_cls: type[ValueError] = ValueError
|
|
208
|
+
|
|
209
|
+
def __init__(self, path: Path):
|
|
210
|
+
self._path = path # legacy JSON location; migration source only
|
|
211
|
+
self._storage = SqliteCollectionStorage.for_state_file(path, self.collection_key)
|
|
212
|
+
self._records: dict = {}
|
|
213
|
+
self._lock = threading.Lock()
|
|
214
|
+
self._load()
|
|
215
|
+
|
|
216
|
+
def _persist(self, record) -> None:
|
|
217
|
+
"""Write-through one record's row. Caller holds the lock."""
|
|
218
|
+
self._storage.upsert(record.id, asdict(record))
|
|
219
|
+
|
|
220
|
+
def add(self, record):
|
|
221
|
+
with self._lock:
|
|
222
|
+
if record.id in self._records:
|
|
223
|
+
raise ValueError(f"{self.record_cls.__name__} already exists: {record.id}")
|
|
224
|
+
self._records[record.id] = record
|
|
225
|
+
self._persist(record)
|
|
226
|
+
return record
|
|
227
|
+
|
|
228
|
+
def get(self, record_id: str):
|
|
229
|
+
with self._lock:
|
|
230
|
+
return self._records.get(record_id)
|
|
231
|
+
|
|
232
|
+
def list_all(self) -> list:
|
|
233
|
+
with self._lock:
|
|
234
|
+
return list(self._records.values())
|
|
235
|
+
|
|
236
|
+
def list_active(self) -> list:
|
|
237
|
+
with self._lock:
|
|
238
|
+
return [r for r in self._records.values() if r.state not in self.terminal_states]
|
|
239
|
+
|
|
240
|
+
def list_for_parent(self, parent_session_id: str) -> list:
|
|
241
|
+
with self._lock:
|
|
242
|
+
return [r for r in self._records.values() if r.parent_session_id == parent_session_id]
|
|
243
|
+
|
|
244
|
+
def update_state(self, record_id: str, new_state: str):
|
|
245
|
+
with self._lock:
|
|
246
|
+
record = self._records.get(record_id)
|
|
247
|
+
if record is None:
|
|
248
|
+
raise KeyError(f"Unknown {self.record_label}: {record_id}")
|
|
249
|
+
allowed = self.valid_transitions.get(record.state, frozenset())
|
|
250
|
+
if new_state not in allowed:
|
|
251
|
+
raise self.transition_error_cls(
|
|
252
|
+
f"Invalid {self.record_cls.__name__} state transition: "
|
|
253
|
+
f"{record.state} -> {new_state} ({self.record_label} {record_id})"
|
|
254
|
+
)
|
|
255
|
+
record.state = new_state
|
|
256
|
+
record.updated_at = now_iso()
|
|
257
|
+
if new_state in self.terminal_states and not record.resolved_at:
|
|
258
|
+
record.resolved_at = record.updated_at
|
|
259
|
+
self._on_state_updated(record)
|
|
260
|
+
self._persist(record)
|
|
261
|
+
return record
|
|
262
|
+
|
|
263
|
+
def _on_state_updated(self, record) -> None:
|
|
264
|
+
"""Hook called (under the lock) after a state transition, before persist.
|
|
265
|
+
Hooks that drop other records must go through `_delete_record` so the
|
|
266
|
+
deletion is durable."""
|
|
267
|
+
|
|
268
|
+
def _delete_record(self, record_id: str) -> None:
|
|
269
|
+
"""Remove a record from memory AND storage. Caller holds the lock."""
|
|
270
|
+
self._records.pop(record_id, None)
|
|
271
|
+
self._storage.delete(record_id)
|
|
272
|
+
|
|
273
|
+
def update(self, record_id: str, **fields):
|
|
274
|
+
with self._lock:
|
|
275
|
+
record = self._records.get(record_id)
|
|
276
|
+
if record is None:
|
|
277
|
+
raise KeyError(f"Unknown {self.record_label}: {record_id}")
|
|
278
|
+
for key, value in fields.items():
|
|
279
|
+
if not hasattr(record, key):
|
|
280
|
+
raise ValueError(f"Unknown {self.record_cls.__name__} field: {key}")
|
|
281
|
+
setattr(record, key, self._coerce_update_value(key, value))
|
|
282
|
+
record.updated_at = now_iso()
|
|
283
|
+
self._persist(record)
|
|
284
|
+
return record
|
|
285
|
+
|
|
286
|
+
def _coerce_update_value(self, key: str, value):
|
|
287
|
+
"""Hook for per-field validation/coercion in update()."""
|
|
288
|
+
return value
|
|
289
|
+
|
|
290
|
+
def _load_entry(self, entry: dict):
|
|
291
|
+
"""Build a record from a raw persisted entry; return None to drop it.
|
|
292
|
+
Subclasses apply schema migrations here."""
|
|
293
|
+
try:
|
|
294
|
+
return self.record_cls(**entry)
|
|
295
|
+
except TypeError as e:
|
|
296
|
+
logger.error("Skipping malformed %s record: %s (%s)", self.record_cls.__name__, entry.get("id"), e)
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
def _after_load(self) -> bool:
|
|
300
|
+
"""Post-load reconciliation hook. Return True to persist the result."""
|
|
301
|
+
return False
|
|
302
|
+
|
|
303
|
+
def _load(self) -> None:
|
|
304
|
+
entries, migrating = self._storage.load_or_migrate(self._path, self.collection_key)
|
|
305
|
+
for entry in entries:
|
|
306
|
+
record = self._load_entry(entry)
|
|
307
|
+
if record is not None:
|
|
308
|
+
self._records[record.id] = record
|
|
309
|
+
if self._after_load() or migrating:
|
|
310
|
+
self._storage.replace_all({r.id: asdict(r) for r in self._records.values()})
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# Deprecated alias - external plugins may still import the old name.
|
|
314
|
+
JsonRecordStore = RecordStore
|
|
315
|
+
|
|
316
|
+
__all__ = ["RecordStore", "JsonRecordStore", "SqliteCollectionStorage", "load_legacy_json_entries", "now_iso"]
|
|
@@ -35,27 +35,30 @@ _EFFORT_TO_BUDGET = {
|
|
|
35
35
|
# Auto-generated by scripts/update_model_registry.py — do not edit manually
|
|
36
36
|
# fmt: off
|
|
37
37
|
_ANTHROPIC_MODELS: dict[str, ModelInfo] = {
|
|
38
|
-
"anthropic/claude-3-
|
|
38
|
+
"anthropic/claude-3-5-sonnet-20240620": ModelInfo(max_input_tokens=200_000, max_output_tokens=8_192, input_cost_per_million=3.0, output_cost_per_million=15.0, supports_vision=True),
|
|
39
|
+
"anthropic/claude-3-5-sonnet-20241022": ModelInfo(max_input_tokens=200_000, max_output_tokens=8_192, input_cost_per_million=3.0, output_cost_per_million=15.0, supports_vision=True),
|
|
40
|
+
"anthropic/claude-3-7-sonnet-20250219": ModelInfo(max_input_tokens=200_000, max_output_tokens=64_000, input_cost_per_million=3.0, output_cost_per_million=15.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "max"], thinking_style="budget_tokens"),
|
|
39
41
|
"anthropic/claude-3-haiku-20240307": ModelInfo(max_input_tokens=200_000, max_output_tokens=4_096, input_cost_per_million=0.25, output_cost_per_million=1.25, supports_vision=True),
|
|
40
42
|
"anthropic/claude-3-opus-20240229": ModelInfo(max_input_tokens=200_000, max_output_tokens=4_096, input_cost_per_million=15.0, output_cost_per_million=75.0, supports_vision=True),
|
|
41
|
-
"anthropic/claude-
|
|
42
|
-
"anthropic/claude-
|
|
43
|
-
"anthropic/claude-haiku-4-5": ModelInfo(max_input_tokens=200_000, max_output_tokens=64_000, input_cost_per_million=1.0, output_cost_per_million=5.0, supports_vision=True, supported_effort_levels=["low", "medium", "high", "max"]),
|
|
44
|
-
"anthropic/claude-haiku-4-5-20251001": ModelInfo(max_input_tokens=200_000, max_output_tokens=64_000, input_cost_per_million=1.0, output_cost_per_million=5.0, supports_vision=True, supported_effort_levels=["low", "medium", "high", "max"]),
|
|
45
|
-
"anthropic/claude-opus-4-
|
|
46
|
-
"anthropic/claude-opus-4-1
|
|
47
|
-
"anthropic/claude-opus-4-
|
|
48
|
-
"anthropic/claude-opus-4-
|
|
49
|
-
"anthropic/claude-opus-4-5
|
|
50
|
-
"anthropic/claude-opus-4-
|
|
51
|
-
"anthropic/claude-opus-4-6
|
|
52
|
-
"anthropic/claude-opus-4-7": ModelInfo(max_input_tokens=1_000_000, max_output_tokens=128_000, input_cost_per_million=5.0, output_cost_per_million=25.0, supports_vision=True, supported_effort_levels=["low", "medium", "high", "max"]),
|
|
53
|
-
"anthropic/claude-opus-4-
|
|
54
|
-
"anthropic/claude-
|
|
55
|
-
"anthropic/claude-sonnet-4-20250514": ModelInfo(max_input_tokens=
|
|
56
|
-
"anthropic/claude-sonnet-4-5": ModelInfo(max_input_tokens=200_000, max_output_tokens=64_000, input_cost_per_million=3.0, output_cost_per_million=15.0, supports_vision=True, supported_effort_levels=["low", "medium", "high", "max"]),
|
|
57
|
-
"anthropic/claude-sonnet-4-5-20250929": ModelInfo(max_input_tokens=200_000, max_output_tokens=64_000, input_cost_per_million=3.0, output_cost_per_million=15.0, supports_vision=True, supported_effort_levels=["low", "medium", "high", "max"]),
|
|
58
|
-
"anthropic/claude-sonnet-4-6": ModelInfo(max_input_tokens=1_000_000, max_output_tokens=64_000, input_cost_per_million=3.0, output_cost_per_million=15.0, supports_vision=True, supported_effort_levels=["low", "medium", "high", "max"]),
|
|
43
|
+
"anthropic/claude-3-sonnet-20240229": ModelInfo(max_input_tokens=200_000, max_output_tokens=4_096, input_cost_per_million=3.0, output_cost_per_million=15.0, supports_vision=True),
|
|
44
|
+
"anthropic/claude-fable-5": ModelInfo(max_input_tokens=1_000_000, max_output_tokens=128_000, input_cost_per_million=10.0, output_cost_per_million=50.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "xhigh", "max"], thinking_style="adaptive"),
|
|
45
|
+
"anthropic/claude-haiku-4-5": ModelInfo(max_input_tokens=200_000, max_output_tokens=64_000, input_cost_per_million=1.0, output_cost_per_million=5.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "max"], thinking_style="budget_tokens"),
|
|
46
|
+
"anthropic/claude-haiku-4-5-20251001": ModelInfo(max_input_tokens=200_000, max_output_tokens=64_000, input_cost_per_million=1.0, output_cost_per_million=5.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "max"], thinking_style="budget_tokens"),
|
|
47
|
+
"anthropic/claude-opus-4-0": ModelInfo(max_input_tokens=200_000, max_output_tokens=32_000, input_cost_per_million=15.0, output_cost_per_million=75.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "max"], thinking_style="budget_tokens"),
|
|
48
|
+
"anthropic/claude-opus-4-1": ModelInfo(max_input_tokens=200_000, max_output_tokens=32_000, input_cost_per_million=15.0, output_cost_per_million=75.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "max"], thinking_style="budget_tokens"),
|
|
49
|
+
"anthropic/claude-opus-4-1-20250805": ModelInfo(max_input_tokens=200_000, max_output_tokens=32_000, input_cost_per_million=15.0, output_cost_per_million=75.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "max"], thinking_style="budget_tokens"),
|
|
50
|
+
"anthropic/claude-opus-4-20250514": ModelInfo(max_input_tokens=200_000, max_output_tokens=32_000, input_cost_per_million=15.0, output_cost_per_million=75.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "max"], thinking_style="budget_tokens"),
|
|
51
|
+
"anthropic/claude-opus-4-5": ModelInfo(max_input_tokens=200_000, max_output_tokens=64_000, input_cost_per_million=5.0, output_cost_per_million=25.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "max"], thinking_style="budget_tokens"),
|
|
52
|
+
"anthropic/claude-opus-4-5-20251101": ModelInfo(max_input_tokens=200_000, max_output_tokens=64_000, input_cost_per_million=5.0, output_cost_per_million=25.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "max"], thinking_style="budget_tokens"),
|
|
53
|
+
"anthropic/claude-opus-4-6": ModelInfo(max_input_tokens=1_000_000, max_output_tokens=128_000, input_cost_per_million=5.0, output_cost_per_million=25.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "max"], thinking_style="budget_tokens"),
|
|
54
|
+
"anthropic/claude-opus-4-7": ModelInfo(max_input_tokens=1_000_000, max_output_tokens=128_000, input_cost_per_million=5.0, output_cost_per_million=25.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "xhigh", "max"], thinking_style="adaptive"),
|
|
55
|
+
"anthropic/claude-opus-4-8": ModelInfo(max_input_tokens=1_000_000, max_output_tokens=128_000, input_cost_per_million=5.0, output_cost_per_million=25.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "xhigh", "max"], thinking_style="adaptive"),
|
|
56
|
+
"anthropic/claude-sonnet-4-0": ModelInfo(max_input_tokens=200_000, max_output_tokens=64_000, input_cost_per_million=3.0, output_cost_per_million=15.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "max"], thinking_style="budget_tokens"),
|
|
57
|
+
"anthropic/claude-sonnet-4-20250514": ModelInfo(max_input_tokens=200_000, max_output_tokens=64_000, input_cost_per_million=3.0, output_cost_per_million=15.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "max"], thinking_style="budget_tokens"),
|
|
58
|
+
"anthropic/claude-sonnet-4-5": ModelInfo(max_input_tokens=200_000, max_output_tokens=64_000, input_cost_per_million=3.0, output_cost_per_million=15.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "max"], thinking_style="budget_tokens"),
|
|
59
|
+
"anthropic/claude-sonnet-4-5-20250929": ModelInfo(max_input_tokens=200_000, max_output_tokens=64_000, input_cost_per_million=3.0, output_cost_per_million=15.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "max"], thinking_style="budget_tokens"),
|
|
60
|
+
"anthropic/claude-sonnet-4-6": ModelInfo(max_input_tokens=1_000_000, max_output_tokens=64_000, input_cost_per_million=3.0, output_cost_per_million=15.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "max"], thinking_style="budget_tokens"),
|
|
61
|
+
"anthropic/claude-sonnet-5": ModelInfo(max_input_tokens=1_000_000, max_output_tokens=128_000, input_cost_per_million=2.0, output_cost_per_million=10.0, supports_vision=True, supports_reasoning=True, supported_effort_levels=["low", "medium", "high", "xhigh", "max"], thinking_style="adaptive"),
|
|
59
62
|
}
|
|
60
63
|
# fmt: on
|
|
61
64
|
# --- END GENERATED MODELS (anthropic) --- #
|
|
@@ -114,20 +117,37 @@ class AnthropicProvider:
|
|
|
114
117
|
if system:
|
|
115
118
|
body["system"] = system
|
|
116
119
|
|
|
120
|
+
# The adaptive-thinking generation (Opus 4.7+, Sonnet 5, Fable 5)
|
|
121
|
+
# rejects budget_tokens thinking AND sampling params with 400; effort
|
|
122
|
+
# rides output_config there instead. Unknown models fall back to the
|
|
123
|
+
# budget_tokens path (pre-adaptive behavior).
|
|
124
|
+
info = self.get_model_info(model)
|
|
125
|
+
adaptive = info is not None and info.thinking_style == "adaptive"
|
|
126
|
+
|
|
117
127
|
for key in ("temperature", "top_p", "top_k", "stop_sequences", "metadata"):
|
|
118
|
-
if key in kwargs:
|
|
119
|
-
|
|
128
|
+
if key not in kwargs:
|
|
129
|
+
continue
|
|
130
|
+
if adaptive and key in ("temperature", "top_p", "top_k"):
|
|
131
|
+
logger.warning(
|
|
132
|
+
"Model %s rejects sampling param %s (adaptive-thinking generation); dropping", model, key
|
|
133
|
+
)
|
|
134
|
+
continue
|
|
135
|
+
body[key] = kwargs[key]
|
|
120
136
|
|
|
121
137
|
effort = kwargs.get("reasoning_effort")
|
|
122
138
|
if effort:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
139
|
+
if adaptive:
|
|
140
|
+
body["thinking"] = {"type": "adaptive"}
|
|
141
|
+
body["output_config"] = {"effort": effort}
|
|
126
142
|
else:
|
|
127
|
-
|
|
128
|
-
if
|
|
129
|
-
|
|
130
|
-
|
|
143
|
+
budget = _EFFORT_TO_BUDGET.get(effort)
|
|
144
|
+
if budget is None:
|
|
145
|
+
logger.warning("Unknown reasoning_effort=%r for Anthropic; dropping", effort)
|
|
146
|
+
else:
|
|
147
|
+
# Extended thinking requires max_tokens > budget_tokens.
|
|
148
|
+
if body["max_tokens"] <= budget:
|
|
149
|
+
body["max_tokens"] = budget + 1024
|
|
150
|
+
body["thinking"] = {"type": "enabled", "budget_tokens": budget}
|
|
131
151
|
|
|
132
152
|
url = f"{self.api_base}/v1/messages"
|
|
133
153
|
headers = self._build_headers()
|
|
@@ -49,6 +49,11 @@ class ModelInfo:
|
|
|
49
49
|
supports_reasoning: bool = False
|
|
50
50
|
supports_streaming: bool = True
|
|
51
51
|
supported_effort_levels: list[str] | None = None
|
|
52
|
+
# How the model exposes reasoning control: "budget_tokens" (extended
|
|
53
|
+
# thinking via thinking budgets) or "adaptive" (native effort parameter;
|
|
54
|
+
# budget_tokens and sampling params are rejected with 400). None when the
|
|
55
|
+
# model has neither or the style is unknown.
|
|
56
|
+
thinking_style: str | None = None
|
|
52
57
|
|
|
53
58
|
|
|
54
59
|
@runtime_checkable
|