Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
run: uv python install ${{ matrix.python-version }}

- name: Run tests
run: uv run --python ${{ matrix.python-version }} --all-extras pytest --cov --cov-report=''
run: uv run --python ${{ matrix.python-version }} --all-extras --no-extra browseruse pytest --cov --cov-report=''

lint-ruff:
runs-on: ubuntu-latest
Expand Down
6 changes: 5 additions & 1 deletion docs/v6/reference/agents.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ agent = ClaudeAgent(ClaudeConfig(model="claude-sonnet-4-5", max_steps=30))
|-------|--------|---------------|
| `ClaudeAgent` | `ClaudeConfig` | `claude-sonnet-4-6` |
| `OpenAIAgent` | `OpenAIConfig` | `gpt-5.6` |
| `GeminiAgent` | `GeminiConfig` | `gemini-3-pro-preview` |
| `GeminiAgent` | `GeminiConfig` | `gemini-3.1-pro-preview` |
| `OpenAIChatAgent` | `OpenAIChatConfig` | `gpt-5.4-mini` |
| `ClaudeCLIAgent` | `ClaudeCLIConfig` | `claude-sonnet-5` |
| `CodexCLIAgent` | `CodexCLIConfig` | `gpt-5.6-sol` |
Expand All @@ -72,6 +72,10 @@ config; `__call__(run)` takes only the run.

`OpenAIConfig.max_output_tokens` and `GeminiConfig.max_output_tokens` are unset by default, so each request gets the model's own output limit. Set them to cap output per turn. Gemini counts thinking tokens toward the cap, so a low cap can truncate a thinking model before it answers.

`GeminiConfig.temperature`, `top_p`, and `top_k` are unset by default, so each request uses the model's own sampling; Gemini 4 models ignore them. `GeminiConfig.thinking_level` accepts `minimal`, `low`, `medium`, or `high`, subject to the model (Gemini 4 Argon rejects `minimal`); unset uses the model's default.

`GeminiAgent` streams each turn. For Gemini 4 long decode, set `max_output_tokens` above 65,536 and install `google-genai` 2.30.1 or later; the turn then runs past the per-request output cap and is recorded as one turn. Otherwise a turn that reaches the cap ends the run with `stop_reason="length"`. Keep `include_thoughts` on for long-thinking runs, since without it the model sends nothing while it thinks and a long think can hit the stream's first-chunk deadline.

`OpenAIConfig` generates a stable `prompt_cache_key` for the agent and sends it with every Responses
request. Pass your own key when multiple agent instances should share the same cached prompt prefix.

Expand Down
3 changes: 1 addition & 2 deletions hud/agents/claude/tools/hosted.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from typing import TYPE_CHECKING, Any

from anthropic.types.beta import (
BetaCitationsConfigParam,
BetaToolSearchToolBm25_20251119Param,
BetaToolUnionParam,
BetaWebFetchTool20250910Param,
Expand Down Expand Up @@ -75,7 +74,7 @@ def to_params(self) -> BetaWebFetchTool20250910Param:
if self.max_content_tokens is not None:
params["max_content_tokens"] = self.max_content_tokens
if self.citations_enabled:
params["citations"] = BetaCitationsConfigParam(enabled=True)
params["citations"] = {"enabled": True}
return params


Expand Down
45 changes: 31 additions & 14 deletions hud/agents/gemini/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,22 +201,40 @@ async def get_response(
thinking_config=thinking_config,
)

api_response = await self.gemini_client.aio.models.generate_content(
stream = await self.gemini_client.aio.models.generate_content_stream(
model=self.config.model,
contents=cast("Any", messages),
config=generate_config,
)
if not api_response.candidates:
# The turn keeps every streamed part as received: thought signatures belong to the
# parts they arrive on, including empty text parts.
parts: list[genai_types.Part] = []
candidate_seen = False
model_version: str | None = None
usage_meta: genai_types.GenerateContentResponseUsageMetadata | None = None
grounding_meta: genai_types.GroundingMetadata | None = None
finish_reason: genai_types.FinishReason | None = None
async for chunk in stream:
model_version = chunk.model_version or model_version
if chunk.usage_metadata is not None:
usage_meta = chunk.usage_metadata
if not chunk.candidates:
continue
candidate_seen = True
candidate = chunk.candidates[0]
if candidate.content is not None and candidate.content.parts:
parts.extend(candidate.content.parts)
if candidate.grounding_metadata is not None:
grounding_meta = candidate.grounding_metadata
if candidate.finish_reason is not None:
finish_reason = candidate.finish_reason
if not candidate_seen:
raise RuntimeError(f"Gemini returned no candidates for model {self.config.model}")

candidate = api_response.candidates[0]
content = candidate.content
if content is not None:
messages.append(content)
if parts:
messages.append(genai_types.Content(role="model", parts=parts))

result = AgentStep(content="", done=True)
result.model = api_response.model_version or self.config.model
usage_meta = api_response.usage_metadata
result.model = model_version or self.config.model
if usage_meta is not None:
result.usage = Usage(
prompt_tokens=usage_meta.prompt_token_count,
Expand All @@ -226,7 +244,7 @@ async def get_response(
text_parts: list[str] = []
thought_parts: list[str] = []

for part in (content.parts or []) if content else []:
for part in parts:
function_call = part.function_call
if function_call is not None:
tc = self._make_tool_call(function_call, computer_tool)
Expand All @@ -241,14 +259,13 @@ async def get_response(

result.content = "".join(text_parts)
if thought_parts:
result.reasoning = "\n".join(thought_parts)
result.reasoning = "".join(thought_parts)

grounding_meta = candidate.grounding_metadata
if grounding_meta is not None:
result.citations = _grounding_citations(grounding_meta)

if candidate.finish_reason is not None:
result.finish_reason = candidate.finish_reason.name
if finish_reason is not None:
result.finish_reason = finish_reason.name
if result.finish_reason == "MALFORMED_FUNCTION_CALL":
result.error = "Provider returned a malformed function call"
result.stop_reason = "malformed_tool_call"
Expand Down
4 changes: 2 additions & 2 deletions hud/agents/tests/test_claude_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,10 @@ def _state(agent: ClaudeAgent) -> Any:

def _inline_error(type_: str, *, headers: dict[str, str] | None = None) -> APIStatusError:
body = {"type": "error", "error": {"type": type_, "message": "stream failed"}}
response = httpx.Response(
response = httpx2.Response(
200,
headers=headers,
request=httpx.Request("POST", "https://api.anthropic.com/v1/messages"),
request=httpx2.Request("POST", "https://api.anthropic.com/v1/messages"),
)
return APIStatusError(str(body), response=response, body=body)

Expand Down
Loading
Loading