focusreactive / focusreactive/payload-plugins

[translator] Add Anthropic, Gemini and OpenRouter completion helpers (no vendor dependencies)

Open
#100 2 comments 0 reactions 1 assignee Claimed by @SearheiParkhamchuk View on GitHub
enhancement translator
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.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.