Normandy.LLM.OpenAICompatibleAdapter (normandy v2.2.0)

View Source

Adapter implementing Normandy.Agents.Model against any OpenAI-compatible Chat Completions endpoint (OpenAI, DigitalOcean Inference, etc.).

Text-completion only (v1): tool/function calling is not supported and a non-empty request.tools raises. An answer is parsed once with Normandy.LLM.JsonDeserializer.parse_and_validate/3 and the :on_parse_failure policy applies; an answer that does not parse is not re-asked (Normandy.LLM.ClaudioAdapter re-asks). An answer that parses but breaks a schema constraint is re-asked by BaseAgent on any client, under enable_json_retry and json_retry_max_attempts.

client = %Normandy.LLM.OpenAICompatibleAdapter{
  api_key: System.get_env("OPENAI_API_KEY"),
  base_url: "https://api.openai.com/v1"
}

Options

The adapter reads these keys from options (a map):

  • :timeout - how long to wait for the response, in milliseconds (default: 120_000)
  • :req_options - Req options merged into the request (see "Transport injection")
  • :max_tokens_param - which body key carries the request's max_tokens: :max_tokens (the default), :max_completion_tokens (OpenAI reasoning models), or :omit (send neither; the server's own limit applies). Any other value raises ArgumentError when the agent is built (BaseAgent.init/1)

Transport injection (testing)

options[:req_options] is merged into the Req request, so tests can route the HTTP layer to a Req.Test stub without network access (this needs :plug in your test dependencies):

Req.Test.stub(MyStub, fn conn ->
  Req.Test.json(conn, %{"choices" => [%{"message" => %{"content" => "hi"}}]})
end)

client = %Normandy.LLM.OpenAICompatibleAdapter{
  api_key: "test",
  options: %{req_options: [plug: {Req.Test, MyStub}]}
}

Errors

converse/3 returns {:ok, %Normandy.LLM.Response{}}, or {:error, %Normandy.LLMError{}} on non-2xx responses or transport failures, classified as follows:

  • 429 → :rate_limit, retryable
  • 408, 5xx → :server, retryable
  • 401 → :authentication, not retryable
  • 403 → :permission, not retryable
  • 404 → :not_found, not retryable
  • Other 4xx → :invalid_request, not retryable
  • Other status → :unknown, not retryable
  • Req.TransportError, Mint.TransportError, Mint.HTTPError → :transport, retryable
  • Anything else → wrapped via LLMError.wrap/1

stream/3 returns {:error, %Normandy.LLMError{type: :unsupported}}: this adapter does not stream.

Summary

Functions

Checks client.options once, up front; BaseAgent.init/1 calls it. Raises ArgumentError for an unknown :max_tokens_param.

Types

t()

@type t() :: %Normandy.LLM.OpenAICompatibleAdapter{
  api_key: String.t(),
  base_url: String.t(),
  finch: atom() | nil,
  options: map()
}

Functions

validate_options!(client)

@spec validate_options!(t()) :: t()

Checks client.options once, up front; BaseAgent.init/1 calls it. Raises ArgumentError for an unknown :max_tokens_param.