Skip to content

Add provider adapters and the first one - #15

Merged
rokoss21 merged 1 commit into
masterfrom
feat/provider-adapters
Aug 21, 2026
Merged

Add provider adapters and the first one#15
rokoss21 merged 1 commit into
masterfrom
feat/provider-adapters

Conversation

@rokoss21

Copy link
Copy Markdown
Owner

ROADMAP item 3.

target_provider_id was the literal string "unknown-provider", there was no adapters directory, and the cross-vendor promise — the reason Canonical JSON exists at all — had no implementation and nothing testing it.

The obligations, as a trait rather than as prose

crates/fct-adapter says what an adapter owes:

  1. Message order and roles survive (§18.1.2) — no reordering, merging or dropping.
  2. Tool identity survives. Canonical names are Interface.fn (§16.2.6) — what policy matches on. Providers allow only [A-Za-z0-9_-], so the adapter must mangle, and the mangling must reverse.
  3. Schemas cross unchanged (Appendix D).
  4. The target is attributedprovider_id lands in metadata.target_provider_id.

No I/O in the crate. Translation is pure, which is what makes "canonical semantics survive this" falsifiable.

OpenAiCompatAdapter

Covers OpenAI and everything on the same wire format: DeepSeek, Together, vLLM, llama.cpp's server, LM Studio.

Canonical Provider
WeatherAPI.get_current WeatherAPI__get_current

Two canonical names that would share a provider name are an error. A returned call mapping to no tool this request exposed is an error. Neither is guessed at — the guard decides on the canonical name, so a wrong mapping means a policy answered by the wrong rule.

End to end

facet-fct agent --input contract.facet --script recorded.json \
  --provider openai-compat --model deepseek-chat --exec

The recording holds raw provider responses, so the translation is part of the run rather than asserted about in isolation:

{"final_text":"It is raining in Minsk, 11C.","target_provider_id":"openai-compat.v1",
 "guard_events":4,"stop":"completed","turns":2}

The provider called WeatherAPI__get_current; the artifact records WeatherAPI.get_current.

Verification

497 tests (6 adapter unit tests over recorded traffic, 1 CLI test through the whole loop), smoke, matrix, doc-examples, coverage gate — all green.

Not done, and written down

  • No HTTP. Carrying bytes is the host's job, and it belongs behind a feature flag so neither CI nor a compile-only workflow needs a network stack.
  • One adapter proves little. Anthropic's format differs enough — system is a top-level field, tool results have their own content block — to be the first real test of whether these obligations are the right ones.
  • target_provider_id deliberately does not enter the provenance chain: Appendix F.4 enumerates the seed and the target is not in it.

🤖 Generated with Claude Code

`target_provider_id` was the string "unknown-provider", there was no
adapters directory, and the cross-vendor promise — the reason Canonical
JSON exists at all — had no implementation and nothing testing it.

`crates/fct-adapter` states what an adapter owes, as a trait rather than
as prose: preserve message order and roles (§18.1.2), preserve tool
identity across a reversible name mangling (§16.2.6), pass Appendix D
schemas through unchanged, and name the target it rendered for. The
crate performs no I/O — translation is pure, which is what makes
"canonical semantics survive this" a claim tests can falsify.

`OpenAiCompatAdapter` implements it for OpenAI and everything on the
same wire format: DeepSeek, Together, vLLM, llama.cpp's server, LM
Studio. Provider tool names allow only `[A-Za-z0-9_-]`, so
`WeatherAPI.get_current` goes out as `WeatherAPI__get_current` and comes
back mapped to the canonical name before the guard sees it. Two
canonical names that would share a provider name are an error; so is a
returned call that maps to no tool this request exposed. Neither is
guessed at, because the guard decides on the canonical name and a wrong
mapping is a policy answered by the wrong rule.

`facet-fct agent --provider openai-compat` replays recorded provider
responses through the adapter, so the translation is part of the run
rather than something asserted about in isolation, and the payload now
records a real target.

Six unit tests over recorded traffic plus a CLI test that drives the
whole loop through the adapter: the provider calls the mangled name, the
artifact records `WeatherAPI.get_current`.

Not done, and recorded in ROADMAP.md and docs/20-adapters.md: no HTTP —
carrying bytes is the host's job and belongs behind a feature flag so
neither CI nor a compile-only workflow needs a network stack. And one
adapter proves little; Anthropic's format differs enough (system is a
top-level field, tool results have their own content block) to be the
first real test of whether these obligations are the right ones.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rokoss21
rokoss21 merged commit f96c146 into master Aug 21, 2026
14 checks passed
@rokoss21
rokoss21 deleted the feat/provider-adapters branch August 21, 2026 22:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant