Merge branch 'main' into fix-anthropic-empty-assistant-msgs-14206613713378994992

This commit is contained in:
Nhat Tan 2026-03-28 22:38:26 +07:00 committed by GitHub
commit d056239be7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 42 additions and 22 deletions

View file

@ -28,13 +28,16 @@ jobs:
sudo apt-get install -y libolm-dev sudo apt-get install -y libolm-dev
- name: Build Go backend - name: Build Go backend
run: make build run: |
make build
make build-launcher-tui
- name: Determine binary name - name: Determine binary name
id: binary id: binary
run: | run: |
# The Makefile outputs a binary named picoclaw-linux-amd64 # The Makefile outputs a binary named picoclaw-linux-amd64
echo "path=build/picoclaw-linux-amd64" >> $GITHUB_OUTPUT echo "path=build/picoclaw-linux-amd64" >> $GITHUB_OUTPUT
echo "tui_path=build/picoclaw-launcher-tui-linux-amd64" >> $GITHUB_OUTPUT
- id: 'auth' - id: 'auth'
name: 'Authenticate to Google Cloud' name: 'Authenticate to Google Cloud'
@ -47,7 +50,7 @@ jobs:
uses: 'google-github-actions/setup-gcloud@v2' uses: 'google-github-actions/setup-gcloud@v2'
- name: Verify build artifact - name: Verify build artifact
run: ls -lh ${{ steps.binary.outputs.path }} run: ls -lh ${{ steps.binary.outputs.path }} ${{ steps.binary.outputs.tui_path }}
- name: 'Deploy binary to GCE' - name: 'Deploy binary to GCE'
run: | run: |
@ -57,13 +60,16 @@ jobs:
--project=${{ secrets.GCP_PROJECT_ID }} \ --project=${{ secrets.GCP_PROJECT_ID }} \
--command="mkdir -p ~/.local/bin" --command="mkdir -p ~/.local/bin"
# Copy the binary to the instance # Copy the binaries to the instance
gcloud compute scp ${{ steps.binary.outputs.path }} ${{ secrets.GCE_INSTANCE }}:~/.local/bin/picoclaw \ gcloud compute scp ${{ steps.binary.outputs.path }} ${{ secrets.GCE_INSTANCE }}:~/.local/bin/picoclaw \
--zone=${{ secrets.GCE_ZONE }} \ --zone=${{ secrets.GCE_ZONE }} \
--project=${{ secrets.GCP_PROJECT_ID }} --project=${{ secrets.GCP_PROJECT_ID }}
gcloud compute scp ${{ steps.binary.outputs.tui_path }} ${{ secrets.GCE_INSTANCE }}:~/.local/bin/picoclaw-launcher-tui \
--zone=${{ secrets.GCE_ZONE }} \
--project=${{ secrets.GCP_PROJECT_ID }}
# Make it executable and restart the service # Make it executable and restart the gateway service via command line
gcloud compute ssh ${{ secrets.GCE_INSTANCE }} \ gcloud compute ssh ${{ secrets.GCE_INSTANCE }} \
--zone=${{ secrets.GCE_ZONE }} \ --zone=${{ secrets.GCE_ZONE }} \
--project=${{ secrets.GCP_PROJECT_ID }} \ --project=${{ secrets.GCP_PROJECT_ID }} \
--command="chmod +x ~/.local/bin/picoclaw && sudo systemctl restart picoclaw" --command="chmod +x ~/.local/bin/picoclaw ~/.local/bin/picoclaw-launcher-tui && pkill -f 'picoclaw gate -E' || true && nohup ~/.local/bin/picoclaw gate -E > /dev/null 2>&1 &"

View file

@ -1048,14 +1048,14 @@ type SearXNGConfig struct {
} }
type GLMSearchConfig struct { type GLMSearchConfig struct {
Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_GLM_ENABLED"` Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_GLM_ENABLED"`
apiKey string apiKey string
secDirty bool secDirty bool
BaseURL string `json:"base_url" env:"PICOCLAW_TOOLS_WEB_GLM_BASE_URL"` BaseURL string `json:"base_url" env:"PICOCLAW_TOOLS_WEB_GLM_BASE_URL"`
// SearchEngine specifies the search backend: "search_std" (default), // SearchEngine specifies the search backend: "search_std" (default),
// "search_pro", "search_pro_sogou", or "search_pro_quark". // "search_pro", "search_pro_sogou", or "search_pro_quark".
SearchEngine string `json:"search_engine" env:"PICOCLAW_TOOLS_WEB_GLM_SEARCH_ENGINE"` SearchEngine string `json:"search_engine" env:"PICOCLAW_TOOLS_WEB_GLM_SEARCH_ENGINE"`
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_GLM_MAX_RESULTS"` MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_GLM_MAX_RESULTS"`
} }
// APIKey returns the GLM search API key // APIKey returns the GLM search API key

View file

@ -239,6 +239,7 @@ func (c *Config) MergeAndApplySecurity(newer *SecurityConfig) error {
c.security = mergeSecurityConfig(c.security, newer) c.security = mergeSecurityConfig(c.security, newer)
return applySecurityConfig(c, c.security) return applySecurityConfig(c, c.security)
} }
// mergeSecurityConfig merges two SecurityConfig instances, preferring non-empty values from 'newer'. // mergeSecurityConfig merges two SecurityConfig instances, preferring non-empty values from 'newer'.
// This is used during config migration to preserve existing security data while adding new entries. // This is used during config migration to preserve existing security data while adding new entries.
func mergeSecurityConfig(existing, newer *SecurityConfig) *SecurityConfig { func mergeSecurityConfig(existing, newer *SecurityConfig) *SecurityConfig {
@ -335,7 +336,8 @@ func mergeChannelsSecurity(dst, src *ChannelsSecurity) {
if src.Pico != nil && src.Pico.Token != "" { if src.Pico != nil && src.Pico.Token != "" {
dst.Pico = src.Pico dst.Pico = src.Pico
} }
if src.IRC != nil && (src.IRC.Password != "" || src.IRC.NickServPassword != "" || src.IRC.SASLPassword != "") { if src.IRC != nil &&
(src.IRC.Password != "" || src.IRC.NickServPassword != "" || src.IRC.SASLPassword != "") {
dst.IRC = src.IRC dst.IRC = src.IRC
} }
} }

View file

@ -58,8 +58,8 @@ func NewProviderWithTimeout(apiKey, apiBase string, timeoutSeconds int) *Provide
} }
return &Provider{ return &Provider{
apiKey: apiKey, apiKey: apiKey,
apiBase: baseURL, apiBase: baseURL,
httpClient: common.NewHTTPClientWithTimeout("", timeout), httpClient: common.NewHTTPClientWithTimeout("", timeout),
} }
} }
@ -102,7 +102,10 @@ func (p *Provider) Chat(
// Set headers // Set headers
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-API-Key", p.apiKey) //nolint:canonicalheader // Anthropic API requires exact header name req.Header.Set(
"X-Api-Key",
p.apiKey,
)
req.Header.Set("Anthropic-Version", defaultAPIVersion) req.Header.Set("Anthropic-Version", defaultAPIVersion)
// Execute request // Execute request
@ -134,7 +137,11 @@ func (p *Provider) Chat(
return nil, fmt.Errorf("service unavailable (503): %s", string(body)) return nil, fmt.Errorf("service unavailable (503): %s", string(body))
default: default:
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body)) return nil, fmt.Errorf(
"API request failed with status %d: %s",
resp.StatusCode,
string(body),
)
} }
} }
@ -194,7 +201,8 @@ func buildRequestBody(
"content": msg.Content, "content": msg.Content,
} }
if len(apiMessages) > 0 { if len(apiMessages) > 0 {
if prev, ok := apiMessages[len(apiMessages)-1].(map[string]any); ok && prev["role"] == "user" { if prev, ok := apiMessages[len(apiMessages)-1].(map[string]any); ok &&
prev["role"] == "user" {
if content, ok := prev["content"].([]map[string]any); ok { if content, ok := prev["content"].([]map[string]any); ok {
prev["content"] = append(content, toolResultBlock) prev["content"] = append(content, toolResultBlock)
continue continue
@ -266,7 +274,8 @@ func buildRequestBody(
"content": msg.Content, "content": msg.Content,
} }
if len(apiMessages) > 0 { if len(apiMessages) > 0 {
if prev, ok := apiMessages[len(apiMessages)-1].(map[string]any); ok && prev["role"] == "user" { if prev, ok := apiMessages[len(apiMessages)-1].(map[string]any); ok &&
prev["role"] == "user" {
if content, ok := prev["content"].([]map[string]any); ok { if content, ok := prev["content"].([]map[string]any); ok {
prev["content"] = append(content, toolResultBlock) prev["content"] = append(content, toolResultBlock)
continue continue
@ -318,7 +327,10 @@ func parseResponseBody(body []byte) (*LLMResponse, error) {
// Extract content and tool calls // Extract content and tool calls
var content strings.Builder var content strings.Builder
toolCalls := make([]ToolCall, 0) // Initialize as empty slice (not nil) for consistent JSON serialization toolCalls := make(
[]ToolCall,
0,
) // Initialize as empty slice (not nil) for consistent JSON serialization
for _, block := range resp.Content { for _, block := range resp.Content {
switch block.Type { switch block.Type {

View file

@ -48,8 +48,8 @@ func (lrt *LoggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, er
// Optional: Log Response Body // Optional: Log Response Body
if res.Body != nil { if res.Body != nil {
bodyBytes, err := io.ReadAll(res.Body) bodyBytes, readErr := io.ReadAll(res.Body)
if err == nil { if readErr == nil {
res.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) res.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
// Truncate response body if too long to avoid flooding logs // Truncate response body if too long to avoid flooding logs
respStr := string(bodyBytes) respStr := string(bodyBytes)

View file

@ -67,7 +67,7 @@ func (h *Handler) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
// Intercept explicitly provided security tokens from JSON payload that json.Unmarshal drops. // Intercept explicitly provided security tokens from JSON payload that json.Unmarshal drops.
var incomingSec config.SecurityConfig var incomingSec config.SecurityConfig
if err := json.Unmarshal(body, &incomingSec); err == nil { if err := json.Unmarshal(body, &incomingSec); err == nil { //nolint:musttag // SecurityConfig uses yaml tags
cfg.MergeAndApplySecurity(&incomingSec) cfg.MergeAndApplySecurity(&incomingSec)
} else { } else {
cfg.ApplySecurity() cfg.ApplySecurity()
@ -164,9 +164,9 @@ func (h *Handler) handlePatchConfig(w http.ResponseWriter, r *http.Request) {
// Restore security fields from existing config and merge explicitly provided overrides. // Restore security fields from existing config and merge explicitly provided overrides.
newCfg.SecurityCopyFrom(cfg) newCfg.SecurityCopyFrom(cfg)
var incomingSec config.SecurityConfig var patchSec config.SecurityConfig
if err := json.Unmarshal(patchBody, &incomingSec); err == nil { if err := json.Unmarshal(patchBody, &patchSec); err == nil { //nolint:musttag // SecurityConfig uses yaml tags
newCfg.MergeAndApplySecurity(&incomingSec) newCfg.MergeAndApplySecurity(&patchSec)
} else { } else {
newCfg.ApplySecurity() newCfg.ApplySecurity()
} }