mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-13 16:47:34 +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 解析(未知字段会报错)
|
||||
- 配置热更新失败自动回滚备份
|
||||
|
||||
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)
|
||||
- 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
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
"api_key": "YOUR_CLIPROXYAPI_KEY",
|
||||
"api_base": "http://localhost:8080/v1",
|
||||
"protocol": "chat_completions",
|
||||
"cross_session_call_id": false,
|
||||
"models": ["glm-4.7", "gpt-4o-mini"],
|
||||
"supports_responses_compact": false,
|
||||
"auth": "bearer",
|
||||
@@ -142,6 +143,7 @@
|
||||
"api_key": "YOUR_BACKUP_PROXY_KEY",
|
||||
"api_base": "http://localhost:8081/v1",
|
||||
"protocol": "responses",
|
||||
"cross_session_call_id": false,
|
||||
"models": ["gpt-4o-mini", "deepseek-chat"],
|
||||
"supports_responses_compact": true,
|
||||
"auth": "bearer",
|
||||
|
||||
@@ -246,6 +246,7 @@ type ProviderConfig struct {
|
||||
APIKey string `json:"api_key" env:"CLAWGO_PROVIDERS_{{.Name}}_API_KEY"`
|
||||
APIBase string `json:"api_base" env:"CLAWGO_PROVIDERS_{{.Name}}_API_BASE"`
|
||||
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"`
|
||||
SupportsResponsesCompact bool `json:"supports_responses_compact" env:"CLAWGO_PROVIDERS_{{.Name}}_SUPPORTS_RESPONSES_COMPACT"`
|
||||
Auth string `json:"auth" env:"CLAWGO_PROVIDERS_{{.Name}}_AUTH"`
|
||||
|
||||
@@ -26,19 +26,21 @@ type HTTPProvider struct {
|
||||
apiBase string
|
||||
protocol string
|
||||
defaultModel string
|
||||
crossSessionCallID bool
|
||||
supportsResponsesCompact bool
|
||||
authMode string
|
||||
timeout time.Duration
|
||||
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)
|
||||
return &HTTPProvider{
|
||||
apiKey: apiKey,
|
||||
apiBase: normalizedBase,
|
||||
protocol: normalizeProtocol(protocol),
|
||||
defaultModel: strings.TrimSpace(defaultModel),
|
||||
crossSessionCallID: crossSessionCallID,
|
||||
supportsResponsesCompact: supportsResponsesCompact,
|
||||
authMode: authMode,
|
||||
timeout: timeout,
|
||||
@@ -143,7 +145,7 @@ func (p *HTTPProvider) callResponses(ctx context.Context, messages []Message, to
|
||||
input := make([]map[string]interface{}, 0, len(messages))
|
||||
pendingCalls := map[string]struct{}{}
|
||||
for _, msg := range messages {
|
||||
input = append(input, toResponsesInputItemsWithState(msg, pendingCalls)...)
|
||||
input = append(input, toResponsesInputItemsWithState(msg, pendingCalls, p.crossSessionCallID)...)
|
||||
}
|
||||
requestBody := map[string]interface{}{
|
||||
"model": model,
|
||||
@@ -243,10 +245,10 @@ func toChatCompletionsContent(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))
|
||||
switch role {
|
||||
case "system", "developer", "user":
|
||||
@@ -302,6 +304,12 @@ func toResponsesInputItemsWithState(msg Message, pendingCalls map[string]struct{
|
||||
}
|
||||
return items
|
||||
case "tool":
|
||||
if !crossSessionCallID {
|
||||
if strings.TrimSpace(msg.Content) == "" {
|
||||
return nil
|
||||
}
|
||||
return []map[string]interface{}{responsesMessageItem("user", msg.Content, "input_text")}
|
||||
}
|
||||
callID := msg.ToolCallID
|
||||
if callID == "" {
|
||||
return nil
|
||||
@@ -432,7 +440,7 @@ func (p *HTTPProvider) callResponsesStream(ctx context.Context, messages []Messa
|
||||
input := make([]map[string]interface{}, 0, len(messages))
|
||||
pendingCalls := map[string]struct{}{}
|
||||
for _, msg := range messages {
|
||||
input = append(input, toResponsesInputItemsWithState(msg, pendingCalls)...)
|
||||
input = append(input, toResponsesInputItemsWithState(msg, pendingCalls, p.crossSessionCallID)...)
|
||||
}
|
||||
requestBody := map[string]interface{}{
|
||||
"model": model,
|
||||
@@ -915,7 +923,7 @@ func (p *HTTPProvider) BuildSummaryViaResponsesCompact(ctx context.Context, mode
|
||||
}
|
||||
pendingCalls := map[string]struct{}{}
|
||||
for _, msg := range messages {
|
||||
input = append(input, toResponsesInputItemsWithState(msg, pendingCalls)...)
|
||||
input = append(input, toResponsesInputItemsWithState(msg, pendingCalls, p.crossSessionCallID)...)
|
||||
}
|
||||
if len(input) == 0 {
|
||||
return strings.TrimSpace(existingSummary), nil
|
||||
@@ -1022,7 +1030,7 @@ func CreateProviderByName(cfg *config.Config, name string) (LLMProvider, error)
|
||||
if len(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) {
|
||||
|
||||
@@ -6,7 +6,7 @@ func TestToResponsesInputItemsWithState_DropsOrphanToolOutputs(t *testing.T) {
|
||||
pending := map[string]struct{}{}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestToResponsesInputItemsWithState_DropsOrphanToolOutputs(t *testing.T) {
|
||||
},
|
||||
}},
|
||||
}
|
||||
items := toResponsesInputItemsWithState(assistant, pending)
|
||||
items := toResponsesInputItemsWithState(assistant, pending, true)
|
||||
if len(items) == 0 {
|
||||
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"}
|
||||
matchedItems := toResponsesInputItemsWithState(matched, pending)
|
||||
matchedItems := toResponsesInputItemsWithState(matched, pending, true)
|
||||
if len(matchedItems) != 1 {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
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_base: '',
|
||||
protocol: 'responses',
|
||||
cross_session_call_id: false,
|
||||
models: [],
|
||||
supports_responses_compact: false,
|
||||
auth: 'bearer',
|
||||
|
||||
Reference in New Issue
Block a user