Dsxir.LM.Sycophant (dsxir v0.1.0)

Copy Markdown

Sycophant-backed implementation of the Dsxir.LM behaviour.

Config shape:

[model: "openai:gpt-4o-mini", api_key: nil | binary,
 base_url: nil | binary, temperature: float, max_tokens: integer,
 top_p: float, num_retries: integer]

Unknown config keys pass through to Sycophant; Sycophant validates them against the resolved wire protocol's param schema.

Per-call opts override per-config opts via Keyword.merge/2. api_key and base_url are lifted into credentials: %{...} for Sycophant. The :headers config key is reserved for future use and intentionally ignored.

Streaming

The :stream opt is forwarded to Sycophant.generate_text/3 unchanged. Sycophant invokes the 1-arity callback with %Sycophant.StreamChunk{} values (:text_delta, :tool_call_delta, :reasoning_delta, :usage, :failed, :incomplete, :cancelled, :done) as the response streams in; the final assembled {:ok, text, usage} tuple is still returned by this callback so Dsxir.Predictor.Predict can build its %Dsxir.Prediction{} normally.

Usage extraction

On a successful response, the %Sycophant.Usage{} struct is flattened into the Dsxir.LM usage map. When Sycophant reports nil usage, the empty usage map (tokens_in: nil, tokens_out: nil, cost: nil) is returned so downstream telemetry can rely on the keys always being present.

Dsxir.LM usage keySycophant.Usage field
:tokens_in:input_tokens
:tokens_out:output_tokens
:cost:total_cost

Error translation

Provider errors are translated into typed Dsxir.Errors.LM.* structs. A %Sycophant.Error.Provider.BadRequest{status: 400} is classified against a small set of regexes (case-insensitive) on its :body to detect upstream context-window-exceeded responses:

  • ~r/context length/i
  • ~r/maximum context/i
  • ~r/prompt is too long/i
  • ~r/too many tokens/i
  • ~r/exceeds.*token/i
  • ~r/request is too large/i

When any of those match the body, the error becomes a Dsxir.Errors.LM.ContextWindow (with prompt_tokens/limit extracted when the body carries them). All other BadRequest bodies stay as Dsxir.Errors.LM.RequestFailed with status: 400.