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
13 changes: 13 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ agent:
# host_auth_file: null # default: $CODEX_HOME/auth.json or ~/.codex/auth.json
# auth_dir: ./.teich/codex-auth

# Trace each agent session to Langfuse (https://langfuse.com). Works for Codex
# and Claude Code -- each uses its own native integration (Codex plugin, Claude
# Code Stop hook) and Teich passes the credentials into the container. Side-
# channel only: it reads transcripts, fails open, and doesn't change agent
# behavior or Teich's output files. All three credentials are required when
# enabled. base_url may be Langfuse Cloud or a self-hosted URL; for a host-local
# instance use http://host.docker.internal:<port>.
# langfuse:
# enabled: true
# public_key: pk-lf-...
# secret_key: sk-lf-...
# base_url: https://cloud.langfuse.com

model:
# Model id passed to the selected agent/provider.
model: deepseek/deepseek-v4-flash
Expand Down
26 changes: 26 additions & 0 deletions docker/codex-runtime.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ RUN ln -sf /usr/bin/python3 /usr/local/bin/python && \
ln -sf /opt/venv/bin/pip3 /usr/local/bin/pip3
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
ENV VIRTUAL_ENV=/opt/venv
ARG TEICH_INSTALL_LANGFUSE=0

# Install Astral uv and npm-backed agent CLIs in one layer
# Use npm cache mount for faster installs
Expand All @@ -47,6 +48,31 @@ RUN --mount=type=cache,target=/root/.cache/uv \
chmod +x /usr/local/bin/hermes && \
(hermes --version || hermes --help >/dev/null)

# Bake Langfuse tooling only for the tracing-enabled runtime image. The default
# runtime image leaves this off so normal Teich runs do not depend on Langfuse
# plugin, pip, or git availability during Docker rebuilds.
RUN HOME=/opt/codex-langfuse CODEX_HOME=/opt/codex-langfuse/.codex \
sh -c 'mkdir -p "$CODEX_HOME" \
&& if [ "$TEICH_INSTALL_LANGFUSE" = "1" ]; then \
codex plugin marketplace add langfuse/codex-observability-plugin \
&& codex plugin add tracing@codex-observability-plugin; \
fi \
&& chmod -R a+rX /opt/codex-langfuse'

# Langfuse hook + SDK for Claude Code, pinned to a known-good commit so the
# baked hook script is reproducible. The SDK goes in the venv because Claude
# strips it from a hook's PATH, so the hook calls it by full path.
RUN if [ "$TEICH_INSTALL_LANGFUSE" = "1" ]; then \
/opt/venv/bin/pip install --no-cache-dir "langfuse>=4.0,<5" \
&& git clone https://github.com/langfuse/Claude-Observability-Plugin.git \
/opt/claude-langfuse-plugin \
&& git -C /opt/claude-langfuse-plugin checkout 597af67d6c6b369f3e55db6cfa2ebe444f1af46c \
&& rm -rf /opt/claude-langfuse-plugin/.git \
&& chmod -R a+rX /opt/claude-langfuse-plugin; \
else \
mkdir -p /opt/claude-langfuse-plugin; \
fi

# Create working directory and user in one layer
WORKDIR /workspace
RUN useradd -m -s /bin/bash codex && \
Expand Down
23 changes: 23 additions & 0 deletions src/teich/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -725,10 +725,20 @@ def generate(
"[yellow]Heads up: once the rotating token is refreshed, your host Codex login "
"will be invalidated — run `codex login` on the host afterward to restore it.[/yellow]"
)
if cfg.agent.langfuse.enabled:
console.print(
"[cyan]Codex Langfuse tracing enabled: uploading each session to "
f"{cfg.agent.langfuse.base_url}.[/cyan]"
)
elif agent_provider == "pi":
runner = PiRunner(cfg)
elif agent_provider in {"claude", "claude-code", "claude_code"}:
runner = ClaudeCodeRunner(cfg)
if cfg.agent.langfuse.enabled:
console.print(
"[cyan]Claude Code Langfuse tracing enabled: uploading each session to "
f"{cfg.agent.langfuse.base_url}.[/cyan]"
)
elif agent_provider in {"hermes", "hermes-agent", "hermes_agent"}:
runner = HermesRunner(cfg)
elif agent_provider == "chat":
Expand Down Expand Up @@ -1044,6 +1054,19 @@ def init(
# host_auth_file: null # default: $CODEX_HOME/auth.json or ~/.codex/auth.json
# auth_dir: ./.teich/codex-auth

# Trace each agent session to Langfuse (https://langfuse.com). Works for Codex
# and Claude Code -- each uses its own native integration (Codex plugin, Claude
# Code Stop hook) and Teich passes the credentials into the container. Side-
# channel only: it reads transcripts, fails open, and doesn't change agent
# behavior or Teich's output files. All three credentials are required when
# enabled. base_url may be Langfuse Cloud or a self-hosted URL; for a host-local
# instance use http://host.docker.internal:<port>.
# langfuse:
# enabled: true
# public_key: pk-lf-...
# secret_key: sk-lf-...
# base_url: https://cloud.langfuse.com

model:
# Model id passed to the selected agent/provider.
model: deepseek/deepseek-v4-flash
Expand Down
34 changes: 34 additions & 0 deletions src/teich/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,38 @@ class APIConfig(BaseModel):
wire_api: str = "responses"


class LangfuseConfig(BaseModel):
"""Langfuse tracing credentials, set under ``agent.langfuse``.

When enabled, Teich wires each agent's Langfuse integration (the Codex
plugin, the Claude Code Stop hook) and passes these credentials into the
container. Tracing is side-channel only: it reads transcripts, fails open,
and does not change agent behavior or Teich's output files.
"""
enabled: bool = False
public_key: str | None = None
secret_key: str | None = None
base_url: str | None = None

@model_validator(mode="after")
def require_credentials_when_enabled(self) -> LangfuseConfig:
if self.enabled:
missing = [
name
for name, value in (
("public_key", self.public_key),
("secret_key", self.secret_key),
("base_url", self.base_url),
)
if not (value and value.strip())
]
if missing:
raise ValueError(
"langfuse requires " + ", ".join(missing) + " when enabled"
)
return self


class CodexAuthConfig(BaseModel):
"""Codex ChatGPT-subscription auth handling.

Expand All @@ -119,6 +151,8 @@ class CodexAuthConfig(BaseModel):
class AgentConfig(BaseModel):
"""Agent runtime selection."""
provider: str = "codex"
# Langfuse tracing, applied to every agent that supports it (Codex, Claude).
langfuse: LangfuseConfig = Field(default_factory=LangfuseConfig)
codex: CodexAuthConfig = Field(default_factory=CodexAuthConfig)


Expand Down
Loading
Loading