mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-18 05:47:38 +00:00
feat: anthropic adapter with bidirectional translation incl streaming
Implements Task 9: the anthropic-type providerAdapter and its
request/response/stream translation to and from OpenAI's wire shape.
- translate_anthropic.go: anthropicRequestFromOpenAI (system messages
joined, content blocks incl. base64 data-URI images, tool_calls ->
tool_use, tool messages -> tool_result, max_tokens defaulting,
stop/stop_sequences, tool_choice incl. the none-drops-tools rule,
n>1/logit_bias/logprobs rejected), openAIResponseFromAnthropic
(content blocks joined/tool_calls re-marshaled, stop_reason mapping,
usage), and anthropicStreamState.translate mapping each SSE event to
zero or more OpenAI chat.completion.chunk payloads. translateError
carries a notSupported flag so the caller (Task 12) can tell a 400
from a 501.
- provider_anthropic.go: anthropicAdapter implementing providerAdapter.
newAnthropicAdapter errors on an empty API key per ruling (c), unlike
openai-type's keyless ruling. Headers: x-api-key, anthropic-version.
embeddings always returns a notSupported translateError without
calling upstream (Anthropic has no embeddings endpoint). listModels
GETs /v1/models and returns any non-2xx (incl. 404) as-is.
- providers.go: wires the anthropic buildAdapters case to
newAnthropicAdapter, propagating its constructor error.
- provider_openai_test.go: replaces the now-stale placeholder
TestBuildAdapters subtest with one asserting a keyed anthropic
provider builds successfully and a keyless one errors.
- translate_anthropic_test.go: golden-file harness walking
testdata/anthropic/{chat,resp}_*_in.json against sibling _want.json
(success) or _wanterr.json (*translateError) fixtures, plus a stream
harness driving each *.sse fixture through anthropicStreamState and
asserting the chunk list and usage().
- provider_anthropic_test.go: httptest-backed adapter wiring tests
(streaming + non-streaming), asserting OpenAI-shaped output on the
wire, per-event flushing, and the required auth headers.
Gate: go test ./... -count=1 and -race both green; gofmt, go vet,
fieldalignment, golangci-lint (incl. gosec) all clean.
This commit is contained in:
+23
-4
@@ -557,13 +557,32 @@ func TestBuildAdapters(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("edge: anthropic type not yet implemented", func(t *testing.T) {
|
||||
t.Run("good: anthropic with an API key builds successfully", func(t *testing.T) {
|
||||
cfg := &Config{Providers: map[string]*ProviderConfig{
|
||||
"p1": {Type: "anthropic", APIKey: "sk-test"},
|
||||
}}
|
||||
_, err := buildAdapters(cfg)
|
||||
if err == nil {
|
||||
t.Fatalf("buildAdapters: want placeholder error for anthropic, got nil")
|
||||
adapters, err := buildAdapters(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("buildAdapters: %v", err)
|
||||
}
|
||||
a, ok := adapters["p1"]
|
||||
if !ok {
|
||||
t.Fatalf("adapters[%q] missing", "p1")
|
||||
}
|
||||
if a.typeName() != "anthropic" {
|
||||
t.Errorf("typeName() = %q, want %q", a.typeName(), "anthropic")
|
||||
}
|
||||
if a.base() != defaultBaseAnthropic {
|
||||
t.Errorf("base() = %q, want %q", a.base(), defaultBaseAnthropic)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("bad: anthropic without an API key is a constructor error", func(t *testing.T) {
|
||||
cfg := &Config{Providers: map[string]*ProviderConfig{
|
||||
"p1": {Type: "anthropic"},
|
||||
}}
|
||||
if _, err := buildAdapters(cfg); err == nil {
|
||||
t.Fatalf("buildAdapters: want error for keyless anthropic provider, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user