Changeset 0.23.0 (#153)
This commit is contained in:
@@ -235,6 +235,20 @@ func (p *AnthropicProvider) doRequest(ctx context.Context, cfg ProviderConfig, r
|
||||
|
||||
antMsg := anthropicMessage{Role: m.Role}
|
||||
|
||||
// ── Foreign persona rewrite (v0.23.0) ──
|
||||
// Anthropic doesn't support the "name" field on messages.
|
||||
// Rewrite named assistant messages (from other personas) to user
|
||||
// role with attribution so Anthropic maintains alternation and
|
||||
// the model understands it's a multi-participant conversation.
|
||||
if m.Role == "assistant" && m.Name != "" {
|
||||
antMsg.Role = "user"
|
||||
antMsg.Content = []anthropicContentBlock{
|
||||
{Type: "text", Text: fmt.Sprintf("[%s responded:]\n%s", m.Name, m.Content)},
|
||||
}
|
||||
messages = append(messages, antMsg)
|
||||
continue
|
||||
}
|
||||
|
||||
if m.Role == "assistant" && len(m.ToolCalls) > 0 {
|
||||
// Assistant with tool calls → mixed content blocks
|
||||
if m.Content != "" {
|
||||
@@ -372,7 +386,7 @@ func (p *AnthropicProvider) doRequest(ctx context.Context, cfg ProviderConfig, r
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(httpReq)
|
||||
resp, err := cfg.Client().Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("provider request: %w", err)
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@ func (p *OpenAIProvider) ListModels(ctx context.Context, cfg ProviderConfig) ([]
|
||||
httpReq.Header.Set(k, v)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(httpReq)
|
||||
resp, err := cfg.Client().Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list models: %w", err)
|
||||
}
|
||||
@@ -303,7 +303,7 @@ func (p *OpenAIProvider) Embed(ctx context.Context, cfg ProviderConfig, req Embe
|
||||
httpReq.Header.Set(k, v)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(httpReq)
|
||||
resp, err := cfg.Client().Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("embedding request: %w", err)
|
||||
}
|
||||
@@ -424,6 +424,12 @@ func (p *OpenAIProvider) doRequest(ctx context.Context, cfg ProviderConfig, req
|
||||
oaiMsg.Name = m.Name
|
||||
}
|
||||
|
||||
// Named messages: pass through for speaker attribution (v0.23.0)
|
||||
// OpenAI supports "name" on any role to distinguish participants
|
||||
if m.Name != "" && m.Role != "tool" {
|
||||
oaiMsg.Name = m.Name
|
||||
}
|
||||
|
||||
oaiReq.Messages = append(oaiReq.Messages, oaiMsg)
|
||||
}
|
||||
|
||||
@@ -453,7 +459,7 @@ func (p *OpenAIProvider) doRequest(ctx context.Context, cfg ProviderConfig, req
|
||||
httpReq.Header.Set(k, v)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(httpReq)
|
||||
resp, err := cfg.Client().Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("provider request: %w", err)
|
||||
}
|
||||
@@ -474,7 +480,7 @@ type openaiMessage struct {
|
||||
Content interface{} `json:"content,omitempty"` // string or []openaiContentPart
|
||||
ToolCalls []openaiToolCall `json:"tool_calls,omitempty"` // assistant requesting tools
|
||||
ToolCallID string `json:"tool_call_id,omitempty"` // role=tool result
|
||||
Name string `json:"name,omitempty"` // tool name for role=tool
|
||||
Name string `json:"name,omitempty"` // speaker name (tool or persona attribution)
|
||||
}
|
||||
|
||||
// openaiContentPart represents a single element in a multimodal content array.
|
||||
|
||||
@@ -56,7 +56,7 @@ func (p *OpenRouterProvider) ListModels(ctx context.Context, cfg ProviderConfig)
|
||||
httpReq.Header.Set(k, v)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(httpReq)
|
||||
resp, err := cfg.Client().Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("openrouter list models: %w", err)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
)
|
||||
@@ -43,6 +47,41 @@ type ProviderConfig struct {
|
||||
APIKey string
|
||||
CustomHeaders map[string]string // Extra HTTP headers (e.g. OpenRouter HTTP-Referer)
|
||||
Settings map[string]interface{} // Provider-specific settings from config JSONB
|
||||
ProxyMode string // "system" (default), "direct", "custom"
|
||||
ProxyURL string // Only used when ProxyMode == "custom"
|
||||
}
|
||||
|
||||
// Client returns an *http.Client configured for this provider's proxy settings.
|
||||
// system = http.ProxyFromEnvironment (Go default), direct = no proxy,
|
||||
// custom = explicit proxy URL (http/https/socks5).
|
||||
func (c ProviderConfig) Client() *http.Client {
|
||||
transport := &http.Transport{
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
}
|
||||
switch c.ProxyMode {
|
||||
case "direct":
|
||||
transport.Proxy = nil
|
||||
case "custom":
|
||||
if c.ProxyURL != "" {
|
||||
u, err := url.Parse(c.ProxyURL)
|
||||
if err != nil {
|
||||
// Fall back to system rather than silently going direct —
|
||||
// a bad custom URL in a mandatory-proxy environment is a
|
||||
// security/audit hole if it silently bypasses.
|
||||
log.Printf("[proxy] invalid custom proxy URL %q: %v — falling back to system proxy", c.ProxyURL, err)
|
||||
transport.Proxy = http.ProxyFromEnvironment
|
||||
} else {
|
||||
transport.Proxy = http.ProxyURL(u)
|
||||
}
|
||||
} else {
|
||||
transport.Proxy = http.ProxyFromEnvironment
|
||||
}
|
||||
default: // "system" or empty
|
||||
transport.Proxy = http.ProxyFromEnvironment
|
||||
}
|
||||
return &http.Client{Transport: transport}
|
||||
}
|
||||
|
||||
// ── Request / Response Types ────────────────
|
||||
@@ -60,7 +99,7 @@ type Message struct {
|
||||
// Tool calling fields (assistant → tool_calls, tool → tool_call_id)
|
||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"` // set when assistant requests tools
|
||||
ToolCallID string `json:"tool_call_id,omitempty"` // set for role="tool" result messages
|
||||
Name string `json:"name,omitempty"` // tool name for role="tool"
|
||||
Name string `json:"name,omitempty"` // speaker name (tool result, or persona attribution v0.23.0)
|
||||
}
|
||||
|
||||
// ContentPart is a single element in a multimodal message.
|
||||
|
||||
@@ -52,7 +52,7 @@ func (p *VeniceProvider) ListModels(ctx context.Context, cfg ProviderConfig) ([]
|
||||
httpReq.Header.Set("Authorization", "Bearer "+cfg.APIKey)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(httpReq)
|
||||
resp, err := cfg.Client().Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("venice list models: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user