Merge branch 'main' into fix-anthropic-empty-assistant-msgs-14206613713378994992
This commit is contained in:
commit
d056239be7
6 changed files with 42 additions and 22 deletions
16
.github/workflows/deploy-gce.yml
vendored
16
.github/workflows/deploy-gce.yml
vendored
|
|
@ -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 &"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue