Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
9ad50a5
fix(client): forward model.parameters to every provider handler
apucacao Sep 24, 2026
beabd79
fix(client): filter model.parameters to what each provider actually a…
apucacao Sep 24, 2026
2e91885
refactor(client): replace runtime SDK introspection with explicit for…
apucacao Sep 24, 2026
d51199d
fix(langchain): never forward model_name or model_id from model.param…
apucacao Sep 24, 2026
8144a3d
Merge remote-tracking branch 'origin/main' into alexis/forward-model-…
apucacao Oct 2, 2026
c25d33b
refactor(client): stop exporting model_parameters and select_forwarde…
apucacao Oct 5, 2026
828279f
fix(claude-agents): forward only model and run settings from model.pa…
apucacao Oct 5, 2026
38d463a
fix(langchain): forward only model request settings to chat models
apucacao Oct 5, 2026
5a43f72
fix(claude-messages): forward the same keys on invoke and stream
apucacao Oct 5, 2026
5314cb5
fix(openai-messages): forward the same keys on invoke and stream
apucacao Oct 5, 2026
d681399
refactor(openai-agents): drop the dead max_turns pop
apucacao Oct 5, 2026
4a233d7
test: check every handler's forwarded lists against the never-forward…
apucacao Oct 5, 2026
fba3d67
fix: stop forwarding container state and request attribution
apucacao Oct 5, 2026
1a1539e
test(claude-agents): replace the extra_body check with the never-forw…
apucacao Oct 5, 2026
175050b
Merge remote-tracking branch 'origin/main' into alexis/forward-model-…
apucacao Oct 5, 2026
372c2ae
test: require a forwarded-keys entry for every discovered handler module
apucacao Oct 7, 2026
9055c2a
feat(client): drop malformed object values when selecting forwarded p…
apucacao Oct 7, 2026
a2a7d73
test: add the cross-SDK forwarded-key lists and a per-key probe
apucacao Oct 7, 2026
956315c
fix(claude-messages): forward exactly the cross-SDK Claude Messages list
apucacao Oct 7, 2026
fba0880
fix(openai-messages): forward exactly the cross-SDK OpenAI Messages list
apucacao Oct 7, 2026
4b03acd
fix(openai-agents): forward exactly the cross-SDK OpenAI Agents list
apucacao Oct 7, 2026
f9b1bf5
fix(claude-agents): drop thinking and output_format values that are n…
apucacao Oct 7, 2026
1c60bdf
fix(langchain): forward exactly the cross-SDK lists for each chat model
apucacao Oct 7, 2026
4e92432
test: add the newly excluded categories to the never-forwarded bag
apucacao Oct 7, 2026
8c1f9f7
fix(openai-messages): stop forwarding max_tool_calls
apucacao Oct 7, 2026
7cfb3a7
fix(langchain): rename max_tokens_to_sample to max_tokens for ChatAnt…
apucacao Oct 7, 2026
52bf2d2
fix(openai-agents): rebuild reasoning as Reasoning and let text.verbo…
apucacao Oct 7, 2026
c76acd5
fix(langchain): cut prompt_cache_key from the ChatOpenAI list
apucacao Oct 7, 2026
db839fe
Merge main into alexis/forward-model-parameters
apucacao Oct 9, 2026
2d52270
Merge main into alexis/forward-model-parameters
apucacao Oct 21, 2026
27ff2c4
test: let a forwarding probe override a sample value
apucacao Oct 21, 2026
3237589
test: add the LiteLLM connection and routing spellings to the never-f…
apucacao Oct 21, 2026
a125877
fix(litellm-messages): forward exactly the LiteLLM Messages list
apucacao Oct 21, 2026
b1438bb
fix(litellm-agents): forward exactly the LiteLLM Agents list
apucacao Oct 21, 2026
af122f9
fix(litellm-agents): apply the root node's max_turns to the native gr…
apucacao Oct 21, 2026
a9f986f
fix(vercel-messages): forward exactly what the Python ai library takes
apucacao Oct 21, 2026
fa5673d
fix(vercel-agents): forward exactly what the Python ai library takes
apucacao Oct 21, 2026
040055d
fix(opencode-agents): declare an empty forwarded-keys list
apucacao Oct 21, 2026
b1ed895
style: sort the parameter-forwarding imports
apucacao Oct 21, 2026
52ccc87
fix(openai-messages): forward max_tool_calls again
apucacao Oct 21, 2026
cf544b1
fix(langchain): forward seed to ChatOpenAI again
apucacao Oct 21, 2026
c61668c
fix(langchain): forward reasoning_effort to ChatOpenAI again
apucacao Oct 21, 2026
b7cecca
fix(langchain): forward stop sequences and reasoning_effort to ChatBe…
apucacao Oct 21, 2026
bfcd09b
docs(langchain): say prompt_cache_key is not supported in Python
apucacao Oct 21, 2026
172a7ac
test: describe how the forwarded-key lists differ between SDKs
apucacao Oct 21, 2026
0e6bdb2
test: fail a forwarding probe when setting a key makes the call raise
apucacao Oct 21, 2026
7a5f15e
fix(openai-agents): forward tool_choice only as a string
apucacao Oct 21, 2026
9e4a966
fix(claude-agents): drop a thinking setting the SDK cannot read
apucacao Oct 21, 2026
1ca49bd
fix(claude-messages): drop thinking, tool_choice and cache_control wi…
apucacao Oct 21, 2026
b3463a8
fix(openai-messages): keep only effort and summary in reasoning
apucacao Oct 21, 2026
34cf0d1
fix(openai-messages): drop a tool_choice that is neither a string nor…
apucacao Oct 21, 2026
fd6f9bc
fix(langchain): drop a ChatAnthropic thinking without a known type
apucacao Oct 21, 2026
985a349
test: add container state to the never-forwarded bag
apucacao Oct 21, 2026
78c3751
Merge main into alexis/forward-model-parameters
apucacao Oct 22, 2026
e2d65f9
fix(typesafe): declare that the TypeSafe handler forwards no model pa…
apucacao Oct 22, 2026
06317e3
fix(claude-messages): forward only effort from output_config
apucacao Oct 22, 2026
65e4cf3
fix(claude-agents): keep output_format out, the handler owns structur…
apucacao Oct 22, 2026
331caa8
Merge main into alexis/forward-model-parameters
apucacao Oct 22, 2026
defb53f
fix(agents): drop a reasoning the SDK's Reasoning type rejects
apucacao Oct 22, 2026
5031a8d
fix(langchain): rename ChatAnthropic stop to stop_sequences
apucacao Oct 22, 2026
3e11f4f
build: require the client release that ships parameter forwarding
apucacao Oct 23, 2026
0f2e422
Merge main into alexis/forward-model-parameters
apucacao Oct 23, 2026
718594e
build: require any client release after 0.4.0
apucacao Oct 23, 2026
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 packages/claude-agents/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "launchdarkly-ai-claude-agents"
version = "0.2.6"
requires-python = ">=3.12"
dependencies = [
"launchdarkly-ai-server>=0.3.1",
"launchdarkly-ai-server>=0.4.1",
"opentelemetry-api>=1.25",
"anthropic>=0.40",
"claude-agent-sdk>=0.2",
Expand Down
146 changes: 145 additions & 1 deletion packages/claude-agents/src/launchdarkly_ai_claude_agents/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import asyncio
import json
from collections.abc import AsyncGenerator, AsyncIterator, Callable
from collections.abc import AsyncGenerator, AsyncIterator, Callable, Mapping
from typing import Any

from claude_agent_sdk import (
Expand Down Expand Up @@ -53,6 +53,8 @@
)
from launchdarkly_ai_server.agui import TurnEmitter
from launchdarkly_ai_server.lifecycle import effective_capture_content
from launchdarkly_ai_server.parameter_forwarding import select_forwarded_parameters
from launchdarkly_ai_server.utils import model_parameters

from ._version import PACKAGE_NAME, __version__
from .spans import (
Expand All @@ -73,6 +75,97 @@
tool_display_name,
)

#: Every field ``ClaudeAgentOptions`` declares, classified by hand into exactly one of: forwarded
#: (below), handler-owned (``model``, ``allowed_tools``, ``mcp_servers``, ``hooks``, ``tools``,
#: ``system_prompt``, set by each call site itself), or excluded (below).
#: ``TestClaudeAgentOptionsAcceptsExactlyTheseFields`` in this package's tests asserts this
#: classification stays exhaustive as the SDK's own dataclass changes.
#:
#: Only model and run settings are forwarded: how the model thinks, how long the run may go, and
#: what it may spend. Everything that configures the host process the SDK launches (its binary,
#: environment, working directory, file access, permissions, settings files, plugins, sandbox,
#: session state) stays under the application's control, never a config's.
#:
#: This is the cross-SDK list for Claude Agents (TESTING.md section 1.12), shared with the native
#: graph through :func:`_options_parameters`.
#:
#: The SDK offers no ``temperature``/``top_p``/``top_k``/``max_tokens``/``stop_sequences``/
#: ``tool_choice``/``metadata``, all of which the LaunchDarkly UI's model parameters panel offers
#: for other providers; they are dropped like any other key not listed here.
_CLAUDE_AGENT_OPTIONS_FORWARDED_KEYS = frozenset(
{
"betas",
"effort",
"fallback_model",
"max_budget_usd",
"max_thinking_tokens",
"max_turns",
"thinking",
}
)

#: Forwarded keys whose value must be an object. A config value of any other type is malformed
#: and dropped rather than passed to ``ClaudeAgentOptions``.
_CLAUDE_AGENT_OPTIONS_MAPPING_KEYS = frozenset({"thinking"})

#: Accepted by ``ClaudeAgentOptions`` but never forwarded, and why:
#: * ``cli_path``, ``env``, ``cwd``, ``add_dirs``, ``settings``, ``setting_sources``, ``plugins``,
#: ``skills``, ``sandbox``, ``user``, ``extra_args``: which binary runs, with what environment,
#: as which user, with what files, settings, plugins, and CLI arguments. A config that could set
#: these could run code on, or read files from, the host.
#: * ``permission_mode``, ``permission_prompt_tool_name``, ``can_use_tool``, ``disallowed_tools``,
#: ``strict_mcp_config``, ``agents``: what the agent is allowed to do and which tools or
#: subagents it gets. The handler wires tools and permissions itself.
#: * ``resume``, ``session_id``, ``fork_session``, ``continue_conversation``, ``session_store``,
#: ``session_store_flush``, ``enable_file_checkpointing``: session state on the host.
#: * ``stderr``, ``debug_stderr``, ``include_partial_messages``, ``include_hook_events``,
#: ``max_buffer_size``, ``load_timeout_ms``: process I/O and transport plumbing, including the
#: streamed message shape the handler reads.
#: * ``output_format``: structured output, which the handler owns through the config's
#: ``outputFormat`` by adding its schema to the system prompt. Forwarding a second schema here
#: would have the reply follow one schema while the prompt asks for another.
#: * ``task_budget``: not one of the agreed run settings yet; ``max_turns`` and ``max_budget_usd``
#: cover run limits.
#:
#: Named for the drift test and for review, not read at runtime: the forwarded list above already
#: leaves these out, so nothing needs to subtract them again.
_CLAUDE_AGENT_OPTIONS_EXCLUDED_KEYS = frozenset(
{
"add_dirs",
"agents",
"can_use_tool",
"cli_path",
Comment thread
jeffdupont marked this conversation as resolved.
"continue_conversation",
"cwd",
"debug_stderr",
"disallowed_tools",
"enable_file_checkpointing",
"env",
"extra_args",
"fork_session",
"include_hook_events",
"include_partial_messages",
"load_timeout_ms",
"max_buffer_size",
"output_format",
"permission_mode",
"permission_prompt_tool_name",
"plugins",
"resume",
"sandbox",
"session_id",
"session_store",
"session_store_flush",
"setting_sources",
"settings",
"skills",
"stderr",
"strict_mcp_config",
"task_budget",
"user",
}
)
Comment thread
cursor[bot] marked this conversation as resolved.

# ---------------------------------------------------------------------------
# Tool wiring
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -504,6 +597,55 @@ def _raise_if_run_failed(message: Any) -> None:
)


#: The ``thinking`` types ``claude_agent_sdk`` knows (``ThinkingConfig``). It reads
#: ``thinking["type"]`` when it builds the CLI command, so a ``thinking`` without one raises.
_THINKING_TYPES = frozenset({"adaptive", "enabled", "disabled"})

#: The ``thinking.display`` values the SDK passes to the CLI (``ThinkingDisplay``).
_THINKING_DISPLAYS = frozenset({"summarized", "omitted"})


def _rebuild_thinking(value: Mapping[str, Any]) -> dict[str, Any] | None:
"""``thinking`` in the shape ``claude_agent_sdk`` reads, or ``None`` when it is malformed.

Keeps ``type``, ``budget_tokens`` and ``display`` only, the same sub-keys the JS SDK keeps.
The type must be one the SDK knows, and an ``enabled`` thinking must carry an integer
``budget_tokens``: the SDK reads ``thinking["budget_tokens"]`` for it straight away, so one
without a budget would raise. ``budgetTokens`` is read as well, as in the JS SDK. ``display``
is kept only for a type that takes it and only with a value the SDK knows.
"""
kind = value.get("type")
if kind not in _THINKING_TYPES:
return None
thinking: dict[str, Any] = {"type": kind}
if kind == "enabled":
budget = value.get("budget_tokens", value.get("budgetTokens"))
if not isinstance(budget, int) or isinstance(budget, bool):
return None
thinking["budget_tokens"] = budget
if kind != "disabled" and value.get("display") in _THINKING_DISPLAYS:
thinking["display"] = value["display"]
return thinking


def _options_parameters(config: AiConfigRep) -> dict[str, Any]:
"""The config's ``model.parameters`` that become ``ClaudeAgentOptions`` fields: only
:data:`_CLAUDE_AGENT_OPTIONS_FORWARDED_KEYS`, with malformed object values dropped and
``thinking`` rebuilt by :func:`_rebuild_thinking`. The handler and the native graph both call
this, so they forward the same keys.
"""
params = select_forwarded_parameters(
model_parameters(config),
_CLAUDE_AGENT_OPTIONS_FORWARDED_KEYS,
mapping_keys=_CLAUDE_AGENT_OPTIONS_MAPPING_KEYS,
)
if "thinking" in params:
thinking = _rebuild_thinking(params.pop("thinking"))
if thinking is not None:
params["thinking"] = thinking
return params


def _build_query_options(
config: AiConfigRep,
system_prompt: str | None,
Expand All @@ -514,7 +656,9 @@ def _build_query_options(
**extra: Any,
) -> ClaudeAgentOptions:
all_allowed = [*mcp_allowed_tools, *native_tool_names]
params = _options_parameters(config)
kwargs: dict[str, Any] = {
**params,
Comment thread
cursor[bot] marked this conversation as resolved.
"model": config["model"]["name"],
"allowed_tools": all_allowed if all_allowed else [],
"mcp_servers": {TOOL_MCP_NAME: tool_mcp} if tool_mcp else {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@

from launchdarkly_ai_claude_agents.handler import (
_build_hooks,
_options_parameters,
_raise_if_run_failed,
build_prompt,
build_query_prompt,
Expand Down Expand Up @@ -158,7 +159,10 @@ async def _run_query(

hooks = _build_hooks(native_tool_map)

params = _options_parameters(node.config)

options = ClaudeAgentOptions(
**params,
# Explicitly set the available built-in tools (empty list disables all).
# When no native tools are needed, disable built-in tools so Claude
# cannot call WebSearch/Bash/etc. and get stuck waiting for permission
Expand Down
Loading
Loading