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
10 changes: 10 additions & 0 deletions lib/req_llm/provider.ex
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,15 @@ defmodule ReqLLM.Provider do
@callback translate_options(operation(), LLMDB.Model.t(), keyword()) ::
{keyword(), [String.t()]}

@doc """
Returns tool call ID compatibility policy for this provider (optional).

Providers can enforce tool call ID constraints when a context built on one
provider is sent to a different provider.
"""
@callback tool_call_id_policy(operation(), LLMDB.Model.t() | map(), keyword() | map()) ::
map() | keyword()

@doc """
Returns the default environment variable name for API authentication.

Expand Down Expand Up @@ -603,6 +612,7 @@ defmodule ReqLLM.Provider do
extract_usage: 2,
default_env_key: 0,
translate_options: 3,
tool_call_id_policy: 3,
build_body: 1,
decode_stream_event: 2,
decode_stream_event: 3,
Expand Down
1 change: 1 addition & 0 deletions lib/req_llm/provider/defaults.ex
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ defmodule ReqLLM.Provider.Defaults do
:n,
:tools,
:tool_choice,
:tool_call_id_compat,
:req_http_options,
:stream,
:frequency_penalty,
Expand Down
6 changes: 6 additions & 0 deletions lib/req_llm/provider/options.ex
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ defmodule ReqLLM.Provider.Options do
default: false,
doc: "Enable streaming responses"
],
tool_call_id_compat: [
type: {:in, [:auto, :sanitize, :strict, :passthrough]},
default: :auto,
doc:
"Tool call ID compatibility mode for cross-provider contexts"
],

# Provider-specific options container
provider_options: [
Expand Down
54 changes: 53 additions & 1 deletion lib/req_llm/providers/amazon_bedrock.ex
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,20 @@ defmodule ReqLLM.Providers.AmazonBedrock do
{endpoint, get_formatter_module(family), family}
end

operation = opts[:operation] || :chat
compat_opts = Keyword.put(opts, :use_converse, use_converse)

context =
ReqLLM.ToolCallIdCompat.apply_context(
__MODULE__,
operation,
model,
opts[:context],
compat_opts
)

opts = Keyword.put(opts, :context, context)

updated_request =
request
|> Map.put(:url, URI.parse(base_url <> endpoint_base))
Expand All @@ -415,7 +429,7 @@ defmodule ReqLLM.Providers.AmazonBedrock do
model_body =
formatter.format_request(
model_id,
opts[:context],
context,
opts
)

Expand Down Expand Up @@ -546,6 +560,17 @@ defmodule ReqLLM.Providers.AmazonBedrock do
{formatter, "/model/#{model_id}/invoke-with-response-stream"}
end

translated_opts = Keyword.put(translated_opts, :use_converse, use_converse)

context =
ReqLLM.ToolCallIdCompat.apply_context(
__MODULE__,
:chat,
model,
context,
translated_opts
)

# Build request body with translated options
body = formatter.format_request(model_id, context, translated_opts)

Expand Down Expand Up @@ -990,6 +1015,33 @@ defmodule ReqLLM.Providers.AmazonBedrock do
end
end

@impl ReqLLM.Provider
def tool_call_id_policy(_operation, model, opts) do
model_id = model.provider_model_id || model.id
use_converse = determine_use_converse(model_id, opts)
family = get_model_family(model_id)

cond do
use_converse ->
%{
mode: :sanitize,
invalid_chars_regex: ~r/[^A-Za-z0-9_-]/,
max_length: 64,
enforce_turn_boundary: true
}

family == "anthropic" ->
%{
mode: :sanitize,
invalid_chars_regex: ~r/[^A-Za-z0-9_-]/,
enforce_turn_boundary: true
}

true ->
%{mode: :passthrough}
end
end

# Move :thinking from top-level opts to additionalModelRequestFields for Bedrock
defp move_thinking_to_additional_fields(opts) do
case Keyword.pop(opts, :thinking) do
Expand Down
12 changes: 12 additions & 0 deletions lib/req_llm/providers/amazon_bedrock/converse.ex
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,18 @@ defmodule ReqLLM.Providers.AmazonBedrock.Converse do
{context, opts}
end

context =
ReqLLM.ToolCallIdCompat.apply_context_with_policy(
context,
%{
mode: :sanitize,
invalid_chars_regex: ~r/[^A-Za-z0-9_-]/,
max_length: 64,
enforce_turn_boundary: true
},
opts
)

request = %{}

# Add messages
Expand Down
32 changes: 32 additions & 0 deletions lib/req_llm/providers/anthropic.ex
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,18 @@ defmodule ReqLLM.Providers.Anthropic do
context = request.options[:context]
model_name = request.options[:model]
opts = request.options
operation = request.options[:operation] || :chat

model =
Req.Request.get_private(request, :req_llm_model) ||
%{
id: model_name,
model: model_name,
provider: :anthropic,
provider_model_id: nil
}

context = ReqLLM.ToolCallIdCompat.apply_context(__MODULE__, operation, model, context, opts)

body = build_request_body(context, model_name, opts)
json_body = Jason.encode!(body)
Expand Down Expand Up @@ -459,6 +471,17 @@ defmodule ReqLLM.Providers.Anthropic do
beta_headers = build_beta_headers(translated_opts)
all_headers = streaming_headers ++ beta_headers

operation = opts[:operation] || :chat

context =
ReqLLM.ToolCallIdCompat.apply_context(
__MODULE__,
operation,
model,
context,
translated_opts
)

body = build_request_body(context, get_api_model_id(model), translated_opts ++ [stream: true])
url = build_request_url(translated_opts)

Expand Down Expand Up @@ -491,6 +514,15 @@ defmodule ReqLLM.Providers.Anthropic do
{translated_opts, []}
end

@impl ReqLLM.Provider
def tool_call_id_policy(_operation, _model, _opts) do
%{
mode: :sanitize,
invalid_chars_regex: ~r/[^A-Za-z0-9_-]/,
enforce_turn_boundary: true
}
end

# Private implementation functions

defp get_anthropic_version(user_opts) do
Expand Down
41 changes: 40 additions & 1 deletion lib/req_llm/providers/azure.ex
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,17 @@ defmodule ReqLLM.Providers.Azure do
|> maybe_clean_thinking_after_translation(model_family, operation)
|> maybe_warn_service_tier(model_family, model_id)

context =
ReqLLM.ToolCallIdCompat.apply_context(
__MODULE__,
operation,
model,
context,
processed_opts
)

processed_opts = Keyword.put(processed_opts, :context, context)

{api_version, deployment, base_url} =
extract_azure_credentials(model, processed_opts)

Expand Down Expand Up @@ -680,7 +691,17 @@ defmodule ReqLLM.Providers.Azure do
headers = base_headers ++ extra_headers

body =
formatter.format_request(model_id, context, Keyword.put(translated_opts, :stream, true))
formatter.format_request(
model_id,
ReqLLM.ToolCallIdCompat.apply_context(
__MODULE__,
operation,
model,
context,
translated_opts
),
Keyword.put(translated_opts, :stream, true)
)
|> maybe_add_model_for_foundry(deployment, base_url)

finch_request = Finch.build(:post, url, headers, Jason.encode!(body))
Expand Down Expand Up @@ -764,6 +785,23 @@ defmodule ReqLLM.Providers.Azure do
end
end

@impl ReqLLM.Provider
def tool_call_id_policy(_operation, model, _opts) do
model_id = effective_model_id(model)

case get_model_family(model_id) do
"claude" ->
%{
mode: :sanitize,
invalid_chars_regex: ~r/[^A-Za-z0-9_-]/,
enforce_turn_boundary: true
}

_ ->
%{mode: :passthrough}
end
end

@doc """
Pre-validates and transforms options before request building.

Expand Down Expand Up @@ -895,6 +933,7 @@ defmodule ReqLLM.Providers.Azure do
:n,
:tools,
:tool_choice,
:tool_call_id_compat,
:req_http_options,
:frequency_penalty,
:system_prompt,
Expand Down
9 changes: 9 additions & 0 deletions lib/req_llm/providers/azure/anthropic.ex
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,15 @@ defmodule ReqLLM.Providers.Azure.Anthropic do
{context, opts}
end

context =
ReqLLM.ToolCallIdCompat.apply_context(
ReqLLM.Providers.Azure,
operation || :chat,
%{id: model_id, provider_model_id: model_id, provider: :azure},
context,
opts
)

model = %{model: model_id}

body = Anthropic.Context.encode_request(context, model)
Expand Down
1 change: 1 addition & 0 deletions lib/req_llm/providers/google.ex
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ defmodule ReqLLM.Providers.Google do
:fixture,
:tools,
:tool_choice,
:tool_call_id_compat,
:n,
:prompt,
:size,
Expand Down
31 changes: 31 additions & 0 deletions lib/req_llm/providers/google_vertex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,11 @@ defmodule ReqLLM.Providers.GoogleVertex do
# Add context to opts so it can be stored in request.options
other_opts = Keyword.put(other_opts, :context, context)

context =
ReqLLM.ToolCallIdCompat.apply_context(__MODULE__, operation, model, context, other_opts)

other_opts = Keyword.put(other_opts, :context, context)

# Build request body using formatter
body = formatter.format_request(model.provider_model_id || model.id, context, other_opts)

Expand Down Expand Up @@ -529,6 +534,27 @@ defmodule ReqLLM.Providers.GoogleVertex do
end
end

@impl ReqLLM.Provider
def tool_call_id_policy(_operation, model, _opts) do
case get_model_family(model.provider_model_id || model.id) do
"claude" ->
%{
mode: :sanitize,
invalid_chars_regex: ~r/[^A-Za-z0-9_-]/,
enforce_turn_boundary: true
}

"gemini" ->
%{
mode: :drop,
drop_function_call_ids: true
}

_ ->
%{mode: :passthrough}
end
end

@impl ReqLLM.Provider
def attach_stream(model, context, opts, _finch_name) do
# Process and validate options
Expand All @@ -537,6 +563,11 @@ defmodule ReqLLM.Providers.GoogleVertex do
{gcp_creds, other_opts, model_family, formatter} =
process_and_validate_opts(opts, model, operation)

context =
ReqLLM.ToolCallIdCompat.apply_context(__MODULE__, operation, model, context, other_opts)

other_opts = Keyword.put(other_opts, :context, context)

# Build request body using formatter (with stream: true)
body =
formatter.format_request(
Expand Down
Loading
Loading