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
|
|
@ -29,6 +29,7 @@ type Provider struct {
|
||||||
client *anthropic.Client
|
client *anthropic.Client
|
||||||
tokenSource func() (string, error)
|
tokenSource func() (string, error)
|
||||||
baseURL string
|
baseURL string
|
||||||
|
useStreaming bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProvider(token string) *Provider {
|
func NewProvider(token string) *Provider {
|
||||||
|
|
@ -44,6 +45,7 @@ func NewProviderWithBaseURL(token, apiBase string) *Provider {
|
||||||
return &Provider{
|
return &Provider{
|
||||||
client: &client,
|
client: &client,
|
||||||
baseURL: baseURL,
|
baseURL: baseURL,
|
||||||
|
useStreaming: true, // Enable streaming by default for kimi-coding compatibility
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,6 +53,7 @@ func NewProviderWithClient(client *anthropic.Client) *Provider {
|
||||||
return &Provider{
|
return &Provider{
|
||||||
client: client,
|
client: client,
|
||||||
baseURL: defaultBaseURL,
|
baseURL: defaultBaseURL,
|
||||||
|
useStreaming: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,6 +88,17 @@ func (p *Provider) Chat(
|
||||||
return nil, err
|
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...)
|
resp, err := p.client.Messages.New(ctx, params, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("claude API call: %w", err)
|
return nil, fmt.Errorf("claude API call: %w", err)
|
||||||
|
|
@ -93,6 +107,97 @@ func (p *Provider) Chat(
|
||||||
return parseResponse(resp), nil
|
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 {
|
func (p *Provider) GetDefaultModel() string {
|
||||||
return "claude-sonnet-4.6"
|
return "claude-sonnet-4.6"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"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.
|
// 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
|
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:
|
default:
|
||||||
return nil, "", fmt.Errorf("unknown protocol %q in model %q", protocol, cfg.Model)
|
return nil, "", fmt.Errorf("unknown protocol %q in model %q", protocol, cfg.Model)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue