Add provider adapters and the first one - #15
Merged
Conversation
`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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ROADMAP item 3.
target_provider_idwas 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-adaptersays what an adapter owes: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.provider_idlands inmetadata.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.
WeatherAPI.get_currentWeatherAPI__get_currentTwo 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
The recording holds raw provider responses, so the translation is part of the run rather than asserted about in isolation:
The provider called
WeatherAPI__get_current; the artifact recordsWeatherAPI.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
systemis 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_iddeliberately does not enter the provenance chain: Appendix F.4 enumerates the seed and the target is not in it.🤖 Generated with Claude Code