tsugite-cli 0.25.1__tar.gz → 0.25.3__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.25.1 → tsugite_cli-0.25.3}/PKG-INFO +1 -1
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/pyproject.toml +1 -1
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/providers/anthropic.py +7 -5
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/providers/base.py +48 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/providers/openai_compat.py +8 -10
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/providers/openrouter.py +2 -3
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/secrets/sqlite.py +89 -13
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/.gitignore +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/AGENTS.md +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/CONTRIBUTING.md +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/LICENSE +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/README.md +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/__init__.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/agent_inheritance.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/agent_preparation.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/agent_runner/__init__.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/agent_runner/exec_directives.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/agent_runner/exec_runner.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/agent_runner/helpers.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/agent_runner/history_integration.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/agent_runner/metrics.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/agent_runner/models.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/agent_runner/runner.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/agent_runner/steps.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/agent_runner/validation.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/agent_utils.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/approval.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/attachments/__init__.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/attachments/agent_config.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/attachments/auto_context.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/attachments/base.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/attachments/delegation.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/attachments/file.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/attachments/inline.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/attachments/storage.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/attachments/url.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/builtin_agents/.gitkeep +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/builtin_agents/code_searcher.md +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/builtin_agents/default.md +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/builtin_agents/file_searcher.md +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/builtin_agents/job_verifier.md +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/builtin_agents/job_worker.md +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/builtin_agents/onboard.md +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/builtin_context.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/builtin_skills/.gitkeep +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/builtin_skills/codebase-exploration/SKILL.md +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/builtin_skills/external-agent-bridge/SKILL.md +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/builtin_skills/python-math/SKILL.md +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/builtin_skills/response-patterns/SKILL.md +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/builtin_skills/scheduling/SKILL.md +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/builtin_skills/skill-authoring/SKILL.md +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/builtin_skills/tsugite-agent-basics/SKILL.md +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/builtin_skills/tsugite-jinja-reference/SKILL.md +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/cache.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/cancellation.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/cli/__init__.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/cli/agents.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/cli/attachments.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/cli/cache.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/cli/chat.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/cli/config.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/cli/daemon.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/cli/exec.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/cli/helpers.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/cli/history.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/cli/init.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/cli/models.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/cli/plugins.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/cli/render.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/cli/run.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/cli/secrets.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/cli/skills.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/cli/tools.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/cli/usage.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/cli/validate.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/cli/workspace.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/config.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/console.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/constants.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/context.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/context_block.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/core/__init__.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/core/agent.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/core/content_blocks.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/core/executor.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/core/executor_registry.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/core/memory.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/core/proxy.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/core/record_store.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/core/sandbox.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/core/state.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/core/subprocess_executor.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/core/tools.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/daemon_client.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/events/__init__.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/events/base.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/events/bus.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/events/events.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/events/helpers.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/exceptions.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/history/__init__.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/history/base.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/history/models.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/history/reconstruction.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/history/registry.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/history/sqlite_backend.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/history/sqlite_conn.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/history/sqlite_schema.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/history/ui_events.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/hooks.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/interaction.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/md_agents.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/models.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/options.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/permissions.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/plugins.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/prompt_xml.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/providers/__init__.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/providers/model_cache.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/providers/model_registry.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/providers/ollama.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/py.typed +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/renderer.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/schemas/__init__.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/schemas/agent.schema.json +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/secrets/__init__.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/secrets/backend.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/secrets/env.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/secrets/exec.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/secrets/file.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/secrets/masking.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/secrets/redaction.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/secrets/registry.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/shell_tool_config.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/skill_discovery.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/templates/AGENTS.md +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/templates/IDENTITY.md +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/templates/MEMORY.md +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/templates/USER.md +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/templates/personas/casual-technical.md +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/templates/personas/marvin.md +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/templates/personas/minimal.md +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/tools/__init__.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/tools/agents.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/tools/artifacts.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/tools/daemon_control.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/tools/fs.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/tools/history.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/tools/http.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/tools/interactive.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/tools/jobs.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/tools/notify.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/tools/schedule.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/tools/secrets.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/tools/sessions.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/tools/shell.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/tools/shell_tools.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/tools/skills.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/tools/time.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/tsugite.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/ui/__init__.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/ui/base.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/ui/chat.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/ui/dispatch.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/ui/helpers.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/ui/jsonl.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/ui/plain.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/ui/repl_chat.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/ui/repl_commands.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/ui/repl_completer.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/ui/repl_handler.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/ui_context.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/ui_surfaces.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/usage/__init__.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/usage/quota.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/usage/store.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/user_agent.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/utils.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/workspace/__init__.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/workspace/models.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/workspace/session.py +0 -0
- {tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/workspace/templates.py +0 -0
|
@@ -5,7 +5,6 @@ from __future__ import annotations
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import json
|
|
7
7
|
import logging
|
|
8
|
-
import os
|
|
9
8
|
import re
|
|
10
9
|
from typing import Any, AsyncIterator
|
|
11
10
|
|
|
@@ -13,7 +12,7 @@ import httpx
|
|
|
13
12
|
|
|
14
13
|
from tsugite.user_agent import set_user_agent_header
|
|
15
14
|
|
|
16
|
-
from .base import CompletionResponse, ModelInfo, StreamChunk, Usage, default_count_tokens
|
|
15
|
+
from .base import CompletionResponse, ModelInfo, StreamChunk, Usage, default_count_tokens, resolve_api_key
|
|
17
16
|
from .model_registry import calculate_cost, register_models
|
|
18
17
|
from .model_registry import get_model_info as _get_model_info
|
|
19
18
|
|
|
@@ -63,9 +62,11 @@ _ANTHROPIC_MODELS: dict[str, ModelInfo] = {
|
|
|
63
62
|
class AnthropicProvider:
|
|
64
63
|
"""Provider for the Anthropic Messages API."""
|
|
65
64
|
|
|
65
|
+
api_key_env = "ANTHROPIC_API_KEY"
|
|
66
|
+
|
|
66
67
|
def __init__(self, name: str = "anthropic", api_key: str | None = None, api_base: str | None = None):
|
|
67
68
|
self.name = name
|
|
68
|
-
self.api_key = api_key
|
|
69
|
+
self.api_key = api_key
|
|
69
70
|
self.api_base = (api_base or API_BASE).rstrip("/")
|
|
70
71
|
self._client: httpx.AsyncClient | None = None
|
|
71
72
|
self._client_loop = None
|
|
@@ -92,8 +93,9 @@ class AnthropicProvider:
|
|
|
92
93
|
"anthropic-version": API_VERSION,
|
|
93
94
|
}
|
|
94
95
|
set_user_agent_header(headers)
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
api_key = self.api_key or resolve_api_key(self.api_key_env)
|
|
97
|
+
if api_key:
|
|
98
|
+
headers["x-api-key"] = api_key
|
|
97
99
|
return headers
|
|
98
100
|
|
|
99
101
|
async def acompletion(
|
|
@@ -2,11 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import time
|
|
5
9
|
from dataclasses import dataclass
|
|
6
10
|
from typing import Any, AsyncIterator, Protocol, runtime_checkable
|
|
7
11
|
|
|
8
12
|
_O200K_PREFIXES = ("gpt-4o", "o1", "o3", "o4", "chatgpt-4o")
|
|
9
13
|
_tiktoken_cache: dict[str, Any] = {}
|
|
14
|
+
API_KEY_CACHE_TTL_SECONDS = 60
|
|
15
|
+
_secret_api_keys: dict[str, tuple[float, str | None]] = {}
|
|
16
|
+
_backend_errors_logged: set[str] = set()
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
10
19
|
|
|
11
20
|
|
|
12
21
|
@dataclass
|
|
@@ -100,3 +109,42 @@ def default_count_tokens(text: str, model: str) -> int:
|
|
|
100
109
|
return len(_tiktoken_cache[encoding_name].encode(text))
|
|
101
110
|
except Exception:
|
|
102
111
|
return len(text) // 4
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def api_key_secret_name(env_var: str) -> str:
|
|
115
|
+
"""Secrets-backend name for the key read from ``env_var``."""
|
|
116
|
+
return env_var.lower().replace("_", "-")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def resolve_api_key(env_var: str) -> str | None:
|
|
120
|
+
"""Key from the secrets backend, else ``env_var``. Backend lookups are cached for ``API_KEY_CACHE_TTL_SECONDS``."""
|
|
121
|
+
now = time.monotonic()
|
|
122
|
+
cached = _secret_api_keys.get(env_var)
|
|
123
|
+
if cached is None or now - cached[0] >= API_KEY_CACHE_TTL_SECONDS:
|
|
124
|
+
from tsugite.secrets import get_backend
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
_secret_api_keys[env_var] = (now, get_backend().get(api_key_secret_name(env_var)))
|
|
128
|
+
except Exception as e:
|
|
129
|
+
if env_var not in _backend_errors_logged:
|
|
130
|
+
_backend_errors_logged.add(env_var)
|
|
131
|
+
logger.warning("Secrets backend failed reading the key for %s, using the env var: %s", env_var, e)
|
|
132
|
+
return os.getenv(env_var)
|
|
133
|
+
return _secret_api_keys[env_var][1] or os.getenv(env_var)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def invalidate_api_key_cache() -> None:
|
|
137
|
+
_secret_api_keys.clear()
|
|
138
|
+
_backend_errors_logged.clear()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def configured_binary(provider: str) -> str | None:
|
|
142
|
+
from tsugite.config import load_config
|
|
143
|
+
|
|
144
|
+
return load_config().plugins.get(provider, {}).get("binary") or None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def resolve_binary(provider: str, default: str) -> str | None:
|
|
148
|
+
"""Path of a CLI provider's executable from config ``plugins.<provider>.binary``, else ``default`` on PATH."""
|
|
149
|
+
configured = configured_binary(provider)
|
|
150
|
+
return shutil.which(os.path.expanduser(configured) if configured else default)
|
|
@@ -5,14 +5,13 @@ from __future__ import annotations
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import json
|
|
7
7
|
import logging
|
|
8
|
-
import os
|
|
9
8
|
from typing import Any, AsyncIterator
|
|
10
9
|
|
|
11
10
|
import httpx
|
|
12
11
|
|
|
13
12
|
from tsugite.user_agent import set_user_agent_header
|
|
14
13
|
|
|
15
|
-
from .base import CompletionResponse, ModelInfo, StreamChunk, Usage, default_count_tokens
|
|
14
|
+
from .base import CompletionResponse, ModelInfo, StreamChunk, Usage, default_count_tokens, resolve_api_key
|
|
16
15
|
from .model_registry import calculate_cost, register_models
|
|
17
16
|
from .model_registry import get_model_info as _get_model_info
|
|
18
17
|
|
|
@@ -122,10 +121,12 @@ class OpenAICompatProvider:
|
|
|
122
121
|
api_base: str,
|
|
123
122
|
api_key: str | None = None,
|
|
124
123
|
extra_headers: dict[str, str] | None = None,
|
|
124
|
+
api_key_env: str | None = None,
|
|
125
125
|
):
|
|
126
126
|
self.name = name
|
|
127
127
|
self.api_base = api_base.rstrip("/")
|
|
128
128
|
self.api_key = api_key
|
|
129
|
+
self.api_key_env = api_key_env
|
|
129
130
|
self.extra_headers = extra_headers or {}
|
|
130
131
|
self._client: httpx.AsyncClient | None = None
|
|
131
132
|
self._client_loop = None
|
|
@@ -148,8 +149,9 @@ class OpenAICompatProvider:
|
|
|
148
149
|
headers: dict[str, str] = {"Content-Type": "application/json"}
|
|
149
150
|
set_user_agent_header(headers)
|
|
150
151
|
headers.update(self.extra_headers)
|
|
151
|
-
if self.
|
|
152
|
-
|
|
152
|
+
api_key = self.api_key or (resolve_api_key(self.api_key_env) if self.api_key_env else None)
|
|
153
|
+
if api_key:
|
|
154
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
153
155
|
return headers
|
|
154
156
|
|
|
155
157
|
async def acompletion(
|
|
@@ -282,16 +284,12 @@ def create_provider(name: str = "openai", **kwargs: Any) -> OpenAICompatProvider
|
|
|
282
284
|
|
|
283
285
|
api_base = kwargs.get("api_base") or config.get("api_base") or "https://api.openai.com/v1"
|
|
284
286
|
|
|
285
|
-
api_key = kwargs.get("api_key")
|
|
286
|
-
if not api_key:
|
|
287
|
-
env_key = config.get("api_key_env")
|
|
288
|
-
api_key = os.getenv(env_key) if env_key else None
|
|
289
|
-
|
|
290
287
|
extra_headers = {**config.get("extra_headers", {}), **kwargs.get("extra_headers", {})}
|
|
291
288
|
|
|
292
289
|
return OpenAICompatProvider(
|
|
293
290
|
name=name,
|
|
294
291
|
api_base=api_base,
|
|
295
|
-
api_key=api_key,
|
|
292
|
+
api_key=kwargs.get("api_key"),
|
|
296
293
|
extra_headers=extra_headers,
|
|
294
|
+
api_key_env=config.get("api_key_env"),
|
|
297
295
|
)
|
|
@@ -59,11 +59,10 @@ class OpenRouterProvider(OpenAICompatProvider):
|
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
def create_provider(name: str = "openrouter", **kwargs: Any) -> OpenRouterProvider:
|
|
62
|
-
import os
|
|
63
|
-
|
|
64
62
|
return OpenRouterProvider(
|
|
65
63
|
name=name,
|
|
66
64
|
api_base=kwargs.get("api_base") or "https://openrouter.ai/api/v1",
|
|
67
|
-
api_key=kwargs.get("api_key")
|
|
65
|
+
api_key=kwargs.get("api_key"),
|
|
68
66
|
extra_headers=kwargs.get("extra_headers", {}),
|
|
67
|
+
api_key_env="OPENROUTER_API_KEY",
|
|
69
68
|
)
|
|
@@ -3,10 +3,18 @@
|
|
|
3
3
|
import base64
|
|
4
4
|
import getpass
|
|
5
5
|
import os
|
|
6
|
+
import secrets
|
|
7
|
+
import shutil
|
|
6
8
|
import sqlite3
|
|
9
|
+
import subprocess
|
|
7
10
|
import sys
|
|
11
|
+
import threading
|
|
8
12
|
from pathlib import Path
|
|
9
13
|
|
|
14
|
+
KEYCHAIN_SERVICE = "tsugite"
|
|
15
|
+
# `security find-generic-password` exit status for errSecItemNotFound.
|
|
16
|
+
MACOS_ITEM_NOT_FOUND = 44
|
|
17
|
+
|
|
10
18
|
|
|
11
19
|
class SqliteSecretBackend:
|
|
12
20
|
"""Stores secrets in SQLite, encrypted with Fernet. A passphrase is always required."""
|
|
@@ -18,14 +26,15 @@ class SqliteSecretBackend:
|
|
|
18
26
|
self._db_path = Path(path)
|
|
19
27
|
self._conn: sqlite3.Connection | None = None
|
|
20
28
|
self._key_cache: dict[bytes, bytes] = {}
|
|
21
|
-
self.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
)
|
|
25
|
-
|
|
29
|
+
self._keychain_account = (config.get("keychain_account") or "secrets") if config.get("keychain") else None
|
|
30
|
+
if self._keychain_account and any(c in self._keychain_account for c in '"\\\r\n'):
|
|
31
|
+
raise ValueError("keychain_account must not contain quotes, backslashes or line breaks")
|
|
32
|
+
self._keychain_lock = threading.Lock()
|
|
33
|
+
self._passphrase_value = self._resolve_passphrase(config)
|
|
34
|
+
if not self._passphrase_value and not self._keychain_account:
|
|
26
35
|
raise ValueError(
|
|
27
36
|
"sqlite secrets backend requires a passphrase. Set TSUGITE_SECRETS_KEY env var, "
|
|
28
|
-
"configure key_file in secrets config, or use an interactive terminal."
|
|
37
|
+
"configure key_file or keychain in secrets config, or use an interactive terminal."
|
|
29
38
|
)
|
|
30
39
|
|
|
31
40
|
def _get_conn(self) -> sqlite3.Connection:
|
|
@@ -40,25 +49,50 @@ class SqliteSecretBackend:
|
|
|
40
49
|
self._conn.commit()
|
|
41
50
|
return self._conn
|
|
42
51
|
|
|
43
|
-
|
|
44
|
-
def _resolve_passphrase(
|
|
45
|
-
config_passphrase: str | None = None,
|
|
46
|
-
key_file: str | None = None,
|
|
47
|
-
) -> str | None:
|
|
52
|
+
def _resolve_passphrase(self, config: dict) -> str | None:
|
|
48
53
|
env_key = os.environ.get("TSUGITE_SECRETS_KEY")
|
|
49
54
|
if env_key:
|
|
50
55
|
return env_key
|
|
56
|
+
key_file = config.get("key_file")
|
|
51
57
|
if key_file:
|
|
52
58
|
kf = Path(key_file).expanduser()
|
|
53
59
|
if kf.is_file():
|
|
54
60
|
return kf.read_text().strip()
|
|
55
|
-
if
|
|
56
|
-
return
|
|
61
|
+
if config.get("keychain"):
|
|
62
|
+
return None
|
|
63
|
+
if config.get("passphrase"):
|
|
64
|
+
return config["passphrase"]
|
|
57
65
|
if sys.stdin.isatty():
|
|
58
66
|
passphrase = getpass.getpass("Secrets passphrase: ")
|
|
59
67
|
return passphrase or None
|
|
60
68
|
return None
|
|
61
69
|
|
|
70
|
+
@property
|
|
71
|
+
def _passphrase(self) -> str:
|
|
72
|
+
with self._keychain_lock:
|
|
73
|
+
if self._passphrase_value is None:
|
|
74
|
+
self._passphrase_value = self._keychain_passphrase()
|
|
75
|
+
return self._passphrase_value
|
|
76
|
+
|
|
77
|
+
def _keychain_passphrase(self) -> str:
|
|
78
|
+
account = self._keychain_account
|
|
79
|
+
key = _keychain_lookup(account)
|
|
80
|
+
if key:
|
|
81
|
+
return key
|
|
82
|
+
if self._get_conn().execute("SELECT 1 FROM secrets LIMIT 1").fetchone():
|
|
83
|
+
raise ValueError(
|
|
84
|
+
f"The OS keychain has no secrets key (service '{KEYCHAIN_SERVICE}', account '{account}'), "
|
|
85
|
+
f"but {self._db_path} already holds encrypted secrets. Restore the key to the keychain "
|
|
86
|
+
"or set TSUGITE_SECRETS_KEY."
|
|
87
|
+
)
|
|
88
|
+
key = secrets.token_urlsafe(32)
|
|
89
|
+
_keychain_store(account, key)
|
|
90
|
+
if _keychain_lookup(account) != key:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"Stored the secrets key in the OS keychain (account '{account}') but could not read it back"
|
|
93
|
+
)
|
|
94
|
+
return key
|
|
95
|
+
|
|
62
96
|
def _derive_key(self, salt: bytes) -> bytes:
|
|
63
97
|
cached = self._key_cache.get(salt)
|
|
64
98
|
if cached:
|
|
@@ -119,3 +153,45 @@ class SqliteSecretBackend:
|
|
|
119
153
|
def list_names(self) -> list[str]:
|
|
120
154
|
rows = self._get_conn().execute("SELECT name FROM secrets ORDER BY name").fetchall()
|
|
121
155
|
return [r[0] for r in rows]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _keychain_tool() -> str:
|
|
159
|
+
if sys.platform == "darwin":
|
|
160
|
+
return "/usr/bin/security"
|
|
161
|
+
tool = shutil.which("secret-tool")
|
|
162
|
+
if not tool:
|
|
163
|
+
raise ValueError(
|
|
164
|
+
"secrets keychain is enabled but secret-tool was not found. Install libsecret-tools "
|
|
165
|
+
"and run a Secret Service (gnome-keyring, KeePassXC or KWallet)."
|
|
166
|
+
)
|
|
167
|
+
return tool
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _keychain_lookup(account: str) -> str | None:
|
|
171
|
+
tool = _keychain_tool()
|
|
172
|
+
if sys.platform == "darwin":
|
|
173
|
+
cmd = [tool, "find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", account, "-w"]
|
|
174
|
+
else:
|
|
175
|
+
cmd = [tool, "lookup", "service", KEYCHAIN_SERVICE, "account", account]
|
|
176
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
177
|
+
if result.returncode == 0:
|
|
178
|
+
return result.stdout.strip()
|
|
179
|
+
# secret-tool exits 1 with empty stderr for a missing item.
|
|
180
|
+
missing = result.returncode == MACOS_ITEM_NOT_FOUND if sys.platform == "darwin" else not result.stderr.strip()
|
|
181
|
+
if missing:
|
|
182
|
+
return None
|
|
183
|
+
raise ValueError(f"Could not read the secrets key from the OS keychain: {result.stderr.strip()}")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _keychain_store(account: str, key: str) -> None:
|
|
187
|
+
tool = _keychain_tool()
|
|
188
|
+
if sys.platform == "darwin":
|
|
189
|
+
# argv is visible to other local users through `ps`.
|
|
190
|
+
cmd = [tool, "-i"]
|
|
191
|
+
stdin = f'add-generic-password -s "{KEYCHAIN_SERVICE}" -a "{account}" -w "{key}"\n'
|
|
192
|
+
else:
|
|
193
|
+
cmd = [tool, "store", "--label=Tsugite secrets key", "service", KEYCHAIN_SERVICE, "account", account]
|
|
194
|
+
stdin = key
|
|
195
|
+
result = subprocess.run(cmd, input=stdin, capture_output=True, text=True)
|
|
196
|
+
if result.returncode != 0:
|
|
197
|
+
raise ValueError(f"Could not store the secrets key in the OS keychain: {result.stderr.strip()}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/builtin_skills/codebase-exploration/SKILL.md
RENAMED
|
File without changes
|
{tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/builtin_skills/external-agent-bridge/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/builtin_skills/tsugite-agent-basics/SKILL.md
RENAMED
|
File without changes
|
{tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/builtin_skills/tsugite-jinja-reference/SKILL.md
RENAMED
|
File without changes
|
{tsugite_cli-0.25.1 → tsugite_cli-0.25.3}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|