Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions src/teich/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ def validate_claude_host_auth(self) -> Config:
so it is simply not used there (see ``claude_host_auth_active``).
"""
if (
self.agent.claude.oauth_token
_normalize_api_key(self.agent.claude.oauth_token)
and self.get_agent_provider() in CLAUDE_PROVIDER_ALIASES
and self.api.base_url
):
Expand Down Expand Up @@ -461,15 +461,13 @@ def get_claude_oauth_token(self) -> str | None:
Honors an explicit ``agent.claude.oauth_token``, then the
``TEICH_CLAUDE_OAUTH_TOKEN`` and ``CLAUDE_CODE_OAUTH_TOKEN`` env vars.
"""
token = self.agent.claude.oauth_token
if isinstance(token, str) and token.strip():
return token.strip()
if token := _normalize_api_key(self.agent.claude.oauth_token):
return token
return _get_env_alias(*CLAUDE_OAUTH_TOKEN_ENV_ALIASES)

def get_claude_oauth_token_source(self) -> str | None:
"""Name the source ``get_claude_oauth_token`` resolves from (for CLI messaging)."""
token = self.agent.claude.oauth_token
if isinstance(token, str) and token.strip():
if _normalize_api_key(self.agent.claude.oauth_token):
return "agent.claude.oauth_token"
for name in CLAUDE_OAUTH_TOKEN_ENV_ALIASES:
if _get_env_alias(name):
Expand Down
15 changes: 10 additions & 5 deletions src/teich/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -3407,6 +3407,15 @@ def _openrouter_proxy_shell_prefix(self) -> str:
)
return f"node {shlex.quote(proxy_script)} >/tmp/claude-openrouter-proxy.log 2>&1 & {readiness_probe}"

def _claude_passthrough_args(self) -> list[str]:
"""Build model-related flags shared by batch and Studio Claude sessions."""
args: list[str] = []
if self.config.model.reasoning_effort:
args.extend(["--effort", self.config.model.reasoning_effort])
if fallback_model := self.config.get_claude_fallback_model():
args.extend(["--fallback-model", fallback_model])
return args

def _build_shell_command(
self,
*,
Expand All @@ -3425,11 +3434,7 @@ def _build_shell_command(
permission_mode = self._permission_mode()
if permission_mode:
claude_command.extend(["--permission-mode", permission_mode])
if self.config.model.reasoning_effort:
claude_command.extend(["--effort", self.config.model.reasoning_effort])
fallback_model = self.config.get_claude_fallback_model()
if fallback_model:
claude_command.extend(["--fallback-model", fallback_model])
claude_command.extend(self._claude_passthrough_args())
if self.config.developer_instructions:
claude_command.extend(["--append-system-prompt", self.config.developer_instructions])
if resume_session_id:
Expand Down
1 change: 1 addition & 0 deletions src/teich/studio/interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ def _native_cli_command(self) -> list[str]:
permission_mode = runner._permission_mode()
if permission_mode:
cli.extend(["--permission-mode", permission_mode])
cli.extend(runner._claude_passthrough_args())
return cli
# hermes
return [
Expand Down
25 changes: 25 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,31 @@ def test_claude_oauth_token_absent_when_unset(monkeypatch):
assert Config().get_claude_oauth_token() is None


def test_claude_oauth_token_placeholder_is_treated_as_absent(monkeypatch):
monkeypatch.delenv("TEICH_CLAUDE_OAUTH_TOKEN", raising=False)
monkeypatch.delenv("CLAUDE_CODE_OAUTH_TOKEN", raising=False)
config = Config(
agent={"provider": "claude-code", "claude": {"oauth_token": " dummy "}},
api={"provider": "anthropic", "api_key": "sk-ant-test"},
)

assert config.get_claude_oauth_token() is None
assert config.get_claude_oauth_token_source() is None
assert config.claude_host_auth_active() is False
assert config.get_api_key() == "sk-ant-test"


def test_claude_oauth_token_placeholder_allows_base_url(monkeypatch):
monkeypatch.delenv("TEICH_CLAUDE_OAUTH_TOKEN", raising=False)
monkeypatch.delenv("CLAUDE_CODE_OAUTH_TOKEN", raising=False)
config = Config(
agent={"provider": "claude-code", "claude": {"oauth_token": "none"}},
api={"provider": "openrouter", "base_url": "https://openrouter.ai/api/v1"},
)

assert config.get_claude_oauth_token() is None


def test_claude_oauth_token_source_names_the_resolving_source(monkeypatch):
"""The CLI notice reports the source; it must track the getter's resolution order."""
monkeypatch.delenv("TEICH_CLAUDE_OAUTH_TOKEN", raising=False)
Expand Down
27 changes: 26 additions & 1 deletion tests/test_studio.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from teich.cli import _configure_studio_event_loop_policy
from teich.config import Config
from teich.extract import CURSOR_EXTRACTION_NOTICE
from teich.runner import SessionProgressUpdate
from teich.runner import ClaudeCodeRunner, SessionProgressUpdate
from teich.studio.events import summarize_chat_row, summarize_event, summarize_trace_events
from teich.studio.generation import RUNNER_CLASSES, GenerationJob
from teich.studio.interactive import InteractiveSession
Expand Down Expand Up @@ -889,6 +889,31 @@ def test_chat_session_discard_rejected_while_turn_running(tmp_path):
assert session.status == "running"


def test_claude_terminal_forwards_effort_and_fallback_model(tmp_path):
config = Config(
agent={"provider": "claude-code", "claude": {"fallback_model": ["sonnet", "haiku"]}},
model={"model": "claude-opus-4-8", "reasoning_effort": "high"},
output={"traces_dir": tmp_path / "output", "sandbox_dir": tmp_path / "sandbox"},
)
session = InteractiveSession(config)
with patch.object(ClaudeCodeRunner, "_ensure_image"):
session._runner = ClaudeCodeRunner(config)

command = session._native_cli_command()

assert command == [
"claude",
"--model",
"claude-opus-4-8",
"--permission-mode",
"bypassPermissions",
"--effort",
"high",
"--fallback-model",
"sonnet,haiku",
]


def test_generation_stop_prevents_later_prompt_starts(tmp_path, monkeypatch):
first_started = threading.Event()
stop_called = threading.Event()
Expand Down
Loading