Merge branch 'sipeed:main' into main
This commit is contained in:
commit
21473d2219
29 changed files with 769 additions and 40 deletions
|
|
@ -34,7 +34,7 @@ func NewGatewayCommand() *cobra.Command {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
RunE: func(_ *cobra.Command, _ []string) error {
|
RunE: func(_ *cobra.Command, _ []string) error {
|
||||||
return gateway.Run(debug, internal.GetConfigPath(), allowEmpty)
|
return gateway.Run(debug, internal.GetPicoclawHome(), internal.GetConfigPath(), allowEmpty)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -678,8 +678,21 @@ func sanitizeHistoryForProvider(history []providers.Message) []providers.Message
|
||||||
// like DeepSeek that enforce: "An assistant message with 'tool_calls' must
|
// like DeepSeek that enforce: "An assistant message with 'tool_calls' must
|
||||||
// be followed by tool messages responding to each 'tool_call_id'."
|
// be followed by tool messages responding to each 'tool_call_id'."
|
||||||
final := make([]providers.Message, 0, len(sanitized))
|
final := make([]providers.Message, 0, len(sanitized))
|
||||||
|
seenToolCallID := make(map[string]bool)
|
||||||
for i := 0; i < len(sanitized); i++ {
|
for i := 0; i < len(sanitized); i++ {
|
||||||
msg := sanitized[i]
|
msg := sanitized[i]
|
||||||
|
|
||||||
|
// Deduplicate tool results by ToolCallID
|
||||||
|
if msg.Role == "tool" && msg.ToolCallID != "" {
|
||||||
|
if seenToolCallID[msg.ToolCallID] {
|
||||||
|
logger.DebugCF("agent", "Dropping duplicate tool result", map[string]any{
|
||||||
|
"tool_call_id": msg.ToolCallID,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seenToolCallID[msg.ToolCallID] = true
|
||||||
|
}
|
||||||
|
|
||||||
if msg.Role == "assistant" && len(msg.ToolCalls) > 0 {
|
if msg.Role == "assistant" && len(msg.ToolCalls) > 0 {
|
||||||
// Collect expected tool_call IDs
|
// Collect expected tool_call IDs
|
||||||
expected := make(map[string]bool, len(msg.ToolCalls))
|
expected := make(map[string]bool, len(msg.ToolCalls))
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,31 @@ func TestSanitizeHistoryForProvider_PlainConversation(t *testing.T) {
|
||||||
assertRoles(t, result, "user", "assistant", "user", "assistant")
|
assertRoles(t, result, "user", "assistant", "user", "assistant")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSanitizeHistoryForProvider_DuplicateToolResults(t *testing.T) {
|
||||||
|
history := []providers.Message{
|
||||||
|
msg("user", "do something"),
|
||||||
|
assistantWithTools("A", "B"),
|
||||||
|
toolResult("A"),
|
||||||
|
toolResult("B"),
|
||||||
|
toolResult("A"), // duplicate
|
||||||
|
toolResult("B"), // duplicate
|
||||||
|
msg("assistant", "done"),
|
||||||
|
}
|
||||||
|
|
||||||
|
result := sanitizeHistoryForProvider(history)
|
||||||
|
if len(result) != 5 {
|
||||||
|
t.Fatalf("expected 5 messages, got %d: %+v", len(result), roles(result))
|
||||||
|
}
|
||||||
|
assertRoles(t, result, "user", "assistant", "tool", "tool", "assistant")
|
||||||
|
// Verify the kept tool results have the correct IDs
|
||||||
|
if result[2].ToolCallID != "A" {
|
||||||
|
t.Errorf("expected tool result A, got %q", result[2].ToolCallID)
|
||||||
|
}
|
||||||
|
if result[3].ToolCallID != "B" {
|
||||||
|
t.Errorf("expected tool result B, got %q", result[3].ToolCallID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func roles(msgs []providers.Message) []string {
|
func roles(msgs []providers.Message) []string {
|
||||||
r := make([]string, len(msgs))
|
r := make([]string, len(msgs))
|
||||||
for i, m := range msgs {
|
for i, m := range msgs {
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ func TestProcessMessage_UseCommandLoadsRequestedSkill(t *testing.T) {
|
||||||
Agents: config.AgentsConfig{
|
Agents: config.AgentsConfig{
|
||||||
Defaults: config.AgentDefaults{
|
Defaults: config.AgentDefaults{
|
||||||
Workspace: tmpDir,
|
Workspace: tmpDir,
|
||||||
Model: "test-model",
|
ModelName: "test-model",
|
||||||
MaxTokens: 4096,
|
MaxTokens: 4096,
|
||||||
MaxToolIterations: 10,
|
MaxToolIterations: 10,
|
||||||
},
|
},
|
||||||
|
|
@ -196,7 +196,7 @@ func TestHandleCommand_UseCommandRejectsUnknownSkill(t *testing.T) {
|
||||||
Agents: config.AgentsConfig{
|
Agents: config.AgentsConfig{
|
||||||
Defaults: config.AgentDefaults{
|
Defaults: config.AgentDefaults{
|
||||||
Workspace: tmpDir,
|
Workspace: tmpDir,
|
||||||
Model: "test-model",
|
ModelName: "test-model",
|
||||||
MaxTokens: 4096,
|
MaxTokens: 4096,
|
||||||
MaxToolIterations: 10,
|
MaxToolIterations: 10,
|
||||||
},
|
},
|
||||||
|
|
@ -240,7 +240,7 @@ func TestProcessMessage_UseCommandArmsSkillForNextMessage(t *testing.T) {
|
||||||
Agents: config.AgentsConfig{
|
Agents: config.AgentsConfig{
|
||||||
Defaults: config.AgentDefaults{
|
Defaults: config.AgentDefaults{
|
||||||
Workspace: tmpDir,
|
Workspace: tmpDir,
|
||||||
Model: "test-model",
|
ModelName: "test-model",
|
||||||
MaxTokens: 4096,
|
MaxTokens: 4096,
|
||||||
MaxToolIterations: 10,
|
MaxToolIterations: 10,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -940,6 +940,7 @@ type ModelConfig struct {
|
||||||
MaxTokensField string `json:"max_tokens_field,omitempty"` // Field name for max tokens (e.g., "max_completion_tokens")
|
MaxTokensField string `json:"max_tokens_field,omitempty"` // Field name for max tokens (e.g., "max_completion_tokens")
|
||||||
RequestTimeout int `json:"request_timeout,omitempty"`
|
RequestTimeout int `json:"request_timeout,omitempty"`
|
||||||
ThinkingLevel string `json:"thinking_level,omitempty"` // Extended thinking: off|low|medium|high|xhigh|adaptive
|
ThinkingLevel string `json:"thinking_level,omitempty"` // Extended thinking: off|low|medium|high|xhigh|adaptive
|
||||||
|
ExtraBody map[string]any `json:"extra_body,omitempty"` // Additional fields to inject into request body
|
||||||
|
|
||||||
// from security
|
// from security
|
||||||
secModelName string
|
secModelName string
|
||||||
|
|
@ -2079,6 +2080,7 @@ func expandMultiKeyModels(models []*ModelConfig) []*ModelConfig {
|
||||||
MaxTokensField: m.MaxTokensField,
|
MaxTokensField: m.MaxTokensField,
|
||||||
RequestTimeout: m.RequestTimeout,
|
RequestTimeout: m.RequestTimeout,
|
||||||
ThinkingLevel: m.ThinkingLevel,
|
ThinkingLevel: m.ThinkingLevel,
|
||||||
|
ExtraBody: m.ExtraBody,
|
||||||
}
|
}
|
||||||
expanded = append(expanded, additionalEntry)
|
expanded = append(expanded, additionalEntry)
|
||||||
fallbackNames = append(fallbackNames, expandedName)
|
fallbackNames = append(fallbackNames, expandedName)
|
||||||
|
|
@ -2097,6 +2099,7 @@ func expandMultiKeyModels(models []*ModelConfig) []*ModelConfig {
|
||||||
MaxTokensField: m.MaxTokensField,
|
MaxTokensField: m.MaxTokensField,
|
||||||
RequestTimeout: m.RequestTimeout,
|
RequestTimeout: m.RequestTimeout,
|
||||||
ThinkingLevel: m.ThinkingLevel,
|
ThinkingLevel: m.ThinkingLevel,
|
||||||
|
ExtraBody: m.ExtraBody,
|
||||||
apiKeys: []string{keys[0]},
|
apiKeys: []string{keys[0]},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1193,3 +1193,62 @@ func TestConfigLogLevelEmpty(t *testing.T) {
|
||||||
t.Errorf("LogLevel = %q, want \"fatal\"", cfg.Gateway.LogLevel)
|
t.Errorf("LogLevel = %q, want \"fatal\"", cfg.Gateway.LogLevel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestModelConfig_ExtraBodyRoundTrip(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
cfgPath := filepath.Join(dir, "config.json")
|
||||||
|
|
||||||
|
cfg := &Config{
|
||||||
|
ModelList: []*ModelConfig{
|
||||||
|
{
|
||||||
|
ModelName: "test-model",
|
||||||
|
Model: "openai/test",
|
||||||
|
apiKeys: []string{"sk-test"},
|
||||||
|
ExtraBody: map[string]any{"custom_field": "value", "num_field": 42},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
security: &SecurityConfig{
|
||||||
|
ModelList: map[string]ModelSecurityEntry{"test-model:0": {APIKeys: []string{"sk-test"}}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := SaveConfig(cfgPath, cfg); err != nil {
|
||||||
|
t.Fatalf("SaveConfig error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
loaded, err := LoadConfig(cfgPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadConfig error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if loaded.ModelList[0].ExtraBody == nil {
|
||||||
|
t.Fatal("ExtraBody should not be nil after round-trip")
|
||||||
|
}
|
||||||
|
if got := loaded.ModelList[0].ExtraBody["custom_field"]; got != "value" {
|
||||||
|
t.Errorf("ExtraBody[custom_field] = %v, want value", got)
|
||||||
|
}
|
||||||
|
if got := loaded.ModelList[0].ExtraBody["num_field"]; got != float64(42) {
|
||||||
|
t.Errorf("ExtraBody[num_field] = %v, want 42", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultConfig_MinimaxExtraBody(t *testing.T) {
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
|
||||||
|
var minimaxCfg *ModelConfig
|
||||||
|
for i := range cfg.ModelList {
|
||||||
|
if cfg.ModelList[i].Model == "minimax/MiniMax-M2.5" {
|
||||||
|
minimaxCfg = cfg.ModelList[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if minimaxCfg == nil {
|
||||||
|
t.Fatal("Minimax model not found in ModelList")
|
||||||
|
}
|
||||||
|
if minimaxCfg.ExtraBody == nil {
|
||||||
|
t.Fatal("Minimax ExtraBody should not be nil")
|
||||||
|
}
|
||||||
|
if got, ok := minimaxCfg.ExtraBody["reasoning_split"]; !ok || got != true {
|
||||||
|
t.Fatalf("Minimax ExtraBody[reasoning_split] = %v, want true", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -339,6 +339,7 @@ func DefaultConfig() *Config {
|
||||||
ModelName: "MiniMax-M2.5",
|
ModelName: "MiniMax-M2.5",
|
||||||
Model: "minimax/MiniMax-M2.5",
|
Model: "minimax/MiniMax-M2.5",
|
||||||
APIBase: "https://api.minimaxi.com/v1",
|
APIBase: "https://api.minimaxi.com/v1",
|
||||||
|
ExtraBody: map[string]any{"reasoning_split": true},
|
||||||
},
|
},
|
||||||
|
|
||||||
// LongCat - https://longcat.chat/platform
|
// LongCat - https://longcat.chat/platform
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,10 @@ const (
|
||||||
serviceShutdownTimeout = 30 * time.Second
|
serviceShutdownTimeout = 30 * time.Second
|
||||||
providerReloadTimeout = 30 * time.Second
|
providerReloadTimeout = 30 * time.Second
|
||||||
gracefulShutdownTimeout = 15 * time.Second
|
gracefulShutdownTimeout = 15 * time.Second
|
||||||
|
|
||||||
|
logPath = "logs"
|
||||||
|
panicFile = "gateway_panic.log"
|
||||||
|
logFile = "gateway.log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type services struct {
|
type services struct {
|
||||||
|
|
@ -79,7 +83,19 @@ func (p *startupBlockedProvider) GetDefaultModel() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run starts the gateway runtime using the configuration loaded from configPath.
|
// Run starts the gateway runtime using the configuration loaded from configPath.
|
||||||
func Run(debug bool, configPath string, allowEmptyStartup bool) error {
|
func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) error {
|
||||||
|
panicPath := filepath.Join(homePath, logPath, panicFile)
|
||||||
|
panicFunc, err := logger.InitPanic(panicPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error initializing panic log: %w", err)
|
||||||
|
}
|
||||||
|
defer panicFunc()
|
||||||
|
|
||||||
|
if err = logger.EnableFileLogging(filepath.Join(homePath, logPath, logFile)); err != nil {
|
||||||
|
panic(fmt.Sprintf("error enabling file logging: %v", err))
|
||||||
|
}
|
||||||
|
defer logger.DisableFileLogging()
|
||||||
|
|
||||||
cfg, err := config.LoadConfig(configPath)
|
cfg, err := config.LoadConfig(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error loading config: %w", err)
|
return fmt.Errorf("error loading config: %w", err)
|
||||||
|
|
|
||||||
36
pkg/logger/panic.go
Normal file
36
pkg/logger/panic.go
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime/debug"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitPanic(filePath string) (func(), error) {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(filePath), 0o755); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create log directory: %w", err)
|
||||||
|
}
|
||||||
|
writer := initPanicFile(filePath)
|
||||||
|
if writer == nil {
|
||||||
|
return nil, fmt.Errorf("failed to create log file: %s", filePath)
|
||||||
|
}
|
||||||
|
return func() {
|
||||||
|
defer writer.Close()
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
now := time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
stack := debug.Stack()
|
||||||
|
logMsg := "\n\n====================\n[" + now + "] PANIC OCCURRED: " + fmt.Sprintf(
|
||||||
|
"%v",
|
||||||
|
err,
|
||||||
|
) + "\n" + string(
|
||||||
|
stack,
|
||||||
|
)
|
||||||
|
|
||||||
|
writer.Write([]byte(logMsg))
|
||||||
|
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
21
pkg/logger/panic_unix.go
Normal file
21
pkg/logger/panic_unix.go
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initPanicFile(panicFile string) io.WriteCloser {
|
||||||
|
file, err := os.OpenFile(panicFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND|os.O_SYNC, 0o600)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("error in open panic: %v", err))
|
||||||
|
}
|
||||||
|
if err = Dup2(int(file.Fd()), int(os.Stderr.Fd())); err != nil {
|
||||||
|
panic(fmt.Sprintf("error in syscall.Dup2: %v", err))
|
||||||
|
}
|
||||||
|
return file
|
||||||
|
}
|
||||||
25
pkg/logger/panic_win.go
Normal file
25
pkg/logger/panic_win.go
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initPanicFile(panicFile string) io.WriteCloser {
|
||||||
|
file, err := os.OpenFile(panicFile, os.O_WRONLY|os.O_CREATE|os.O_SYNC|os.O_APPEND, 0600)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("error in open panic: %v", err))
|
||||||
|
}
|
||||||
|
err = windows.SetStdHandle(windows.STD_ERROR_HANDLE, windows.Handle(file.Fd()))
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("Failed to redirect stderr to file: %v", err))
|
||||||
|
}
|
||||||
|
os.Stderr = file
|
||||||
|
return file
|
||||||
|
}
|
||||||
12
pkg/logger/syscall_amd64.go
Normal file
12
pkg/logger/syscall_amd64.go
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
//go:build linux && amd64
|
||||||
|
// +build linux,amd64
|
||||||
|
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Dup2(oldfd int, newfd int) error {
|
||||||
|
return syscall.Dup2(oldfd, newfd)
|
||||||
|
}
|
||||||
12
pkg/logger/syscall_arm64.go
Normal file
12
pkg/logger/syscall_arm64.go
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
//go:build linux && arm64
|
||||||
|
// +build linux,arm64
|
||||||
|
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Dup2(oldfd int, newfd int) error {
|
||||||
|
return syscall.Dup3(oldfd, newfd, 0)
|
||||||
|
}
|
||||||
12
pkg/logger/syscall_darwin.go
Normal file
12
pkg/logger/syscall_darwin.go
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
//go:build darwin
|
||||||
|
// +build darwin
|
||||||
|
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Dup2(oldfd int, newfd int) error {
|
||||||
|
return syscall.Dup2(oldfd, newfd)
|
||||||
|
}
|
||||||
12
pkg/logger/syscall_loong64.go
Normal file
12
pkg/logger/syscall_loong64.go
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
//go:build linux && loong64
|
||||||
|
// +build linux,loong64
|
||||||
|
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Dup2(oldfd int, newfd int) error {
|
||||||
|
return syscall.Dup3(oldfd, newfd, 0)
|
||||||
|
}
|
||||||
|
|
@ -188,17 +188,23 @@ func buildRequestBody(
|
||||||
|
|
||||||
case "user":
|
case "user":
|
||||||
if msg.ToolCallID != "" {
|
if msg.ToolCallID != "" {
|
||||||
// Tool result message
|
// Tool result message — merge into previous user message if it contains tool_results
|
||||||
content := []map[string]any{
|
toolResultBlock := map[string]any{
|
||||||
{
|
|
||||||
"type": "tool_result",
|
"type": "tool_result",
|
||||||
"tool_use_id": msg.ToolCallID,
|
"tool_use_id": msg.ToolCallID,
|
||||||
"content": msg.Content,
|
"content": msg.Content,
|
||||||
},
|
}
|
||||||
|
if len(apiMessages) > 0 {
|
||||||
|
if prev, ok := apiMessages[len(apiMessages)-1].(map[string]any); ok && prev["role"] == "user" {
|
||||||
|
if content, ok := prev["content"].([]map[string]any); ok {
|
||||||
|
prev["content"] = append(content, toolResultBlock)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
apiMessages = append(apiMessages, map[string]any{
|
apiMessages = append(apiMessages, map[string]any{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": content,
|
"content": []map[string]any{toolResultBlock},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Regular user message
|
// Regular user message
|
||||||
|
|
@ -246,17 +252,23 @@ func buildRequestBody(
|
||||||
})
|
})
|
||||||
|
|
||||||
case "tool":
|
case "tool":
|
||||||
// Tool result (alternative format)
|
// Tool result (alternative format) — merge into previous user message if it contains tool_results
|
||||||
content := []map[string]any{
|
toolResultBlock := map[string]any{
|
||||||
{
|
|
||||||
"type": "tool_result",
|
"type": "tool_result",
|
||||||
"tool_use_id": msg.ToolCallID,
|
"tool_use_id": msg.ToolCallID,
|
||||||
"content": msg.Content,
|
"content": msg.Content,
|
||||||
},
|
}
|
||||||
|
if len(apiMessages) > 0 {
|
||||||
|
if prev, ok := apiMessages[len(apiMessages)-1].(map[string]any); ok && prev["role"] == "user" {
|
||||||
|
if content, ok := prev["content"].([]map[string]any); ok {
|
||||||
|
prev["content"] = append(content, toolResultBlock)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
apiMessages = append(apiMessages, map[string]any{
|
apiMessages = append(apiMessages, map[string]any{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": content,
|
"content": []map[string]any{toolResultBlock},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -562,6 +562,96 @@ func TestBuildRequestBodyEdgeCases(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildRequestBody_ConsecutiveToolResultsMerged(t *testing.T) {
|
||||||
|
// Consecutive tool results (role "tool") should be merged into a single "user" message
|
||||||
|
messages := []Message{
|
||||||
|
{Role: "user", Content: "Use tools"},
|
||||||
|
{Role: "assistant", Content: "", ToolCalls: []ToolCall{
|
||||||
|
{ID: "t1", Name: "tool_a", Arguments: map[string]any{"x": 1}},
|
||||||
|
{ID: "t2", Name: "tool_b", Arguments: map[string]any{"y": 2}},
|
||||||
|
}},
|
||||||
|
{Role: "tool", ToolCallID: "t1", Content: "result1"},
|
||||||
|
{Role: "tool", ToolCallID: "t2", Content: "result2"},
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := buildRequestBody(messages, nil, "test-model", map[string]any{"max_tokens": 8192})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("buildRequestBody() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
apiMessages, ok := got["messages"].([]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("messages is not []any")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expect: user, assistant, user (merged tool results)
|
||||||
|
if len(apiMessages) != 3 {
|
||||||
|
for i, m := range apiMessages {
|
||||||
|
t.Logf("message[%d]: %+v", i, m)
|
||||||
|
}
|
||||||
|
t.Fatalf("expected 3 API messages, got %d", len(apiMessages))
|
||||||
|
}
|
||||||
|
|
||||||
|
// The third message should be a user message with 2 tool_result blocks
|
||||||
|
toolResultMsg, ok := apiMessages[2].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("tool result message is not map[string]any")
|
||||||
|
}
|
||||||
|
if toolResultMsg["role"] != "user" {
|
||||||
|
t.Errorf("expected role 'user', got %v", toolResultMsg["role"])
|
||||||
|
}
|
||||||
|
content, ok := toolResultMsg["content"].([]map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("content is not []map[string]any: %T", toolResultMsg["content"])
|
||||||
|
}
|
||||||
|
if len(content) != 2 {
|
||||||
|
t.Fatalf("expected 2 tool_result blocks, got %d", len(content))
|
||||||
|
}
|
||||||
|
if content[0]["tool_use_id"] != "t1" {
|
||||||
|
t.Errorf("first tool_result tool_use_id = %v, want t1", content[0]["tool_use_id"])
|
||||||
|
}
|
||||||
|
if content[1]["tool_use_id"] != "t2" {
|
||||||
|
t.Errorf("second tool_result tool_use_id = %v, want t2", content[1]["tool_use_id"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildRequestBody_UserToolResultsMerged(t *testing.T) {
|
||||||
|
// Consecutive tool results using role "user" with ToolCallID should also be merged
|
||||||
|
messages := []Message{
|
||||||
|
{Role: "user", Content: "Use tools"},
|
||||||
|
{Role: "assistant", Content: "", ToolCalls: []ToolCall{
|
||||||
|
{ID: "t1", Name: "tool_a", Arguments: map[string]any{"x": 1}},
|
||||||
|
{ID: "t2", Name: "tool_b", Arguments: map[string]any{"y": 2}},
|
||||||
|
}},
|
||||||
|
{Role: "user", ToolCallID: "t1", Content: "result1"},
|
||||||
|
{Role: "user", ToolCallID: "t2", Content: "result2"},
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := buildRequestBody(messages, nil, "test-model", map[string]any{"max_tokens": 8192})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("buildRequestBody() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
apiMessages, ok := got["messages"].([]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("messages is not []any")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expect: user, assistant, user (merged tool results)
|
||||||
|
if len(apiMessages) != 3 {
|
||||||
|
t.Fatalf("expected 3 API messages, got %d", len(apiMessages))
|
||||||
|
}
|
||||||
|
|
||||||
|
toolResultMsg := apiMessages[2].(map[string]any)
|
||||||
|
content, ok := toolResultMsg["content"].([]map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("content is not []map[string]any: %T", toolResultMsg["content"])
|
||||||
|
}
|
||||||
|
if len(content) != 2 {
|
||||||
|
t.Fatalf("expected 2 tool_result blocks, got %d", len(content))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestParseResponseBodyEdgeCases tests edge cases for parseResponseBody.
|
// TestParseResponseBodyEdgeCases tests edge cases for parseResponseBody.
|
||||||
func TestParseResponseBodyEdgeCases(t *testing.T) {
|
func TestParseResponseBodyEdgeCases(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
|
||||||
cfg.Proxy,
|
cfg.Proxy,
|
||||||
cfg.MaxTokensField,
|
cfg.MaxTokensField,
|
||||||
cfg.RequestTimeout,
|
cfg.RequestTimeout,
|
||||||
|
cfg.ExtraBody,
|
||||||
), modelID, nil
|
), modelID, nil
|
||||||
|
|
||||||
case "azure", "azure-openai":
|
case "azure", "azure-openai":
|
||||||
|
|
@ -116,7 +117,7 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
|
||||||
case "litellm", "openrouter", "groq", "zhipu", "gemini", "nvidia",
|
case "litellm", "openrouter", "groq", "zhipu", "gemini", "nvidia",
|
||||||
"ollama", "moonshot", "shengsuanyun", "deepseek", "cerebras",
|
"ollama", "moonshot", "shengsuanyun", "deepseek", "cerebras",
|
||||||
"vivgrid", "volcengine", "vllm", "qwen", "qwen-intl", "qwen-international", "dashscope-intl",
|
"vivgrid", "volcengine", "vllm", "qwen", "qwen-intl", "qwen-international", "dashscope-intl",
|
||||||
"qwen-us", "dashscope-us", "mistral", "avian", "minimax", "longcat", "modelscope", "novita",
|
"qwen-us", "dashscope-us", "mistral", "avian", "longcat", "modelscope", "novita",
|
||||||
"coding-plan", "alibaba-coding", "qwen-coding":
|
"coding-plan", "alibaba-coding", "qwen-coding":
|
||||||
// All other OpenAI-compatible HTTP providers
|
// All other OpenAI-compatible HTTP providers
|
||||||
if cfg.APIKey() == "" && cfg.APIBase == "" {
|
if cfg.APIKey() == "" && cfg.APIBase == "" {
|
||||||
|
|
@ -132,6 +133,32 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
|
||||||
cfg.Proxy,
|
cfg.Proxy,
|
||||||
cfg.MaxTokensField,
|
cfg.MaxTokensField,
|
||||||
cfg.RequestTimeout,
|
cfg.RequestTimeout,
|
||||||
|
cfg.ExtraBody,
|
||||||
|
), modelID, nil
|
||||||
|
|
||||||
|
case "minimax":
|
||||||
|
// Minimax requires reasoning_split: true in the request body
|
||||||
|
if cfg.APIKey() == "" && cfg.APIBase == "" {
|
||||||
|
return nil, "", fmt.Errorf("api_key or api_base is required for HTTP-based protocol %q", protocol)
|
||||||
|
}
|
||||||
|
apiBase := cfg.APIBase
|
||||||
|
if apiBase == "" {
|
||||||
|
apiBase = getDefaultAPIBase(protocol)
|
||||||
|
}
|
||||||
|
extraBody := cfg.ExtraBody
|
||||||
|
if extraBody == nil {
|
||||||
|
extraBody = make(map[string]any)
|
||||||
|
}
|
||||||
|
if _, ok := extraBody["reasoning_split"]; !ok {
|
||||||
|
extraBody["reasoning_split"] = true
|
||||||
|
}
|
||||||
|
return NewHTTPProviderWithMaxTokensFieldAndRequestTimeout(
|
||||||
|
cfg.APIKey(),
|
||||||
|
apiBase,
|
||||||
|
cfg.Proxy,
|
||||||
|
cfg.MaxTokensField,
|
||||||
|
cfg.RequestTimeout,
|
||||||
|
extraBody,
|
||||||
), modelID, nil
|
), modelID, nil
|
||||||
|
|
||||||
case "anthropic":
|
case "anthropic":
|
||||||
|
|
@ -157,6 +184,7 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
|
||||||
cfg.Proxy,
|
cfg.Proxy,
|
||||||
cfg.MaxTokensField,
|
cfg.MaxTokensField,
|
||||||
cfg.RequestTimeout,
|
cfg.RequestTimeout,
|
||||||
|
cfg.ExtraBody,
|
||||||
), modelID, nil
|
), modelID, nil
|
||||||
|
|
||||||
case "anthropic-messages":
|
case "anthropic-messages":
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
package providers
|
package providers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -604,3 +605,98 @@ func TestGetDefaultAPIBase_QwenUSAliases(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateProviderFromConfig_MinimaxInjectsReasoningSplit(t *testing.T) {
|
||||||
|
var requestBody map[string]any
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"ok"},"finish_reason":"stop"}]}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
cfg := &config.ModelConfig{
|
||||||
|
ModelName: "test-minimax",
|
||||||
|
Model: "minimax/MiniMax-M2.5",
|
||||||
|
APIBase: server.URL,
|
||||||
|
}
|
||||||
|
cfg.SetAPIKey("test-key")
|
||||||
|
|
||||||
|
provider, modelID, err := CreateProviderFromConfig(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateProviderFromConfig() error = %v", err)
|
||||||
|
}
|
||||||
|
if provider == nil {
|
||||||
|
t.Fatal("CreateProviderFromConfig() returned nil provider")
|
||||||
|
}
|
||||||
|
if modelID != "MiniMax-M2.5" {
|
||||||
|
t.Errorf("modelID = %q, want %q", modelID, "MiniMax-M2.5")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = provider.Chat(
|
||||||
|
t.Context(),
|
||||||
|
[]Message{{Role: "user", Content: "hi"}},
|
||||||
|
nil,
|
||||||
|
modelID,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Chat() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify reasoning_split is automatically injected
|
||||||
|
if got, ok := requestBody["reasoning_split"]; !ok || got != true {
|
||||||
|
t.Fatalf("reasoning_split = %v, want true", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateProviderFromConfig_MinimaxPreservesUserExtraBody(t *testing.T) {
|
||||||
|
var requestBody map[string]any
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"ok"},"finish_reason":"stop"}]}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
cfg := &config.ModelConfig{
|
||||||
|
ModelName: "test-minimax-custom",
|
||||||
|
Model: "minimax/MiniMax-M2.5",
|
||||||
|
APIBase: server.URL,
|
||||||
|
ExtraBody: map[string]any{"custom_field": "test"},
|
||||||
|
}
|
||||||
|
cfg.SetAPIKey("test-key")
|
||||||
|
|
||||||
|
provider, modelID, err := CreateProviderFromConfig(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateProviderFromConfig() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = provider.Chat(
|
||||||
|
t.Context(),
|
||||||
|
[]Message{{Role: "user", Content: "hi"}},
|
||||||
|
nil,
|
||||||
|
modelID,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Chat() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify reasoning_split is automatically injected
|
||||||
|
if got, ok := requestBody["reasoning_split"]; !ok || got != true {
|
||||||
|
t.Fatalf("reasoning_split = %v, want true", got)
|
||||||
|
}
|
||||||
|
// Verify user's custom field is preserved
|
||||||
|
if got, ok := requestBody["custom_field"]; !ok || got != "test" {
|
||||||
|
t.Fatalf("custom_field = %v, want test", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,13 @@ func NewHTTPProvider(apiKey, apiBase, proxy string) *HTTPProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTPProviderWithMaxTokensField(apiKey, apiBase, proxy, maxTokensField string) *HTTPProvider {
|
func NewHTTPProviderWithMaxTokensField(apiKey, apiBase, proxy, maxTokensField string) *HTTPProvider {
|
||||||
return NewHTTPProviderWithMaxTokensFieldAndRequestTimeout(apiKey, apiBase, proxy, maxTokensField, 0)
|
return NewHTTPProviderWithMaxTokensFieldAndRequestTimeout(apiKey, apiBase, proxy, maxTokensField, 0, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTPProviderWithMaxTokensFieldAndRequestTimeout(
|
func NewHTTPProviderWithMaxTokensFieldAndRequestTimeout(
|
||||||
apiKey, apiBase, proxy, maxTokensField string,
|
apiKey, apiBase, proxy, maxTokensField string,
|
||||||
requestTimeoutSeconds int,
|
requestTimeoutSeconds int,
|
||||||
|
extraBody map[string]any,
|
||||||
) *HTTPProvider {
|
) *HTTPProvider {
|
||||||
return &HTTPProvider{
|
return &HTTPProvider{
|
||||||
delegate: openai_compat.NewProvider(
|
delegate: openai_compat.NewProvider(
|
||||||
|
|
@ -38,6 +39,7 @@ func NewHTTPProviderWithMaxTokensFieldAndRequestTimeout(
|
||||||
proxy,
|
proxy,
|
||||||
openai_compat.WithMaxTokensField(maxTokensField),
|
openai_compat.WithMaxTokensField(maxTokensField),
|
||||||
openai_compat.WithRequestTimeout(time.Duration(requestTimeoutSeconds)*time.Second),
|
openai_compat.WithRequestTimeout(time.Duration(requestTimeoutSeconds)*time.Second),
|
||||||
|
openai_compat.WithExtraBody(extraBody),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ type Provider struct {
|
||||||
apiBase string
|
apiBase string
|
||||||
maxTokensField string // Field name for max tokens (e.g., "max_completion_tokens" for o1/glm models)
|
maxTokensField string // Field name for max tokens (e.g., "max_completion_tokens" for o1/glm models)
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
|
extraBody map[string]any // Additional fields to inject into request body
|
||||||
}
|
}
|
||||||
|
|
||||||
type Option func(*Provider)
|
type Option func(*Provider)
|
||||||
|
|
@ -55,6 +56,12 @@ func WithRequestTimeout(timeout time.Duration) Option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithExtraBody(extraBody map[string]any) Option {
|
||||||
|
return func(p *Provider) {
|
||||||
|
p.extraBody = extraBody
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func NewProvider(apiKey, apiBase, proxy string, opts ...Option) *Provider {
|
func NewProvider(apiKey, apiBase, proxy string, opts ...Option) *Provider {
|
||||||
p := &Provider{
|
p := &Provider{
|
||||||
apiKey: apiKey,
|
apiKey: apiKey,
|
||||||
|
|
@ -140,6 +147,12 @@ func (p *Provider) buildRequestBody(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Merge extra body fields configured per-provider/model.
|
||||||
|
// These are injected last so they take precedence over defaults.
|
||||||
|
for k, v := range p.extraBody {
|
||||||
|
requestBody[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
return requestBody
|
return requestBody
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -610,6 +610,90 @@ func TestProvider_RequestTimeoutOverride(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProviderChat_ExtraBodyInjected(t *testing.T) {
|
||||||
|
var requestBody map[string]any
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp := map[string]any{
|
||||||
|
"choices": []map[string]any{
|
||||||
|
{
|
||||||
|
"message": map[string]any{"content": "ok"},
|
||||||
|
"finish_reason": "stop",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
extraBody := map[string]any{"reasoning_split": true, "custom_field": "test"}
|
||||||
|
p := NewProvider("key", server.URL, "", WithExtraBody(extraBody))
|
||||||
|
|
||||||
|
_, err := p.Chat(
|
||||||
|
t.Context(),
|
||||||
|
[]Message{{Role: "user", Content: "hi"}},
|
||||||
|
nil,
|
||||||
|
"minimax/abab7",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Chat() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, ok := requestBody["reasoning_split"]; !ok || got != true {
|
||||||
|
t.Fatalf("reasoning_split = %v, want true", got)
|
||||||
|
}
|
||||||
|
if got, ok := requestBody["custom_field"]; !ok || got != "test" {
|
||||||
|
t.Fatalf("custom_field = %v, want test", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProviderChat_ExtraBodyOverridesOptions(t *testing.T) {
|
||||||
|
var requestBody map[string]any
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp := map[string]any{
|
||||||
|
"choices": []map[string]any{
|
||||||
|
{
|
||||||
|
"message": map[string]any{"content": "ok"},
|
||||||
|
"finish_reason": "stop",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
extraBody := map[string]any{"temperature": 0.9}
|
||||||
|
p := NewProvider("key", server.URL, "", WithExtraBody(extraBody))
|
||||||
|
|
||||||
|
_, err := p.Chat(
|
||||||
|
t.Context(),
|
||||||
|
[]Message{{Role: "user", Content: "hi"}},
|
||||||
|
nil,
|
||||||
|
"gpt-4o",
|
||||||
|
map[string]any{"temperature": 0.5},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Chat() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtraBody takes precedence over options since it is merged last.
|
||||||
|
if got := requestBody["temperature"]; got != float64(0.9) {
|
||||||
|
t.Fatalf("temperature = %v, want 0.9 (from extraBody, overriding options)", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type roundTripperFunc func(*http.Request) (*http.Response, error)
|
type roundTripperFunc func(*http.Request) (*http.Response, error)
|
||||||
|
|
||||||
func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {
|
func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ type modelResponse struct {
|
||||||
MaxTokensField string `json:"max_tokens_field,omitempty"`
|
MaxTokensField string `json:"max_tokens_field,omitempty"`
|
||||||
RequestTimeout int `json:"request_timeout,omitempty"`
|
RequestTimeout int `json:"request_timeout,omitempty"`
|
||||||
ThinkingLevel string `json:"thinking_level,omitempty"`
|
ThinkingLevel string `json:"thinking_level,omitempty"`
|
||||||
|
ExtraBody map[string]any `json:"extra_body,omitempty"`
|
||||||
// Meta
|
// Meta
|
||||||
Configured bool `json:"configured"`
|
Configured bool `json:"configured"`
|
||||||
IsDefault bool `json:"is_default"`
|
IsDefault bool `json:"is_default"`
|
||||||
|
|
@ -81,6 +82,7 @@ func (h *Handler) handleListModels(w http.ResponseWriter, r *http.Request) {
|
||||||
MaxTokensField: m.MaxTokensField,
|
MaxTokensField: m.MaxTokensField,
|
||||||
RequestTimeout: m.RequestTimeout,
|
RequestTimeout: m.RequestTimeout,
|
||||||
ThinkingLevel: m.ThinkingLevel,
|
ThinkingLevel: m.ThinkingLevel,
|
||||||
|
ExtraBody: m.ExtraBody,
|
||||||
Configured: configured[i],
|
Configured: configured[i],
|
||||||
IsDefault: m.ModelName == defaultModel,
|
IsDefault: m.ModelName == defaultModel,
|
||||||
})
|
})
|
||||||
|
|
@ -183,6 +185,9 @@ func (h *Handler) handleUpdateModel(w http.ResponseWriter, r *http.Request) {
|
||||||
if mc.APIKey() == "" {
|
if mc.APIKey() == "" {
|
||||||
mc.SetAPIKey(cfg.ModelList[idx].APIKey())
|
mc.SetAPIKey(cfg.ModelList[idx].APIKey())
|
||||||
}
|
}
|
||||||
|
if mc.ExtraBody == nil {
|
||||||
|
mc.ExtraBody = cfg.ModelList[idx].ExtraBody
|
||||||
|
}
|
||||||
|
|
||||||
cfg.ModelList[idx] = &mc
|
cfg.ModelList[idx] = &mc
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,10 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
appName = "PicoClaw"
|
appName = "PicoClaw"
|
||||||
|
|
||||||
|
logPath = "logs"
|
||||||
|
panicFile = "launcher_panic.log"
|
||||||
|
logFile = "launcher.log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -72,6 +76,14 @@ func main() {
|
||||||
|
|
||||||
// Initialize logger
|
// Initialize logger
|
||||||
picoHome := utils.GetPicoclawHome()
|
picoHome := utils.GetPicoclawHome()
|
||||||
|
|
||||||
|
f := filepath.Join(picoHome, logPath, panicFile)
|
||||||
|
panicFunc, err := logger.InitPanic(f)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("error initializing panic log: %v", err))
|
||||||
|
}
|
||||||
|
defer panicFunc()
|
||||||
|
|
||||||
// By default, detect terminal to decide console log behavior
|
// By default, detect terminal to decide console log behavior
|
||||||
// If -console-logs flag is explicitly set, it overrides the detection
|
// If -console-logs flag is explicitly set, it overrides the detection
|
||||||
enableConsole := *console
|
enableConsole := *console
|
||||||
|
|
@ -79,11 +91,9 @@ func main() {
|
||||||
// Disable console logging by setting level to Fatal (no output)
|
// Disable console logging by setting level to Fatal (no output)
|
||||||
logger.SetConsoleLevel(logger.FATAL)
|
logger.SetConsoleLevel(logger.FATAL)
|
||||||
|
|
||||||
logPath := filepath.Join(picoHome, "logs", "web.log")
|
f := filepath.Join(picoHome, logPath, logFile)
|
||||||
if err := logger.EnableFileLogging(logPath); err != nil {
|
if err = logger.EnableFileLogging(f); err != nil {
|
||||||
// FIXME: https://github.com/sipeed/picoclaw/issues/1734
|
panic(fmt.Sprintf("error enabling file logging: %v", err))
|
||||||
fmt.Fprintf(os.Stderr, "Failed to initialize logger: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
defer logger.DisableFileLogging()
|
defer logger.DisableFileLogging()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@
|
||||||
"react-i18next": "^16.5.8",
|
"react-i18next": "^16.5.8",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-textarea-autosize": "^8.5.9",
|
"react-textarea-autosize": "^8.5.9",
|
||||||
|
"rehype-raw": "^7.0.0",
|
||||||
|
"rehype-sanitize": "^6.0.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"shadcn": "^4.1.0",
|
"shadcn": "^4.1.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
|
|
|
||||||
127
web/frontend/pnpm-lock.yaml
generated
127
web/frontend/pnpm-lock.yaml
generated
|
|
@ -62,6 +62,12 @@ importers:
|
||||||
react-textarea-autosize:
|
react-textarea-autosize:
|
||||||
specifier: ^8.5.9
|
specifier: ^8.5.9
|
||||||
version: 8.5.9(@types/react@19.2.14)(react@19.2.4)
|
version: 8.5.9(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
rehype-raw:
|
||||||
|
specifier: ^7.0.0
|
||||||
|
version: 7.0.0
|
||||||
|
rehype-sanitize:
|
||||||
|
specifier: ^6.0.0
|
||||||
|
version: 6.0.0
|
||||||
remark-gfm:
|
remark-gfm:
|
||||||
specifier: ^4.0.1
|
specifier: ^4.0.1
|
||||||
version: 4.0.1
|
version: 4.0.1
|
||||||
|
|
@ -2155,6 +2161,10 @@ packages:
|
||||||
resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
|
resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
|
entities@6.0.1:
|
||||||
|
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
|
||||||
|
engines: {node: '>=0.12'}
|
||||||
|
|
||||||
env-paths@2.2.1:
|
env-paths@2.2.1:
|
||||||
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
|
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
@ -2467,12 +2477,30 @@ packages:
|
||||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
hast-util-from-parse5@8.0.3:
|
||||||
|
resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==}
|
||||||
|
|
||||||
|
hast-util-parse-selector@4.0.0:
|
||||||
|
resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
|
||||||
|
|
||||||
|
hast-util-raw@9.1.0:
|
||||||
|
resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==}
|
||||||
|
|
||||||
|
hast-util-sanitize@5.0.2:
|
||||||
|
resolution: {integrity: sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==}
|
||||||
|
|
||||||
hast-util-to-jsx-runtime@2.3.6:
|
hast-util-to-jsx-runtime@2.3.6:
|
||||||
resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==}
|
resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==}
|
||||||
|
|
||||||
|
hast-util-to-parse5@8.0.1:
|
||||||
|
resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==}
|
||||||
|
|
||||||
hast-util-whitespace@3.0.0:
|
hast-util-whitespace@3.0.0:
|
||||||
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
|
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
|
||||||
|
|
||||||
|
hastscript@9.0.1:
|
||||||
|
resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
|
||||||
|
|
||||||
headers-polyfill@4.0.3:
|
headers-polyfill@4.0.3:
|
||||||
resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==}
|
resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==}
|
||||||
|
|
||||||
|
|
@ -2492,6 +2520,9 @@ packages:
|
||||||
html-url-attributes@3.0.1:
|
html-url-attributes@3.0.1:
|
||||||
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
|
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
|
||||||
|
|
||||||
|
html-void-elements@3.0.0:
|
||||||
|
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
||||||
|
|
||||||
http-errors@2.0.1:
|
http-errors@2.0.1:
|
||||||
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
|
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
@ -3141,6 +3172,9 @@ packages:
|
||||||
parse-statements@1.0.11:
|
parse-statements@1.0.11:
|
||||||
resolution: {integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==}
|
resolution: {integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==}
|
||||||
|
|
||||||
|
parse5@7.3.0:
|
||||||
|
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
|
||||||
|
|
||||||
parseurl@1.3.3:
|
parseurl@1.3.3:
|
||||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
@ -3390,6 +3424,12 @@ packages:
|
||||||
resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==}
|
resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
|
|
||||||
|
rehype-raw@7.0.0:
|
||||||
|
resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==}
|
||||||
|
|
||||||
|
rehype-sanitize@6.0.0:
|
||||||
|
resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==}
|
||||||
|
|
||||||
remark-gfm@4.0.1:
|
remark-gfm@4.0.1:
|
||||||
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
|
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
|
||||||
|
|
||||||
|
|
@ -3812,6 +3852,9 @@ packages:
|
||||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
vfile-location@5.0.3:
|
||||||
|
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
|
||||||
|
|
||||||
vfile-message@4.0.3:
|
vfile-message@4.0.3:
|
||||||
resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
|
resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
|
||||||
|
|
||||||
|
|
@ -3862,6 +3905,9 @@ packages:
|
||||||
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
|
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
web-namespaces@2.0.1:
|
||||||
|
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
|
||||||
|
|
||||||
web-streams-polyfill@3.3.3:
|
web-streams-polyfill@3.3.3:
|
||||||
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
@ -5945,6 +5991,8 @@ snapshots:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
tapable: 2.3.0
|
tapable: 2.3.0
|
||||||
|
|
||||||
|
entities@6.0.1: {}
|
||||||
|
|
||||||
env-paths@2.2.1: {}
|
env-paths@2.2.1: {}
|
||||||
|
|
||||||
error-ex@1.3.4:
|
error-ex@1.3.4:
|
||||||
|
|
@ -6318,6 +6366,43 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
|
|
||||||
|
hast-util-from-parse5@8.0.3:
|
||||||
|
dependencies:
|
||||||
|
'@types/hast': 3.0.4
|
||||||
|
'@types/unist': 3.0.3
|
||||||
|
devlop: 1.1.0
|
||||||
|
hastscript: 9.0.1
|
||||||
|
property-information: 7.1.0
|
||||||
|
vfile: 6.0.3
|
||||||
|
vfile-location: 5.0.3
|
||||||
|
web-namespaces: 2.0.1
|
||||||
|
|
||||||
|
hast-util-parse-selector@4.0.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/hast': 3.0.4
|
||||||
|
|
||||||
|
hast-util-raw@9.1.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/hast': 3.0.4
|
||||||
|
'@types/unist': 3.0.3
|
||||||
|
'@ungap/structured-clone': 1.3.0
|
||||||
|
hast-util-from-parse5: 8.0.3
|
||||||
|
hast-util-to-parse5: 8.0.1
|
||||||
|
html-void-elements: 3.0.0
|
||||||
|
mdast-util-to-hast: 13.2.1
|
||||||
|
parse5: 7.3.0
|
||||||
|
unist-util-position: 5.0.0
|
||||||
|
unist-util-visit: 5.1.0
|
||||||
|
vfile: 6.0.3
|
||||||
|
web-namespaces: 2.0.1
|
||||||
|
zwitch: 2.0.4
|
||||||
|
|
||||||
|
hast-util-sanitize@5.0.2:
|
||||||
|
dependencies:
|
||||||
|
'@types/hast': 3.0.4
|
||||||
|
'@ungap/structured-clone': 1.3.0
|
||||||
|
unist-util-position: 5.0.0
|
||||||
|
|
||||||
hast-util-to-jsx-runtime@2.3.6:
|
hast-util-to-jsx-runtime@2.3.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.8
|
||||||
|
|
@ -6338,10 +6423,28 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
hast-util-to-parse5@8.0.1:
|
||||||
|
dependencies:
|
||||||
|
'@types/hast': 3.0.4
|
||||||
|
comma-separated-tokens: 2.0.3
|
||||||
|
devlop: 1.1.0
|
||||||
|
property-information: 7.1.0
|
||||||
|
space-separated-tokens: 2.0.2
|
||||||
|
web-namespaces: 2.0.1
|
||||||
|
zwitch: 2.0.4
|
||||||
|
|
||||||
hast-util-whitespace@3.0.0:
|
hast-util-whitespace@3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/hast': 3.0.4
|
'@types/hast': 3.0.4
|
||||||
|
|
||||||
|
hastscript@9.0.1:
|
||||||
|
dependencies:
|
||||||
|
'@types/hast': 3.0.4
|
||||||
|
comma-separated-tokens: 2.0.3
|
||||||
|
hast-util-parse-selector: 4.0.0
|
||||||
|
property-information: 7.1.0
|
||||||
|
space-separated-tokens: 2.0.2
|
||||||
|
|
||||||
headers-polyfill@4.0.3: {}
|
headers-polyfill@4.0.3: {}
|
||||||
|
|
||||||
hermes-estree@0.25.1: {}
|
hermes-estree@0.25.1: {}
|
||||||
|
|
@ -6358,6 +6461,8 @@ snapshots:
|
||||||
|
|
||||||
html-url-attributes@3.0.1: {}
|
html-url-attributes@3.0.1: {}
|
||||||
|
|
||||||
|
html-void-elements@3.0.0: {}
|
||||||
|
|
||||||
http-errors@2.0.1:
|
http-errors@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
depd: 2.0.0
|
depd: 2.0.0
|
||||||
|
|
@ -7135,6 +7240,10 @@ snapshots:
|
||||||
|
|
||||||
parse-statements@1.0.11: {}
|
parse-statements@1.0.11: {}
|
||||||
|
|
||||||
|
parse5@7.3.0:
|
||||||
|
dependencies:
|
||||||
|
entities: 6.0.1
|
||||||
|
|
||||||
parseurl@1.3.3: {}
|
parseurl@1.3.3: {}
|
||||||
|
|
||||||
path-browserify@1.0.1: {}
|
path-browserify@1.0.1: {}
|
||||||
|
|
@ -7369,6 +7478,17 @@ snapshots:
|
||||||
tiny-invariant: 1.3.3
|
tiny-invariant: 1.3.3
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
rehype-raw@7.0.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/hast': 3.0.4
|
||||||
|
hast-util-raw: 9.1.0
|
||||||
|
vfile: 6.0.3
|
||||||
|
|
||||||
|
rehype-sanitize@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/hast': 3.0.4
|
||||||
|
hast-util-sanitize: 5.0.2
|
||||||
|
|
||||||
remark-gfm@4.0.1:
|
remark-gfm@4.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/mdast': 4.0.4
|
'@types/mdast': 4.0.4
|
||||||
|
|
@ -7860,6 +7980,11 @@ snapshots:
|
||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
|
|
||||||
|
vfile-location@5.0.3:
|
||||||
|
dependencies:
|
||||||
|
'@types/unist': 3.0.3
|
||||||
|
vfile: 6.0.3
|
||||||
|
|
||||||
vfile-message@4.0.3:
|
vfile-message@4.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
|
|
@ -7887,6 +8012,8 @@ snapshots:
|
||||||
|
|
||||||
void-elements@3.1.0: {}
|
void-elements@3.1.0: {}
|
||||||
|
|
||||||
|
web-namespaces@2.0.1: {}
|
||||||
|
|
||||||
web-streams-polyfill@3.3.3: {}
|
web-streams-polyfill@3.3.3: {}
|
||||||
|
|
||||||
webpack-virtual-modules@0.6.2: {}
|
webpack-virtual-modules@0.6.2: {}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ export interface ModelInfo {
|
||||||
max_tokens_field?: string
|
max_tokens_field?: string
|
||||||
request_timeout?: number
|
request_timeout?: number
|
||||||
thinking_level?: string
|
thinking_level?: string
|
||||||
|
extra_body?: Record<string, unknown>
|
||||||
// Meta
|
// Meta
|
||||||
configured: boolean
|
configured: boolean
|
||||||
is_default: boolean
|
is_default: boolean
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { IconCheck, IconCopy } from "@tabler/icons-react"
|
import { IconCheck, IconCopy } from "@tabler/icons-react"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import ReactMarkdown from "react-markdown"
|
import ReactMarkdown from "react-markdown"
|
||||||
|
import rehypeRaw from "rehype-raw"
|
||||||
|
import rehypeSanitize from "rehype-sanitize"
|
||||||
import remarkGfm from "remark-gfm"
|
import remarkGfm from "remark-gfm"
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
|
|
@ -42,7 +44,12 @@ export function AssistantMessage({
|
||||||
|
|
||||||
<div className="bg-card text-card-foreground relative overflow-hidden rounded-xl border">
|
<div className="bg-card text-card-foreground relative overflow-hidden rounded-xl border">
|
||||||
<div className="prose dark:prose-invert prose-p:my-2 prose-pre:my-2 prose-pre:rounded-lg prose-pre:border prose-pre:bg-zinc-950 prose-pre:p-3 max-w-none p-4 text-[15px] leading-relaxed">
|
<div className="prose dark:prose-invert prose-p:my-2 prose-pre:my-2 prose-pre:rounded-lg prose-pre:border prose-pre:bg-zinc-950 prose-pre:p-3 max-w-none p-4 text-[15px] leading-relaxed">
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
rehypePlugins={[rehypeRaw, rehypeSanitize]}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
import { type ChangeEvent, useRef, useState } from "react"
|
import { type ChangeEvent, useRef, useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import ReactMarkdown from "react-markdown"
|
import ReactMarkdown from "react-markdown"
|
||||||
|
import rehypeRaw from "rehype-raw"
|
||||||
|
import rehypeSanitize from "rehype-sanitize"
|
||||||
import remarkGfm from "remark-gfm"
|
import remarkGfm from "remark-gfm"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
|
|
@ -260,7 +262,10 @@ export function SkillsPage() {
|
||||||
) : selectedSkillDetail ? (
|
) : selectedSkillDetail ? (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<div className="prose prose-sm dark:prose-invert prose-pre:rounded-lg prose-pre:border prose-pre:bg-zinc-950 prose-pre:p-3 max-w-none">
|
<div className="prose prose-sm dark:prose-invert prose-pre:rounded-lg prose-pre:border prose-pre:bg-zinc-950 prose-pre:p-3 max-w-none">
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
rehypePlugins={[rehypeRaw, rehypeSanitize]}
|
||||||
|
>
|
||||||
{selectedSkillDetail.content}
|
{selectedSkillDetail.content}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue