focusreactive / focusreactive/payload-plugins
[translator] Add Anthropic, Gemini and OpenRouter completion helpers (no vendor dependencies)
- Dominant language
- TypeScript
- Stars
- 19
- Forks
- 0
- Avg merge
- 16h 54m
- Merged PRs (30d)
- 19
Description
## Summary
Ship a **completion helper per vendor** — `anthropicComplete`, `geminiComplete`, `openRouterComplete`
— alongside the existing `openAIComplete`. Each knows how to call its service and carries **no
dependency on that service's SDK**: the consumer constructs the client, the helper builds the request.
~~Cover **Anthropic**, **Google Gemini** and **OpenRouter** as documented recipes built on
`createTranslationProvider({ complete })` — not as built-in adapters shipping optional SDKs.~~
### Revision history — read this before re-litigating the shape
**Revision 1 (original).** Three adapters under `src/translation-providers/`, each with its own
`optionalDependency` and lazy loader, mirroring what `createOpenAIProvider` did.
~~**This issue was rewritten on 2026-08-28.** It previously asked for three adapters under
`src/translation-providers/`, each with its own `optionalDependency` and lazy loader. That premise
died with the decision in #101 to deprecate the built-in OpenAI adapter and remove it in the next
major (`docs/DEPRECATIONS.md#built-in-openai-adapter`). Shipping three more of the thing we are
removing would have been the package arguing with itself.~~
**Revision 2 (2026-08-28, superseded within the day).** Documentation only — three README recipes, no
package code. Written while #101 looked like it was deprecating the whole OpenAI adapter.
**Revision 3 (2026-08-28, current).** #101 landed with a narrower boundary
(`docs/DEPRECATIONS.md#openai-client-construction`): what is deprecated is the layer that *builds the
SDK client* and carries `openai` as an optional dependency. `openAIComplete` **stays and is public** —
the request body, the structured-output envelope choice, reading the reply and turning a schema
rejection into advice all cost no dependency. So the OpenAI answer is neither an adapter nor a recipe;
it is a vendor helper with no vendor dependency, and the other three should match it.
## Why this shape
The expensive part of an adapter was never knowing how to call a service. It was carrying that
service's SDK as an *optional* dependency of ours — a lazy import shaped around deployment
file-tracers that resolve `import()` statically, plus a classifier telling "not installed" from
"installed but broken" across four runtimes' wordings. All of that exists so a package the consumer
may not have can be absent safely.
A completion helper skips the whole problem: it takes a client the consumer already constructed, so
there is nothing to load, nothing to guard, and no entry in `optionalDependencies`. The knowledge
stays with us, where one fix serves everyone; the dependency stays with them, where the version is
theirs.
~~**No new package code is planned.** If writing the three recipes surfaces something genuinely shared
— the same helper needed by all three and by the OpenAI recipe — that is a finding worth raising,
but it does not get built speculatively: the package's own rule is no abstraction without at least
two real callers already in the codebase, and a recipe in the README is not a caller.~~
## Proposed change
One helper per vendor, following `openAIComplete`'s shape: a `CompletionFn` factory taking a client
and a model, returning the function `createTranslationProvider({ complete })` wants. Where the vendors
genuinely diverge is structured output, and that is what each helper has to get right:
- **Anthropic** — the Messages API has no `json_object` mode. Use a tool-shaped schema so the reply is
a validated object rather than prose.
- **Google Gemini** — `responseMimeType: "application/json"` plus an explicit response schema. The
OpenAI-compatible endpoint exists but lags on schema support; use the native shape.
- **OpenRouter** — the OpenAI wire format with a different `baseURL`, so this one may be a thin
configuration of `openAIComplete` rather than a helper of its own — decide once the other two are
written. `model` has no sensible default. Structured-output support varies per upstream model, so
the helper must make the `json_object` fallback easy and say plainly that key-set validation is then
the guarantee. The optional `HTTP-Referer` / `X-Title` attribution headers belong in its options.
**Each client type is a decision, not a default.** `openAIComplete` takes `OpenAIClientShape` — a
hand-written structural slice of the SDK client, so a real `new OpenAI()` satisfies it unwrapped. That
pattern is sound as a convenience but must not be copied by reflex; see the analysis in
`docs/plans/2026-08-28-provider-review-findings.md` ("Is imitating a vendor's interface a sound
pattern?"). The test to apply per vendor: **does a real client satisfy the slice without the consumer
constructing anything?** If yes, mirror the OpenAI approach. If no, the helper takes an input in our
own vocabulary instead.
## Acceptance criteria
- [ ] `anthropicComplete` and `geminiComplete` are exported from the package root with `@since` JSDoc
and a README note; OpenRouter either gets its own or is documented as a configuration of
`openAIComplete`, whichever the investigation shows.
- [ ] **No new runtime dependency and no new `optionalDependencies` entry.** This is the criterion the
whole shape exists to satisfy.
- [ ] Each helper names its structured-output approach and what key preservation rests on: the API
when a schema is enforced, this package's key-set validation when it is not.
- [ ] The client type of each helper is justified against the test above, and the justification is
recorded — not just chosen.
- [ ] Unit tests per helper against a stub client: happy path, key-set mismatch, unparseable reply,
and the vendor's own schema-rejection wording mapped to a useful message.
- [ ] A README recipe per vendor showing client construction plus the helper, in the three-line shape
`openAIComplete` now uses.
- [ ] A comparison table saying which vendor can *prevent* a dropped key and which can only have it
*detected*.
- [ ] The "Pluggable providers" line stops implying OpenAI is special.
~~- [ ] README carries a recipe for each of Anthropic, Gemini and OpenRouter, each a complete,
copy-paste `createTranslationProvider({ complete })` call.~~
~~- [ ] The recipes **type-check**, not just read well — a compile-only fixture per vendor.~~
_(Superseded: with a helper per vendor the recipes are three lines and the helpers carry their own
tests, so a separate compile-only fixture buys nothing.)_
## Notes
The three helpers are independent and can land separately.
Depends on #101 (the layering, and the narrowed OpenAI deprecation) landing first.
Part of #98 — that epic's framing needs the same correction.
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.