Repository navigation
0.23.0 - 2026-09-15
Added
{{SourceFields}}prompt variable.Translation.build_variables/3
(used by the translation pipeline'sdo_translate/6) now also binds a
computed{{SourceFields}}block — one---MARKER---section per field
actually passed — alongside the existing per-field variables. A prompt
template can be written against this single variable instead of one
hardcoded slot per field, so it never has an unbound{{fieldname}}
placeholder for a field the caller didn't supply. Additive: an existing
prompt built on named slots ({{title}},{{Title}}, …) keeps
rendering exactly as before.Prompt.unbound_placeholders/1— returns the literal{{...}}
sequences left in already-rendered text.PhoenixKitAI.ask_with_prompt/4
andcomplete_with_system_prompt/5now call it after rendering: a
leftover placeholder logs aLogger.warningand is recorded as
unbound_placeholdersin the request's metadata. Non-fatal — a prompt
may legitimately contain a literal{{...}}unrelated to this engine —
and metadata-neutral when nothing is unbound (no[]added to every
request row).put_translation/4'soptsnow carries:source_fields.
TranslateWorkerthreads the%{field_name => text}it read via
source_fields/2— captured before the AI call, not the resource's
state at persist time, which can be seconds to tens of seconds later —
through to theTranslatableadapter. An adapter that doesn't need it
(most don't) ignores the key; one that wants to fingerprint what was
actually translated (e.g. to detect a source that changed again while
the call was in flight) no longer has to reconstruct it from a
possibly-staleresource.TranslateWorker.retryable?/1now also treats
{:parse_error, {:missing_fields, _}}as retryable, within the existing
max_attempts: 3. Insurance against ordinary model non-determinism on
an otherwise well-formed prompt, not a substitute for one.
Fixed
- A failed AI request no longer drops its
:attribution.
log_failed_request/7mergesattributioninto the request's metadata
the same way the success path already did, so a timed-out or errored
call stays traceable to the resource that triggered it instead of only
the calls that happened to succeed. - A provider error delivered inside a 200 response body is now
classified instead of discarded.Translation.handle_ai_response/2
normalizes%{"error" => %{"code" => code}}to the same
{:api_error, code}shape the transport-error path already produces,
so the existing retry classification picks up a transient failure
(observed: a 504 reported in the body by OpenRouter) instead of
discarding it on the first attempt. PK_AI_SKIP_DB=1is reachable throughmix testagain. Thetest
alias ranecto.create --quietunconditionally, so on a machine with no
reachable Postgresecto.createitself exited nonzero before
test/test_helper.exsever loaded — the soft-skip documented there
could never actually trigger viamix test. The alias is now a
function that checks the same env var before deciding whether to
provision the database at all.
Fixed (post-merge review of #27)
- A provider error in a 200 body is classified where the request is
logged, not only inTranslation.Completion.chat_completion/3maps
{"error": {"code": N}}through the samehandle_error_status/2a non-2xx
status uses, so the usage row is a failure (not a zero-token success), a
429 comes back:rate_limited(snoozed byTranslateWorker, not retried
against its attempts), 401/402 keep their own atoms, and the request cache
never stores the error for a retry to replay. - Translation retries reach the model again with the request cache on.
Translation.translate_fields/6now passes:cachethrough, and
TranslateWorkersendscache: :refreshfrom attempt 2 when the host's
cache is enabled. Before, amissing_fieldsanswer was stored on attempt 1
and every retry replayed it. - The unbound-placeholder guard scans the template, not the rendered
text. NewPrompt.unbound_placeholders/2(template, variables). A{{...}}
inside source content is no longer reported as an unbound slot, logged, or
written to request metadata outside thecapture_request_contentgate. A
cache hit's row now carriesunbound_placeholderslike the fresh row, and
the key is kept out of the cache key. mix testno longer aborts when the database role cannot connect to
postgres. The alias'secto.createis best-effort;test_helper.exs
still aborts the run on a test database it cannot use.
No action is required from an existing consumer, phoenix_kit_publishing
included — every change above is additive: a new template variable, a
new metadata field, a new opts key adapters may ignore, and wider
retry/attribution coverage on paths that previously under-reported. A
Translatable adapter that wants the staleness-tracking use case this
was built for reads opts[:source_fields] in its own put_translation/4;
everything else keeps working unchanged.