feat: add streaming support and custom base URL for Anthropic API
- Add streaming API support in anthropic provider for better compatibility - Enable custom base URL configuration for Anthropic-compatible endpoints - Add kimi-coding protocol support in factory provider This allows users to configure custom Anthropic API endpoints via the model_list configuration with api_base field.
This commit is contained in:
parent
3584c0c7be
commit
24182be571
2 changed files with 126 additions and 9 deletions
|
|
@ -26,9 +26,10 @@ type (
|
|||
const defaultBaseURL = "https://api.anthropic.com"
|
||||
|
||||
type Provider struct {
|
||||
client *anthropic.Client
|
||||
tokenSource func() (string, error)
|
||||
baseURL string
|
||||
client *anthropic.Client
|
||||
tokenSource func() (string, error)
|
||||
baseURL string
|
||||
useStreaming bool
|
||||
}
|
||||
|
||||
func NewProvider(token string) *Provider {
|
||||
|
|
@ -42,15 +43,17 @@ func NewProviderWithBaseURL(token, apiBase string) *Provider {
|
|||
option.WithBaseURL(baseURL),
|
||||
)
|
||||
return &Provider{
|
||||
client: &client,
|
||||
baseURL: baseURL,
|
||||
client: &client,
|
||||
baseURL: baseURL,
|
||||
useStreaming: true, // Enable streaming by default for kimi-coding compatibility
|
||||
}
|
||||
}
|
||||
|
||||
func NewProviderWithClient(client *anthropic.Client) *Provider {
|
||||
return &Provider{
|
||||
client: client,
|
||||
baseURL: defaultBaseURL,
|
||||
client: client,
|
||||
baseURL: defaultBaseURL,
|
||||
useStreaming: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -85,6 +88,17 @@ func (p *Provider) Chat(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if p.useStreaming {
|
||||
return p.chatStreaming(ctx, params, opts)
|
||||
}
|
||||
return p.chatNonStreaming(ctx, params, opts)
|
||||
}
|
||||
|
||||
func (p *Provider) chatNonStreaming(
|
||||
ctx context.Context,
|
||||
params anthropic.MessageNewParams,
|
||||
opts []option.RequestOption,
|
||||
) (*LLMResponse, error) {
|
||||
resp, err := p.client.Messages.New(ctx, params, opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("claude API call: %w", err)
|
||||
|
|
@ -93,6 +107,97 @@ func (p *Provider) Chat(
|
|||
return parseResponse(resp), nil
|
||||
}
|
||||
|
||||
func (p *Provider) chatStreaming(
|
||||
ctx context.Context,
|
||||
params anthropic.MessageNewParams,
|
||||
opts []option.RequestOption,
|
||||
) (*LLMResponse, error) {
|
||||
stream := p.client.Messages.NewStreaming(ctx, params, opts...)
|
||||
|
||||
var content strings.Builder
|
||||
var toolCalls []ToolCall
|
||||
var usage UsageInfo
|
||||
var finishReason string
|
||||
|
||||
for stream.Next() {
|
||||
event := stream.Current()
|
||||
|
||||
switch event.Type {
|
||||
case "message_start":
|
||||
msg := event.AsMessageStart()
|
||||
if msg.Message.Usage.InputTokens > 0 {
|
||||
usage.PromptTokens = int(msg.Message.Usage.InputTokens)
|
||||
}
|
||||
if msg.Message.Usage.OutputTokens > 0 {
|
||||
usage.CompletionTokens = int(msg.Message.Usage.OutputTokens)
|
||||
}
|
||||
|
||||
case "content_block_start":
|
||||
block := event.AsContentBlockStart()
|
||||
if block.ContentBlock.Type == "tool_use" {
|
||||
toolUse := block.ContentBlock.AsToolUse()
|
||||
// Initialize tool call
|
||||
toolCalls = append(toolCalls, ToolCall{
|
||||
ID: toolUse.ID,
|
||||
Name: toolUse.Name,
|
||||
Arguments: map[string]any{},
|
||||
})
|
||||
}
|
||||
|
||||
case "content_block_delta":
|
||||
delta := event.AsContentBlockDelta()
|
||||
if delta.Delta.Type == "text_delta" {
|
||||
content.WriteString(delta.Delta.Text)
|
||||
} else if delta.Delta.Type == "input_json_delta" {
|
||||
// Accumulate JSON for tool calls
|
||||
if len(toolCalls) > 0 {
|
||||
lastIdx := len(toolCalls) - 1
|
||||
// Parse partial JSON
|
||||
var args map[string]any
|
||||
if err := json.Unmarshal([]byte(delta.Delta.PartialJSON), &args); err == nil {
|
||||
for k, v := range args {
|
||||
toolCalls[lastIdx].Arguments[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case "message_stop":
|
||||
finishReason = "stop"
|
||||
|
||||
case "message_delta":
|
||||
msgDelta := event.AsMessageDelta()
|
||||
if msgDelta.Delta.StopReason != "" {
|
||||
switch msgDelta.Delta.StopReason {
|
||||
case "tool_use":
|
||||
finishReason = "tool_calls"
|
||||
case "max_tokens":
|
||||
finishReason = "length"
|
||||
default:
|
||||
finishReason = "stop"
|
||||
}
|
||||
}
|
||||
// Update usage from delta
|
||||
if msgDelta.Usage.OutputTokens > 0 {
|
||||
usage.CompletionTokens = int(msgDelta.Usage.OutputTokens)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := stream.Err(); err != nil {
|
||||
return nil, fmt.Errorf("streaming error: %w", err)
|
||||
}
|
||||
|
||||
usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens
|
||||
|
||||
return &LLMResponse{
|
||||
Content: content.String(),
|
||||
ToolCalls: toolCalls,
|
||||
FinishReason: finishReason,
|
||||
Usage: &usage,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *Provider) GetDefaultModel() string {
|
||||
return "claude-sonnet-4.6"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
anthropicprovider "github.com/sipeed/picoclaw/pkg/providers/anthropic"
|
||||
)
|
||||
|
||||
// createClaudeAuthProvider creates a Claude provider using OAuth credentials from auth store.
|
||||
|
|
@ -168,6 +169,17 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
|
|||
}
|
||||
return provider, modelID, nil
|
||||
|
||||
case "kimi-coding", "kimicoding":
|
||||
// Kimi for Coding uses Anthropic API format
|
||||
apiBase := cfg.APIBase
|
||||
if apiBase == "" {
|
||||
apiBase = "https://api.kimi.com/coding"
|
||||
}
|
||||
if cfg.APIKey == "" {
|
||||
return nil, "", fmt.Errorf("api_key is required for kimi-coding protocol (model: %s)", cfg.Model)
|
||||
}
|
||||
return anthropicprovider.NewProviderWithBaseURL(cfg.APIKey, apiBase), modelID, nil
|
||||
|
||||
default:
|
||||
return nil, "", fmt.Errorf("unknown protocol %q in model %q", protocol, cfg.Model)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue