V2 Migration Audit
View SourceReqLLM ships a versioned deprecation ledger and a read-only source audit so an application can prepare for V2 without changing its V1 behavior. V1 deprecated APIs continue to work for their documented overlap window.
Run the audit from an application root:
mix req_llm.migration_audit
mix req_llm.migration_audit lib test
mix req_llm.migration_audit --exclude test/fixtures
mix req_llm.migration_audit --format json
The default command recursively scans .ex and .exs files. It ignores
dependency, build, VCS, coverage, generated documentation, and Node dependency
directories. It parses source but never evaluates or rewrites it, starts ReqLLM,
loads credentials, or makes a provider request.
Exit status 0 means the report is clean or contains advisories only. Status
1 means actionable migration findings are present.
Status 2 means a requested path could not be read, source could not be parsed,
or the bundled ledger was invalid.
Machine-readable ledger
priv/deprecations.json
is the source of truth. Schema version 1 records the owner, deprecated contract,
replacement, introduction release, target major, V2 scope status, minimum overlap
window, guide, and static detector for every active deprecation. An
introduced_version of unreleased identifies a deprecation added after the
latest tagged release.
target_major: 2 identifies the next major in which a removal could be
compatible; it is not approval by itself. Only records with
v2_scope: "approved" belong to the breaking scope accepted by the V2
roadmap. An unapproved record remains an active deprecation and must not be
removed in V2 without a separate scope decision. The audit reports those
records as non-blocking advisories so they remain visible without expanding the
approved V2 migration plan.
Applications and tooling can inspect the exact shipped data:
ledger = ReqLLM.Migration.ledger()
deprecations = ReqLLM.Migration.deprecations()
checks = ReqLLM.Migration.migration_checks()ReqLLM.Migration.audit/2 returns the same schema-versioned report as the Mix
task. Reports contain source locations and migration guidance, never source
snippets or evaluated values.
Schema version 1 has stable common fields:
{
"schema_version": 1,
"status": "findings",
"summary": {
"files_scanned": 12,
"actionable": 1,
"advisory": 0,
"errors": 0
},
"findings": [
{
"id": "req_llm.stream_text_bang",
"category": "deprecated_api",
"actionable": true,
"file": "lib/client.ex",
"line": 12,
"column": 5,
"contract": "ReqLLM.stream_text!/2",
"owner": "ReqLLM core maintainers",
"message": "ReqLLM.stream_text!/3 is deprecated.",
"replacement": "ReqLLM.stream_text/3 and a ReqLLM.StreamResponse projection",
"guide": "https://hexdocs.pm/req_llm/v2-migration-audit.html#bang-streaming-apis"
}
],
"errors": []
}Top-level status is clean, advisory, findings, or error. Every finding
retains the common fields shown above. Errors contain file and message.
Additional finding IDs may be added without changing schema version; existing
common fields retain their meaning. Precompile a newly changed project with
mix compile --quiet before piping the task's standard output to another
process.
Bang streaming APIs
The deprecated bang functions return :ok; they do not return an enumerable.
Choose the projection needed by the consumer.
Before:
ReqLLM.stream_text!(model, prompt)After, for text deltas:
{:ok, response} = ReqLLM.stream_text(model, prompt)
Enum.each(ReqLLM.StreamResponse.tokens(response), &IO.write/1)After, for canonical lifecycle and output events:
{:ok, response} = ReqLLM.stream_text(model, prompt)
Enum.each(ReqLLM.StreamResponse.events(response), &handle_event/1)The same replacement applies to ReqLLM.Generation.stream_text!/3,
ReqLLM.stream_object!/4, and ReqLLM.Generation.stream_object!/4 using their
non-bang counterparts.
Context tool-call helpers
Before:
ReqLLM.Context.assistant_with_tools(tool_calls, "Checking")
ReqLLM.Context.assistant_tool_call("weather", %{city: "Chicago"})
ReqLLM.Context.assistant_tool_calls(calls)After:
ReqLLM.Context.assistant("Checking", tool_calls: tool_calls)
ReqLLM.Context.assistant("", tool_calls: [{"weather", %{city: "Chicago"}}])
ReqLLM.Context.assistant("", tool_calls: calls)Preserve any IDs and metadata in the supported assistant/2 option shape.
Key lookup aliases
Before:
ReqLLM.Keys.fetch(:openai)
ReqLLM.Keys.fetch!(:openai)After:
ReqLLM.Keys.get(:openai)
ReqLLM.Keys.get!(:openai)Tool-call flag helper
Before:
ReqLLM.ToolCall.builtin_flag?(value)After:
ReqLLM.ToolCall.flagged_builtin?(value)The replacement retains the original flag-only behavior and names it explicitly.
Meta Llama delegates
The compatibility delegates on ReqLLM.Providers.Meta move to the module that
owns the Llama wire format. For example:
ReqLLM.Providers.Meta.format_request(context, options)
ReqLLM.Providers.Meta.Llama.format_request(context, options)Apply the same module move to format_llama_prompt/1, parse_response/2,
extract_usage/1, and parse_stop_reason/1.
Provider option namespaces
The V1 flat shape remains supported. A literal provider model and literal flat options can be migrated mechanically to the additive provider-keyed shape.
Before:
ReqLLM.generate_text("openai:gpt-4o", prompt,
provider_options: [store: false]
)After:
ReqLLM.generate_text("openai:gpt-4o", prompt,
provider_options: [openai: [store: false]]
)The audit reports this only when the provider identity and option container are
both literal. Hosted providers use the selected ReqLLM provider identity, such
as azure or google_vertex, as the outer key.
Raw stream assumptions
StreamResponse.stream remains stable throughout V1. V2 may make canonical
events the primary raw stream, so code that explicitly matches the struct field
should choose a named projection now.
Before:
%ReqLLM.StreamResponse{stream: chunks} = response
Enum.each(chunks, &handle_chunk/1)After:
response
|> ReqLLM.StreamResponse.events()
|> Enum.each(&handle_event/1)Use tokens/1, tool_calls/1, or another documented projection when those
semantics are the real dependency. The audit detects explicit struct field
matching; it does not infer the type of an arbitrary value.stream expression.
Provider extension inventory
Direct use ReqLLM.Provider and @behaviour ReqLLM.Provider declarations are
reported as advisories. The provider behavior remains stable in V1 and the V2
replacement is not frozen. There is no code migration to perform yet.
Keep the existing provider conformance suite green, record the extension in the V2 ecosystem survey, and follow the provider extension decision. Advisory-only reports exit successfully.
Structured-output validation default
V1 preserves compatible validation. V2 may make strict final validation the default. Set the intended policy explicitly on statically structured calls.
Before:
ReqLLM.generate_text(model, prompt,
output: ReqLLM.Output.object(name: [type: :string])
)After, preserving V1 behavior:
ReqLLM.generate_text(model, prompt,
output: ReqLLM.Output.object(name: [type: :string]),
output_validation: :compatible
)After, opting into the proposed V2 safety default now:
ReqLLM.generate_text(model, prompt,
output: ReqLLM.Output.object(name: [type: :string]),
output_validation: :strict
)The check also covers literal generate_object and streaming object calls when
their option source is statically visible.
Limitations and false positives
The audit deliberately avoids general Elixir data-flow analysis. It detects
fully qualified remote calls, literal model and option shapes, explicit
ReqLLM.StreamResponse struct matching, and direct fully qualified provider
behavior declarations.
It does not resolve imported functions, local aliases, macro-generated calls, computed model specifications, variable option containers, or the runtime type of field access. These cases require manual review. A computed value is not reported merely because it could contain a legacy shape.
Comments and strings are ignored because the audit parses syntax. A source file that cannot be parsed is an error rather than a partial success. Findings are deterministically ordered by file and source location, making JSON output stable for CI and editor integrations.