mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-05-07 14:58:58 +08:00
feat(provider): add cross_session_call_id toggle and docs
This commit is contained in:
@@ -239,6 +239,12 @@ clawgo uninstall [--purge] [--remove-bin]
|
|||||||
- 严格 JSON 解析(未知字段会报错)
|
- 严格 JSON 解析(未知字段会报错)
|
||||||
- 配置热更新失败自动回滚备份
|
- 配置热更新失败自动回滚备份
|
||||||
|
|
||||||
|
Provider(`providers.proxy` / `providers.proxies.<name>`)新增开关:
|
||||||
|
|
||||||
|
- `cross_session_call_id`(bool,默认 `false`)
|
||||||
|
- `true`:沿用 `call_id` 传 `function_call_output`
|
||||||
|
- `false`:不传 `call_id`,改为传递 tool 结果内容(适合跨会话/聚合路由场景)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 稳定性与审计建议
|
## 稳定性与审计建议
|
||||||
|
|||||||
@@ -239,6 +239,12 @@ clawgo uninstall [--purge] [--remove-bin]
|
|||||||
- Strict JSON parsing (unknown fields fail fast)
|
- Strict JSON parsing (unknown fields fail fast)
|
||||||
- Auto rollback on failed hot reload
|
- Auto rollback on failed hot reload
|
||||||
|
|
||||||
|
New provider switch (`providers.proxy` / `providers.proxies.<name>`):
|
||||||
|
|
||||||
|
- `cross_session_call_id` (bool, default `false`)
|
||||||
|
- `true`: keep using `call_id` with `function_call_output`
|
||||||
|
- `false`: do not send `call_id`; pass tool results as plain content (recommended for cross-session/aggregator routing)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Stability / Operations Notes
|
## Stability / Operations Notes
|
||||||
|
|||||||
@@ -132,6 +132,7 @@
|
|||||||
"api_key": "YOUR_CLIPROXYAPI_KEY",
|
"api_key": "YOUR_CLIPROXYAPI_KEY",
|
||||||
"api_base": "http://localhost:8080/v1",
|
"api_base": "http://localhost:8080/v1",
|
||||||
"protocol": "chat_completions",
|
"protocol": "chat_completions",
|
||||||
|
"cross_session_call_id": false,
|
||||||
"models": ["glm-4.7", "gpt-4o-mini"],
|
"models": ["glm-4.7", "gpt-4o-mini"],
|
||||||
"supports_responses_compact": false,
|
"supports_responses_compact": false,
|
||||||
"auth": "bearer",
|
"auth": "bearer",
|
||||||
@@ -142,6 +143,7 @@
|
|||||||
"api_key": "YOUR_BACKUP_PROXY_KEY",
|
"api_key": "YOUR_BACKUP_PROXY_KEY",
|
||||||
"api_base": "http://localhost:8081/v1",
|
"api_base": "http://localhost:8081/v1",
|
||||||
"protocol": "responses",
|
"protocol": "responses",
|
||||||
|
"cross_session_call_id": false,
|
||||||
"models": ["gpt-4o-mini", "deepseek-chat"],
|
"models": ["gpt-4o-mini", "deepseek-chat"],
|
||||||
"supports_responses_compact": true,
|
"supports_responses_compact": true,
|
||||||
"auth": "bearer",
|
"auth": "bearer",
|
||||||
|
|||||||
@@ -246,6 +246,7 @@ type ProviderConfig struct {
|
|||||||
APIKey string `json:"api_key" env:"CLAWGO_PROVIDERS_{{.Name}}_API_KEY"`
|
APIKey string `json:"api_key" env:"CLAWGO_PROVIDERS_{{.Name}}_API_KEY"`
|
||||||
APIBase string `json:"api_base" env:"CLAWGO_PROVIDERS_{{.Name}}_API_BASE"`
|
APIBase string `json:"api_base" env:"CLAWGO_PROVIDERS_{{.Name}}_API_BASE"`
|
||||||
Protocol string `json:"protocol" env:"CLAWGO_PROVIDERS_{{.Name}}_PROTOCOL"`
|
Protocol string `json:"protocol" env:"CLAWGO_PROVIDERS_{{.Name}}_PROTOCOL"`
|
||||||
|
CrossSessionCallID bool `json:"cross_session_call_id" env:"CLAWGO_PROVIDERS_{{.Name}}_CROSS_SESSION_CALL_ID"`
|
||||||
Models []string `json:"models" env:"CLAWGO_PROVIDERS_{{.Name}}_MODELS"`
|
Models []string `json:"models" env:"CLAWGO_PROVIDERS_{{.Name}}_MODELS"`
|
||||||
SupportsResponsesCompact bool `json:"supports_responses_compact" env:"CLAWGO_PROVIDERS_{{.Name}}_SUPPORTS_RESPONSES_COMPACT"`
|
SupportsResponsesCompact bool `json:"supports_responses_compact" env:"CLAWGO_PROVIDERS_{{.Name}}_SUPPORTS_RESPONSES_COMPACT"`
|
||||||
Auth string `json:"auth" env:"CLAWGO_PROVIDERS_{{.Name}}_AUTH"`
|
Auth string `json:"auth" env:"CLAWGO_PROVIDERS_{{.Name}}_AUTH"`
|
||||||
|
|||||||
@@ -26,19 +26,21 @@ type HTTPProvider struct {
|
|||||||
apiBase string
|
apiBase string
|
||||||
protocol string
|
protocol string
|
||||||
defaultModel string
|
defaultModel string
|
||||||
|
crossSessionCallID bool
|
||||||
supportsResponsesCompact bool
|
supportsResponsesCompact bool
|
||||||
authMode string
|
authMode string
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTPProvider(apiKey, apiBase, protocol, defaultModel string, supportsResponsesCompact bool, authMode string, timeout time.Duration) *HTTPProvider {
|
func NewHTTPProvider(apiKey, apiBase, protocol, defaultModel string, crossSessionCallID bool, supportsResponsesCompact bool, authMode string, timeout time.Duration) *HTTPProvider {
|
||||||
normalizedBase := normalizeAPIBase(apiBase)
|
normalizedBase := normalizeAPIBase(apiBase)
|
||||||
return &HTTPProvider{
|
return &HTTPProvider{
|
||||||
apiKey: apiKey,
|
apiKey: apiKey,
|
||||||
apiBase: normalizedBase,
|
apiBase: normalizedBase,
|
||||||
protocol: normalizeProtocol(protocol),
|
protocol: normalizeProtocol(protocol),
|
||||||
defaultModel: strings.TrimSpace(defaultModel),
|
defaultModel: strings.TrimSpace(defaultModel),
|
||||||
|
crossSessionCallID: crossSessionCallID,
|
||||||
supportsResponsesCompact: supportsResponsesCompact,
|
supportsResponsesCompact: supportsResponsesCompact,
|
||||||
authMode: authMode,
|
authMode: authMode,
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
@@ -143,7 +145,7 @@ func (p *HTTPProvider) callResponses(ctx context.Context, messages []Message, to
|
|||||||
input := make([]map[string]interface{}, 0, len(messages))
|
input := make([]map[string]interface{}, 0, len(messages))
|
||||||
pendingCalls := map[string]struct{}{}
|
pendingCalls := map[string]struct{}{}
|
||||||
for _, msg := range messages {
|
for _, msg := range messages {
|
||||||
input = append(input, toResponsesInputItemsWithState(msg, pendingCalls)...)
|
input = append(input, toResponsesInputItemsWithState(msg, pendingCalls, p.crossSessionCallID)...)
|
||||||
}
|
}
|
||||||
requestBody := map[string]interface{}{
|
requestBody := map[string]interface{}{
|
||||||
"model": model,
|
"model": model,
|
||||||
@@ -243,10 +245,10 @@ func toChatCompletionsContent(msg Message) []map[string]interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func toResponsesInputItems(msg Message) []map[string]interface{} {
|
func toResponsesInputItems(msg Message) []map[string]interface{} {
|
||||||
return toResponsesInputItemsWithState(msg, nil)
|
return toResponsesInputItemsWithState(msg, nil, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func toResponsesInputItemsWithState(msg Message, pendingCalls map[string]struct{}) []map[string]interface{} {
|
func toResponsesInputItemsWithState(msg Message, pendingCalls map[string]struct{}, crossSessionCallID bool) []map[string]interface{} {
|
||||||
role := strings.ToLower(strings.TrimSpace(msg.Role))
|
role := strings.ToLower(strings.TrimSpace(msg.Role))
|
||||||
switch role {
|
switch role {
|
||||||
case "system", "developer", "user":
|
case "system", "developer", "user":
|
||||||
@@ -302,6 +304,12 @@ func toResponsesInputItemsWithState(msg Message, pendingCalls map[string]struct{
|
|||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
case "tool":
|
case "tool":
|
||||||
|
if !crossSessionCallID {
|
||||||
|
if strings.TrimSpace(msg.Content) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []map[string]interface{}{responsesMessageItem("user", msg.Content, "input_text")}
|
||||||
|
}
|
||||||
callID := msg.ToolCallID
|
callID := msg.ToolCallID
|
||||||
if callID == "" {
|
if callID == "" {
|
||||||
return nil
|
return nil
|
||||||
@@ -432,7 +440,7 @@ func (p *HTTPProvider) callResponsesStream(ctx context.Context, messages []Messa
|
|||||||
input := make([]map[string]interface{}, 0, len(messages))
|
input := make([]map[string]interface{}, 0, len(messages))
|
||||||
pendingCalls := map[string]struct{}{}
|
pendingCalls := map[string]struct{}{}
|
||||||
for _, msg := range messages {
|
for _, msg := range messages {
|
||||||
input = append(input, toResponsesInputItemsWithState(msg, pendingCalls)...)
|
input = append(input, toResponsesInputItemsWithState(msg, pendingCalls, p.crossSessionCallID)...)
|
||||||
}
|
}
|
||||||
requestBody := map[string]interface{}{
|
requestBody := map[string]interface{}{
|
||||||
"model": model,
|
"model": model,
|
||||||
@@ -915,7 +923,7 @@ func (p *HTTPProvider) BuildSummaryViaResponsesCompact(ctx context.Context, mode
|
|||||||
}
|
}
|
||||||
pendingCalls := map[string]struct{}{}
|
pendingCalls := map[string]struct{}{}
|
||||||
for _, msg := range messages {
|
for _, msg := range messages {
|
||||||
input = append(input, toResponsesInputItemsWithState(msg, pendingCalls)...)
|
input = append(input, toResponsesInputItemsWithState(msg, pendingCalls, p.crossSessionCallID)...)
|
||||||
}
|
}
|
||||||
if len(input) == 0 {
|
if len(input) == 0 {
|
||||||
return strings.TrimSpace(existingSummary), nil
|
return strings.TrimSpace(existingSummary), nil
|
||||||
@@ -1022,7 +1030,7 @@ func CreateProviderByName(cfg *config.Config, name string) (LLMProvider, error)
|
|||||||
if len(pc.Models) > 0 {
|
if len(pc.Models) > 0 {
|
||||||
defaultModel = pc.Models[0]
|
defaultModel = pc.Models[0]
|
||||||
}
|
}
|
||||||
return NewHTTPProvider(pc.APIKey, pc.APIBase, pc.Protocol, defaultModel, pc.SupportsResponsesCompact, pc.Auth, time.Duration(pc.TimeoutSec)*time.Second), nil
|
return NewHTTPProvider(pc.APIKey, pc.APIBase, pc.Protocol, defaultModel, pc.CrossSessionCallID, pc.SupportsResponsesCompact, pc.Auth, time.Duration(pc.TimeoutSec)*time.Second), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateProviders(cfg *config.Config) (map[string]LLMProvider, error) {
|
func CreateProviders(cfg *config.Config) (map[string]LLMProvider, error) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ func TestToResponsesInputItemsWithState_DropsOrphanToolOutputs(t *testing.T) {
|
|||||||
pending := map[string]struct{}{}
|
pending := map[string]struct{}{}
|
||||||
|
|
||||||
orphan := Message{Role: "tool", ToolCallID: "call-orphan", Content: "orphan output"}
|
orphan := Message{Role: "tool", ToolCallID: "call-orphan", Content: "orphan output"}
|
||||||
if got := toResponsesInputItemsWithState(orphan, pending); len(got) != 0 {
|
if got := toResponsesInputItemsWithState(orphan, pending, true); len(got) != 0 {
|
||||||
t.Fatalf("expected orphan tool output to be dropped, got: %#v", got)
|
t.Fatalf("expected orphan tool output to be dropped, got: %#v", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ func TestToResponsesInputItemsWithState_DropsOrphanToolOutputs(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
items := toResponsesInputItemsWithState(assistant, pending)
|
items := toResponsesInputItemsWithState(assistant, pending, true)
|
||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
t.Fatalf("assistant tool call should produce responses items")
|
t.Fatalf("assistant tool call should produce responses items")
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ func TestToResponsesInputItemsWithState_DropsOrphanToolOutputs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
matched := Message{Role: "tool", ToolCallID: "call-1", Content: "file content"}
|
matched := Message{Role: "tool", ToolCallID: "call-1", Content: "file content"}
|
||||||
matchedItems := toResponsesInputItemsWithState(matched, pending)
|
matchedItems := toResponsesInputItemsWithState(matched, pending, true)
|
||||||
if len(matchedItems) != 1 {
|
if len(matchedItems) != 1 {
|
||||||
t.Fatalf("expected matched tool output item, got %#v", matchedItems)
|
t.Fatalf("expected matched tool output item, got %#v", matchedItems)
|
||||||
}
|
}
|
||||||
@@ -40,3 +40,24 @@ func TestToResponsesInputItemsWithState_DropsOrphanToolOutputs(t *testing.T) {
|
|||||||
t.Fatalf("matched tool output should clear pending call id")
|
t.Fatalf("matched tool output should clear pending call id")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestToResponsesInputItemsWithState_ToolResultAsUserInputWhenCallIDDisabled(t *testing.T) {
|
||||||
|
pending := map[string]struct{}{
|
||||||
|
"call-1": {},
|
||||||
|
}
|
||||||
|
msg := Message{Role: "tool", ToolCallID: "call-1", Content: "file content"}
|
||||||
|
|
||||||
|
items := toResponsesInputItemsWithState(msg, pending, false)
|
||||||
|
if len(items) != 1 {
|
||||||
|
t.Fatalf("expected one fallback tool result item, got %#v", items)
|
||||||
|
}
|
||||||
|
if items[0]["type"] != "message" {
|
||||||
|
t.Fatalf("expected message item, got %#v", items[0])
|
||||||
|
}
|
||||||
|
if items[0]["role"] != "user" {
|
||||||
|
t.Fatalf("expected fallback role=user, got %#v", items[0])
|
||||||
|
}
|
||||||
|
if _, ok := pending["call-1"]; !ok {
|
||||||
|
t.Fatalf("pending call state should remain untouched when call_id mode is disabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ const Config: React.FC = () => {
|
|||||||
api_key: '',
|
api_key: '',
|
||||||
api_base: '',
|
api_base: '',
|
||||||
protocol: 'responses',
|
protocol: 'responses',
|
||||||
|
cross_session_call_id: false,
|
||||||
models: [],
|
models: [],
|
||||||
supports_responses_compact: false,
|
supports_responses_compact: false,
|
||||||
auth: 'bearer',
|
auth: 'bearer',
|
||||||
|
|||||||
Reference in New Issue
Block a user