You've already forked agentic-coding-workflow
feat(infra/llm): Anthropic client with stream + count_tokens via official SDK
This commit is contained in:
@@ -18,6 +18,9 @@ require (
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/anthropics/anthropic-sdk-go v1.38.0 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.2 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
@@ -36,6 +39,7 @@ require (
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
@@ -44,6 +48,7 @@ require (
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/go-archive v0.2.0 // indirect
|
||||
github.com/moby/moby/api v1.54.1 // indirect
|
||||
@@ -67,8 +72,13 @@ require (
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/testcontainers/testcontainers-go v0.42.0 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
|
||||
@@ -8,6 +8,12 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
|
||||
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
|
||||
github.com/anthropics/anthropic-sdk-go v1.38.0 h1:bA4DcK+91gorIX+5VTONnynyt9LRU4nnN6rRQ+j/NIg=
|
||||
github.com/anthropics/anthropic-sdk-go v1.38.0/go.mod h1:d288C1L+m74OYuYBvc4UFtR1Q8J0gC55oYDh2t+XxdI=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
|
||||
github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
@@ -65,6 +71,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
||||
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 h1:D/V0gu4zQ3cL2WKeVNVM4r2gLxGGf6McLwgXzRTo2RQ=
|
||||
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
@@ -75,6 +83,7 @@ github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
||||
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -87,6 +96,8 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
|
||||
github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
@@ -156,10 +167,22 @@ github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44Xt
|
||||
github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30=
|
||||
github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 h1:GCbb1ndrF7OTDiIvxXyItaDab4qkzTFJ48LKFdM7EIo=
|
||||
github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0/go.mod h1:IRPBaI8jXdrNfD0e4Zm7Fbcgaz5shKxOQv4axiL09xs=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/anthropics/anthropic-sdk-go"
|
||||
"github.com/anthropics/anthropic-sdk-go/option"
|
||||
)
|
||||
|
||||
// Compile-time assertion: AnthropicClient must satisfy LLMClient.
|
||||
var _ LLMClient = (*AnthropicClient)(nil)
|
||||
|
||||
// AnthropicClient 通过官方 SDK 调用 Anthropic Messages API。
|
||||
// baseURL 可改写以走 LiteLLM/OpenRouter 等代理。
|
||||
type AnthropicClient struct {
|
||||
sdk anthropic.Client
|
||||
}
|
||||
|
||||
// NewAnthropic 构造客户端。baseURL 可空(用 SDK 默认)。
|
||||
func NewAnthropic(apiKey, baseURL string) (*AnthropicClient, error) {
|
||||
opts := []option.RequestOption{option.WithAPIKey(apiKey)}
|
||||
if baseURL != "" {
|
||||
opts = append(opts, option.WithBaseURL(baseURL))
|
||||
}
|
||||
c := anthropic.NewClient(opts...)
|
||||
return &AnthropicClient{sdk: c}, nil
|
||||
}
|
||||
|
||||
func (c *AnthropicClient) Provider() string { return "anthropic" }
|
||||
|
||||
// toSDKMessages 把中立 Message 转 SDK 类型。
|
||||
// image/document 的 Data 字段为原始字节,此处进行 base64 编码。
|
||||
func toSDKMessages(msgs []Message) []anthropic.MessageParam {
|
||||
out := make([]anthropic.MessageParam, 0, len(msgs))
|
||||
for _, m := range msgs {
|
||||
blocks := make([]anthropic.ContentBlockParamUnion, 0, len(m.Blocks))
|
||||
for _, b := range m.Blocks {
|
||||
switch b.Type {
|
||||
case "text":
|
||||
blocks = append(blocks, anthropic.NewTextBlock(b.Text))
|
||||
case "image":
|
||||
// NewImageBlockBase64 期望 base64 字符串,Data 字段存原始字节
|
||||
encoded := base64.StdEncoding.EncodeToString(b.Data)
|
||||
blocks = append(blocks, anthropic.NewImageBlockBase64(b.MimeType, encoded))
|
||||
case "document":
|
||||
// NewDocumentBlock 泛型支持 Base64PDFSourceParam,Data 字段存原始字节
|
||||
encoded := base64.StdEncoding.EncodeToString(b.Data)
|
||||
blocks = append(blocks, anthropic.NewDocumentBlock(anthropic.Base64PDFSourceParam{
|
||||
Data: encoded,
|
||||
}))
|
||||
}
|
||||
}
|
||||
role := anthropic.MessageParamRoleUser
|
||||
if m.Role == RoleAssistant {
|
||||
role = anthropic.MessageParamRoleAssistant
|
||||
}
|
||||
out = append(out, anthropic.MessageParam{Role: role, Content: blocks})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (c *AnthropicClient) Stream(ctx context.Context, modelID string, req Request) (<-chan StreamEvent, error) {
|
||||
params := anthropic.MessageNewParams{
|
||||
Model: anthropic.Model(modelID),
|
||||
MaxTokens: int64(req.MaxOutputTokens),
|
||||
Messages: toSDKMessages(req.Messages),
|
||||
}
|
||||
if req.SystemPrompt != "" {
|
||||
params.System = []anthropic.TextBlockParam{{Text: req.SystemPrompt}}
|
||||
}
|
||||
if req.Reasoning {
|
||||
params.Thinking = anthropic.ThinkingConfigParamOfEnabled(int64(req.MaxOutputTokens / 2))
|
||||
}
|
||||
|
||||
stream := c.sdk.Messages.NewStreaming(ctx, params)
|
||||
out := make(chan StreamEvent, 16)
|
||||
|
||||
go func() {
|
||||
defer close(out)
|
||||
var inputTokens int
|
||||
for stream.Next() {
|
||||
event := stream.Current()
|
||||
// SDK v1.38: AsAny() returns unexported anyMessageStreamEvent which cannot
|
||||
// be used in a type switch. Use event.Type string + typed As*() methods.
|
||||
switch event.Type {
|
||||
case "message_start":
|
||||
ev := event.AsMessageStart()
|
||||
inputTokens = int(ev.Message.Usage.InputTokens)
|
||||
case "content_block_delta":
|
||||
ev := event.AsContentBlockDelta()
|
||||
switch ev.Delta.Type {
|
||||
case "text_delta":
|
||||
if text := ev.Delta.AsTextDelta().Text; text != "" {
|
||||
out <- StreamEvent{Type: EventTextDelta, Text: text}
|
||||
}
|
||||
case "thinking_delta":
|
||||
if thinking := ev.Delta.AsThinkingDelta().Thinking; thinking != "" {
|
||||
out <- StreamEvent{Type: EventThinkingDelta, Text: thinking}
|
||||
}
|
||||
}
|
||||
case "message_delta":
|
||||
ev := event.AsMessageDelta()
|
||||
out <- StreamEvent{Type: EventUsage, Usage: &Usage{
|
||||
PromptTokens: inputTokens,
|
||||
CompletionTokens: int(ev.Usage.OutputTokens),
|
||||
}}
|
||||
case "message_stop":
|
||||
out <- StreamEvent{Type: EventDone, Reason: "end_turn"}
|
||||
}
|
||||
}
|
||||
if err := stream.Err(); err != nil {
|
||||
out <- StreamEvent{Type: EventError, Err: fmt.Errorf("anthropic stream: %w", err)}
|
||||
}
|
||||
}()
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *AnthropicClient) CountTokens(ctx context.Context, modelID string, req Request) (int, error) {
|
||||
params := anthropic.MessageCountTokensParams{
|
||||
Model: anthropic.Model(modelID),
|
||||
Messages: toSDKMessages(req.Messages),
|
||||
}
|
||||
if req.SystemPrompt != "" {
|
||||
// MessageCountTokensParams.System 是 MessageCountTokensParamsSystemUnion,
|
||||
// 不同于 MessageNewParams.System ([]TextBlockParam)
|
||||
params.System = anthropic.MessageCountTokensParamsSystemUnion{
|
||||
OfTextBlockArray: []anthropic.TextBlockParam{{Text: req.SystemPrompt}},
|
||||
}
|
||||
}
|
||||
res, err := c.sdk.Messages.CountTokens(ctx, params)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("anthropic count_tokens: %w", err)
|
||||
}
|
||||
return int(res.InputTokens), nil
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const anthropicStreamFixture = `event: message_start
|
||||
data: {"type":"message_start","message":{"id":"m1","content":[],"model":"claude-opus","role":"assistant","stop_reason":null,"stop_sequence":null,"type":"message","usage":{"input_tokens":10,"output_tokens":0}}}
|
||||
|
||||
event: content_block_start
|
||||
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
|
||||
|
||||
event: content_block_delta
|
||||
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
|
||||
|
||||
event: content_block_delta
|
||||
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" world"}}
|
||||
|
||||
event: content_block_stop
|
||||
data: {"type":"content_block_stop","index":0}
|
||||
|
||||
event: message_delta
|
||||
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":2}}
|
||||
|
||||
event: message_stop
|
||||
data: {"type":"message_stop"}
|
||||
|
||||
`
|
||||
|
||||
func TestAnthropic_Stream_TextDeltaAndDone(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/v1/messages", r.URL.Path)
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.WriteHeader(200)
|
||||
_, _ = w.Write([]byte(anthropicStreamFixture))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c, err := NewAnthropic("test-key", srv.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
req := Request{
|
||||
Messages: []Message{{Role: RoleUser, Blocks: []ContentBlock{{Type: "text", Text: "hi"}}}},
|
||||
MaxOutputTokens: 100,
|
||||
}
|
||||
ch, err := c.Stream(context.Background(), "claude-opus-4-7", req)
|
||||
require.NoError(t, err)
|
||||
|
||||
var deltas []string
|
||||
var sawUsage, sawDone bool
|
||||
var promptTokens, completionTokens int
|
||||
for ev := range ch {
|
||||
switch ev.Type {
|
||||
case EventTextDelta:
|
||||
deltas = append(deltas, ev.Text)
|
||||
case EventUsage:
|
||||
sawUsage = true
|
||||
promptTokens = ev.Usage.PromptTokens
|
||||
completionTokens = ev.Usage.CompletionTokens
|
||||
case EventDone:
|
||||
sawDone = true
|
||||
}
|
||||
}
|
||||
require.Equal(t, "Hello world", strings.Join(deltas, ""))
|
||||
require.True(t, sawUsage)
|
||||
require.Equal(t, 10, promptTokens)
|
||||
require.Equal(t, 2, completionTokens)
|
||||
require.True(t, sawDone)
|
||||
}
|
||||
|
||||
func TestAnthropic_Provider(t *testing.T) {
|
||||
c, _ := NewAnthropic("k", "http://localhost")
|
||||
require.Equal(t, "anthropic", c.Provider())
|
||||
}
|
||||
Reference in New Issue
Block a user