refactor(providers): reorganize provider packages and facades
This commit is contained in:
parent
72f30c58e9
commit
ee634dc8db
29 changed files with 573 additions and 102 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
package providers
|
package cliprovider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//go:build integration
|
//go:build integration
|
||||||
|
|
||||||
package providers
|
package cliprovider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package providers
|
package cliprovider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -9,8 +9,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- Compile-time interface check ---
|
// --- Compile-time interface check ---
|
||||||
|
|
@ -409,83 +407,6 @@ func TestChat_EmptyWorkspaceDoesNotSetDir(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- CreateProvider factory tests ---
|
|
||||||
|
|
||||||
func TestCreateProvider_ClaudeCli(t *testing.T) {
|
|
||||||
cfg := config.DefaultConfig()
|
|
||||||
cfg.ModelList = []*config.ModelConfig{
|
|
||||||
{ModelName: "claude-sonnet-4.6", Model: "claude-cli/claude-sonnet-4.6", Workspace: "/test/ws"},
|
|
||||||
}
|
|
||||||
cfg.Agents.Defaults.ModelName = "claude-sonnet-4.6"
|
|
||||||
|
|
||||||
provider, _, err := CreateProvider(cfg)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CreateProvider(claude-cli) error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cliProvider, ok := provider.(*ClaudeCliProvider)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("CreateProvider(claude-cli) returned %T, want *ClaudeCliProvider", provider)
|
|
||||||
}
|
|
||||||
if cliProvider.workspace != "/test/ws" {
|
|
||||||
t.Errorf("workspace = %q, want %q", cliProvider.workspace, "/test/ws")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateProvider_ClaudeCode(t *testing.T) {
|
|
||||||
cfg := config.DefaultConfig()
|
|
||||||
cfg.ModelList = []*config.ModelConfig{
|
|
||||||
{ModelName: "claude-code", Model: "claude-cli/claude-code"},
|
|
||||||
}
|
|
||||||
cfg.Agents.Defaults.ModelName = "claude-code"
|
|
||||||
|
|
||||||
provider, _, err := CreateProvider(cfg)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CreateProvider(claude-code) error = %v", err)
|
|
||||||
}
|
|
||||||
if _, ok := provider.(*ClaudeCliProvider); !ok {
|
|
||||||
t.Fatalf("CreateProvider(claude-code) returned %T, want *ClaudeCliProvider", provider)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateProvider_ClaudeCodec(t *testing.T) {
|
|
||||||
cfg := config.DefaultConfig()
|
|
||||||
cfg.ModelList = []*config.ModelConfig{
|
|
||||||
{ModelName: "claudecode", Model: "claude-cli/claudecode"},
|
|
||||||
}
|
|
||||||
cfg.Agents.Defaults.ModelName = "claudecode"
|
|
||||||
|
|
||||||
provider, _, err := CreateProvider(cfg)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CreateProvider(claudecode) error = %v", err)
|
|
||||||
}
|
|
||||||
if _, ok := provider.(*ClaudeCliProvider); !ok {
|
|
||||||
t.Fatalf("CreateProvider(claudecode) returned %T, want *ClaudeCliProvider", provider)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateProvider_ClaudeCliDefaultWorkspace(t *testing.T) {
|
|
||||||
cfg := config.DefaultConfig()
|
|
||||||
cfg.ModelList = []*config.ModelConfig{
|
|
||||||
{ModelName: "claude-cli", Model: "claude-cli/claude-sonnet"},
|
|
||||||
}
|
|
||||||
cfg.Agents.Defaults.ModelName = "claude-cli"
|
|
||||||
cfg.Agents.Defaults.Workspace = ""
|
|
||||||
|
|
||||||
provider, _, err := CreateProvider(cfg)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CreateProvider error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cliProvider, ok := provider.(*ClaudeCliProvider)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("returned %T, want *ClaudeCliProvider", provider)
|
|
||||||
}
|
|
||||||
if cliProvider.workspace != "." {
|
|
||||||
t.Errorf("workspace = %q, want %q (default)", cliProvider.workspace, ".")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- messagesToPrompt tests ---
|
// --- messagesToPrompt tests ---
|
||||||
|
|
||||||
func TestMessagesToPrompt_SingleUser(t *testing.T) {
|
func TestMessagesToPrompt_SingleUser(t *testing.T) {
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package providers
|
package cliprovider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package providers
|
package cliprovider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package providers
|
package cliprovider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//go:build integration
|
//go:build integration
|
||||||
|
|
||||||
package providers
|
package cliprovider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package providers
|
package cliprovider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
@ -400,6 +401,9 @@ func TestCodexCliProvider_GetDefaultModel(t *testing.T) {
|
||||||
|
|
||||||
func createMockCodexCLI(t *testing.T, events []string) string {
|
func createMockCodexCLI(t *testing.T, events []string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("mock CLI scripts not supported on Windows")
|
||||||
|
}
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
scriptPath := filepath.Join(tmpDir, "codex")
|
scriptPath := filepath.Join(tmpDir, "codex")
|
||||||
|
|
||||||
|
|
@ -471,6 +475,9 @@ func TestCodexCliProvider_MockCLI_Error(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCodexCliProvider_MockCLI_WithModel(t *testing.T) {
|
func TestCodexCliProvider_MockCLI_WithModel(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("mock CLI scripts not supported on Windows")
|
||||||
|
}
|
||||||
// Mock script that captures args to verify model flag is passed
|
// Mock script that captures args to verify model flag is passed
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
scriptPath := filepath.Join(tmpDir, "codex")
|
scriptPath := filepath.Join(tmpDir, "codex")
|
||||||
|
|
@ -517,6 +524,9 @@ echo '{"type":"turn.completed"}'`
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCodexCliProvider_MockCLI_ContextCancel(t *testing.T) {
|
func TestCodexCliProvider_MockCLI_ContextCancel(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("mock CLI scripts not supported on Windows")
|
||||||
|
}
|
||||||
// Script that sleeps forever
|
// Script that sleeps forever
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
scriptPath := filepath.Join(tmpDir, "codex")
|
scriptPath := filepath.Join(tmpDir, "codex")
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package providers
|
package cliprovider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package providers
|
package cliprovider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
//
|
//
|
||||||
// Copyright (c) 2026 PicoClaw contributors
|
// Copyright (c) 2026 PicoClaw contributors
|
||||||
|
|
||||||
package providers
|
package cliprovider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
28
pkg/providers/cli/types.go
Normal file
28
pkg/providers/cli/types.go
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
package cliprovider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
ToolCall = protocoltypes.ToolCall
|
||||||
|
FunctionCall = protocoltypes.FunctionCall
|
||||||
|
LLMResponse = protocoltypes.LLMResponse
|
||||||
|
UsageInfo = protocoltypes.UsageInfo
|
||||||
|
Message = protocoltypes.Message
|
||||||
|
ToolDefinition = protocoltypes.ToolDefinition
|
||||||
|
ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition
|
||||||
|
)
|
||||||
|
|
||||||
|
type LLMProvider interface {
|
||||||
|
Chat(
|
||||||
|
ctx context.Context,
|
||||||
|
messages []Message,
|
||||||
|
tools []ToolDefinition,
|
||||||
|
model string,
|
||||||
|
options map[string]any,
|
||||||
|
) (*LLMResponse, error)
|
||||||
|
GetDefaultModel() string
|
||||||
|
}
|
||||||
40
pkg/providers/cli_facade.go
Normal file
40
pkg/providers/cli_facade.go
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
cliprovider "github.com/sipeed/picoclaw/pkg/providers/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
ClaudeCliProvider = cliprovider.ClaudeCliProvider
|
||||||
|
CodexCliProvider = cliprovider.CodexCliProvider
|
||||||
|
CodexCliAuth = cliprovider.CodexCliAuth
|
||||||
|
GitHubCopilotProvider = cliprovider.GitHubCopilotProvider
|
||||||
|
)
|
||||||
|
|
||||||
|
const CodexHomeEnvVar = cliprovider.CodexHomeEnvVar
|
||||||
|
|
||||||
|
func NewClaudeCliProvider(workspace string) *ClaudeCliProvider {
|
||||||
|
return cliprovider.NewClaudeCliProvider(workspace)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCodexCliProvider(workspace string) *CodexCliProvider {
|
||||||
|
return cliprovider.NewCodexCliProvider(workspace)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGitHubCopilotProvider(uri string, connectMode string, model string) (*GitHubCopilotProvider, error) {
|
||||||
|
return cliprovider.NewGitHubCopilotProvider(uri, connectMode, model)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadCodexCliCredentials() (accessToken, accountID string, expiresAt time.Time, err error) {
|
||||||
|
return cliprovider.ReadCodexCliCredentials()
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateCodexCliTokenSource() func() (string, string, error) {
|
||||||
|
return cliprovider.CreateCodexCliTokenSource()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NormalizeToolCall(tc ToolCall) ToolCall {
|
||||||
|
return cliprovider.NormalizeToolCall(tc)
|
||||||
|
}
|
||||||
99
pkg/providers/cli_factory_test.go
Normal file
99
pkg/providers/cli_factory_test.go
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testProviderWorkspace(t *testing.T, provider any) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
v := reflect.ValueOf(provider)
|
||||||
|
if v.Kind() != reflect.Ptr || v.IsNil() {
|
||||||
|
t.Fatalf("provider = %T, want non-nil pointer", provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
field := v.Elem().FieldByName("workspace")
|
||||||
|
if !field.IsValid() || field.Kind() != reflect.String {
|
||||||
|
t.Fatalf("provider %T does not expose workspace field", provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
return field.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateProvider_ClaudeCli(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.ModelList = []*config.ModelConfig{
|
||||||
|
{ModelName: "claude-sonnet-4.6", Model: "claude-cli/claude-sonnet-4.6", Workspace: "/test/ws"},
|
||||||
|
}
|
||||||
|
cfg.Agents.Defaults.ModelName = "claude-sonnet-4.6"
|
||||||
|
|
||||||
|
provider, _, err := CreateProvider(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateProvider(claude-cli) error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cliProvider, ok := provider.(*ClaudeCliProvider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("CreateProvider(claude-cli) returned %T, want *ClaudeCliProvider", provider)
|
||||||
|
}
|
||||||
|
if got := testProviderWorkspace(t, cliProvider); got != "/test/ws" {
|
||||||
|
t.Errorf("workspace = %q, want %q", got, "/test/ws")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateProvider_ClaudeCode(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.ModelList = []*config.ModelConfig{
|
||||||
|
{ModelName: "claude-code", Model: "claude-cli/claude-code"},
|
||||||
|
}
|
||||||
|
cfg.Agents.Defaults.ModelName = "claude-code"
|
||||||
|
|
||||||
|
provider, _, err := CreateProvider(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateProvider(claude-code) error = %v", err)
|
||||||
|
}
|
||||||
|
if _, ok := provider.(*ClaudeCliProvider); !ok {
|
||||||
|
t.Fatalf("CreateProvider(claude-code) returned %T, want *ClaudeCliProvider", provider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateProvider_ClaudeCodec(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.ModelList = []*config.ModelConfig{
|
||||||
|
{ModelName: "claudecode", Model: "claude-cli/claudecode"},
|
||||||
|
}
|
||||||
|
cfg.Agents.Defaults.ModelName = "claudecode"
|
||||||
|
|
||||||
|
provider, _, err := CreateProvider(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateProvider(claudecode) error = %v", err)
|
||||||
|
}
|
||||||
|
if _, ok := provider.(*ClaudeCliProvider); !ok {
|
||||||
|
t.Fatalf("CreateProvider(claudecode) returned %T, want *ClaudeCliProvider", provider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateProvider_ClaudeCliDefaultWorkspace(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.ModelList = []*config.ModelConfig{
|
||||||
|
{ModelName: "claude-cli", Model: "claude-cli/claude-sonnet"},
|
||||||
|
}
|
||||||
|
cfg.Agents.Defaults.ModelName = "claude-cli"
|
||||||
|
cfg.Agents.Defaults.Workspace = ""
|
||||||
|
|
||||||
|
provider, _, err := CreateProvider(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateProvider error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cliProvider, ok := provider.(*ClaudeCliProvider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("returned %T, want *ClaudeCliProvider", provider)
|
||||||
|
}
|
||||||
|
if got := testProviderWorkspace(t, cliProvider); got != "." {
|
||||||
|
t.Errorf("workspace = %q, want %q (default)", got, ".")
|
||||||
|
}
|
||||||
|
}
|
||||||
52
pkg/providers/facade_compat_test.go
Normal file
52
pkg/providers/facade_compat_test.go
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
cliprovider "github.com/sipeed/picoclaw/pkg/providers/cli"
|
||||||
|
oauthprovider "github.com/sipeed/picoclaw/pkg/providers/oauth"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNormalizeToolCallFacadeMatchesCLIProvider(t *testing.T) {
|
||||||
|
input := ToolCall{
|
||||||
|
ID: "call_1",
|
||||||
|
Type: "function",
|
||||||
|
Function: &FunctionCall{
|
||||||
|
Name: "read_file",
|
||||||
|
Arguments: `{"path":"README.md"}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := NormalizeToolCall(input)
|
||||||
|
want := cliprovider.NormalizeToolCall(input)
|
||||||
|
|
||||||
|
if got.Name != want.Name {
|
||||||
|
t.Fatalf("Name = %q, want %q", got.Name, want.Name)
|
||||||
|
}
|
||||||
|
if got.Function == nil || want.Function == nil {
|
||||||
|
t.Fatalf("Function should not be nil: got=%v want=%v", got.Function, want.Function)
|
||||||
|
}
|
||||||
|
if got.Function.Name != want.Function.Name {
|
||||||
|
t.Fatalf("Function.Name = %q, want %q", got.Function.Name, want.Function.Name)
|
||||||
|
}
|
||||||
|
if got.Function.Arguments != want.Function.Arguments {
|
||||||
|
t.Fatalf("Function.Arguments = %q, want %q", got.Function.Arguments, want.Function.Arguments)
|
||||||
|
}
|
||||||
|
if got.Arguments["path"] != want.Arguments["path"] {
|
||||||
|
t.Fatalf("Arguments[path] = %v, want %v", got.Arguments["path"], want.Arguments["path"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAntigravityFacadeSignaturesRemainAvailable(t *testing.T) {
|
||||||
|
var projectFetcher func(string) (string, error) = FetchAntigravityProjectID
|
||||||
|
var modelsFetcher func(string, string) ([]AntigravityModelInfo, error) = FetchAntigravityModels
|
||||||
|
|
||||||
|
if projectFetcher == nil {
|
||||||
|
t.Fatal("FetchAntigravityProjectID facade should be available")
|
||||||
|
}
|
||||||
|
if modelsFetcher == nil {
|
||||||
|
t.Fatal("FetchAntigravityModels facade should be available")
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ AntigravityModelInfo = oauthprovider.AntigravityModelInfo{}
|
||||||
|
}
|
||||||
139
pkg/providers/httpapi/gemini_helpers.go
Normal file
139
pkg/providers/httpapi/gemini_helpers.go
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
package httpapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func normalizeStoredToolCall(tc ToolCall) (string, map[string]any, string) {
|
||||||
|
name := tc.Name
|
||||||
|
args := tc.Arguments
|
||||||
|
thoughtSignature := ""
|
||||||
|
|
||||||
|
if name == "" && tc.Function != nil {
|
||||||
|
name = tc.Function.Name
|
||||||
|
thoughtSignature = tc.Function.ThoughtSignature
|
||||||
|
} else if tc.Function != nil {
|
||||||
|
thoughtSignature = tc.Function.ThoughtSignature
|
||||||
|
}
|
||||||
|
|
||||||
|
if args == nil {
|
||||||
|
args = map[string]any{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 0 && tc.Function != nil && tc.Function.Arguments != "" {
|
||||||
|
var parsed map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(tc.Function.Arguments), &parsed); err == nil && parsed != nil {
|
||||||
|
args = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return name, args, thoughtSignature
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveToolResponseName(toolCallID string, toolCallNames map[string]string) string {
|
||||||
|
if toolCallID == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if name, ok := toolCallNames[toolCallID]; ok && name != "" {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
return inferToolNameFromCallID(toolCallID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func inferToolNameFromCallID(toolCallID string) string {
|
||||||
|
if !strings.HasPrefix(toolCallID, "call_") {
|
||||||
|
return toolCallID
|
||||||
|
}
|
||||||
|
|
||||||
|
rest := strings.TrimPrefix(toolCallID, "call_")
|
||||||
|
if idx := strings.LastIndex(rest, "_"); idx > 0 {
|
||||||
|
candidate := rest[:idx]
|
||||||
|
if candidate != "" {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return toolCallID
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractPartThoughtSignature(thoughtSignature string, thoughtSignatureSnake string) string {
|
||||||
|
if thoughtSignature != "" {
|
||||||
|
return thoughtSignature
|
||||||
|
}
|
||||||
|
if thoughtSignatureSnake != "" {
|
||||||
|
return thoughtSignatureSnake
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var geminiUnsupportedKeywords = map[string]bool{
|
||||||
|
"patternProperties": true,
|
||||||
|
"additionalProperties": true,
|
||||||
|
"$schema": true,
|
||||||
|
"$id": true,
|
||||||
|
"$ref": true,
|
||||||
|
"$defs": true,
|
||||||
|
"definitions": true,
|
||||||
|
"examples": true,
|
||||||
|
"minLength": true,
|
||||||
|
"maxLength": true,
|
||||||
|
"minimum": true,
|
||||||
|
"maximum": true,
|
||||||
|
"multipleOf": true,
|
||||||
|
"pattern": true,
|
||||||
|
"format": true,
|
||||||
|
"minItems": true,
|
||||||
|
"maxItems": true,
|
||||||
|
"uniqueItems": true,
|
||||||
|
"minProperties": true,
|
||||||
|
"maxProperties": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeSchemaForGemini(schema map[string]any) map[string]any {
|
||||||
|
if schema == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[string]any)
|
||||||
|
for k, v := range schema {
|
||||||
|
if geminiUnsupportedKeywords[k] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch val := v.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
result[k] = sanitizeSchemaForGemini(val)
|
||||||
|
case []any:
|
||||||
|
sanitized := make([]any, len(val))
|
||||||
|
for i, item := range val {
|
||||||
|
if m, ok := item.(map[string]any); ok {
|
||||||
|
sanitized[i] = sanitizeSchemaForGemini(m)
|
||||||
|
} else {
|
||||||
|
sanitized[i] = item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result[k] = sanitized
|
||||||
|
default:
|
||||||
|
result[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, hasProps := result["properties"]; hasProps {
|
||||||
|
if _, hasType := result["type"]; !hasType {
|
||||||
|
result["type"] = "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractProtocol(model string) (protocol, modelID string) {
|
||||||
|
model = strings.TrimSpace(model)
|
||||||
|
protocol, modelID, found := strings.Cut(model, "/")
|
||||||
|
if !found {
|
||||||
|
return "openai", model
|
||||||
|
}
|
||||||
|
return protocol, modelID
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package providers
|
package httpapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
|
@ -303,7 +303,7 @@ func normalizeGeminiModel(model string) string {
|
||||||
model = strings.TrimSpace(model)
|
model = strings.TrimSpace(model)
|
||||||
model = strings.TrimPrefix(model, "models/")
|
model = strings.TrimPrefix(model, "models/")
|
||||||
if strings.Contains(model, "/") {
|
if strings.Contains(model, "/") {
|
||||||
_, modelID := ExtractProtocol(model)
|
_, modelID := extractProtocol(model)
|
||||||
if modelID != "" {
|
if modelID != "" {
|
||||||
return modelID
|
return modelID
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package providers
|
package httpapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
//
|
//
|
||||||
// Copyright (c) 2026 PicoClaw contributors
|
// Copyright (c) 2026 PicoClaw contributors
|
||||||
|
|
||||||
package providers
|
package httpapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
43
pkg/providers/httpapi/types.go
Normal file
43
pkg/providers/httpapi/types.go
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
package httpapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
ToolCall = protocoltypes.ToolCall
|
||||||
|
FunctionCall = protocoltypes.FunctionCall
|
||||||
|
LLMResponse = protocoltypes.LLMResponse
|
||||||
|
UsageInfo = protocoltypes.UsageInfo
|
||||||
|
Message = protocoltypes.Message
|
||||||
|
ToolDefinition = protocoltypes.ToolDefinition
|
||||||
|
ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition
|
||||||
|
ExtraContent = protocoltypes.ExtraContent
|
||||||
|
GoogleExtra = protocoltypes.GoogleExtra
|
||||||
|
ContentBlock = protocoltypes.ContentBlock
|
||||||
|
CacheControl = protocoltypes.CacheControl
|
||||||
|
)
|
||||||
|
|
||||||
|
type LLMProvider interface {
|
||||||
|
Chat(
|
||||||
|
ctx context.Context,
|
||||||
|
messages []Message,
|
||||||
|
tools []ToolDefinition,
|
||||||
|
model string,
|
||||||
|
options map[string]any,
|
||||||
|
) (*LLMResponse, error)
|
||||||
|
GetDefaultModel() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type StreamingProvider interface {
|
||||||
|
ChatStream(
|
||||||
|
ctx context.Context,
|
||||||
|
messages []Message,
|
||||||
|
tools []ToolDefinition,
|
||||||
|
model string,
|
||||||
|
options map[string]any,
|
||||||
|
onChunk func(accumulated string),
|
||||||
|
) (*LLMResponse, error)
|
||||||
|
}
|
||||||
46
pkg/providers/httpapi_facade.go
Normal file
46
pkg/providers/httpapi_facade.go
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
package providers
|
||||||
|
|
||||||
|
import httpapi "github.com/sipeed/picoclaw/pkg/providers/httpapi"
|
||||||
|
|
||||||
|
type (
|
||||||
|
GeminiProvider = httpapi.GeminiProvider
|
||||||
|
HTTPProvider = httpapi.HTTPProvider
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewGeminiProvider(
|
||||||
|
apiKey string,
|
||||||
|
apiBase string,
|
||||||
|
proxy string,
|
||||||
|
userAgent string,
|
||||||
|
requestTimeoutSeconds int,
|
||||||
|
extraBody map[string]any,
|
||||||
|
customHeaders map[string]string,
|
||||||
|
) *GeminiProvider {
|
||||||
|
return httpapi.NewGeminiProvider(apiKey, apiBase, proxy, userAgent, requestTimeoutSeconds, extraBody, customHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTPProvider(apiKey, apiBase, proxy string) *HTTPProvider {
|
||||||
|
return httpapi.NewHTTPProvider(apiKey, apiBase, proxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTPProviderWithMaxTokensField(apiKey, apiBase, proxy, maxTokensField string) *HTTPProvider {
|
||||||
|
return httpapi.NewHTTPProviderWithMaxTokensField(apiKey, apiBase, proxy, maxTokensField)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTPProviderWithMaxTokensFieldAndRequestTimeout(
|
||||||
|
apiKey, apiBase, proxy, maxTokensField, userAgent string,
|
||||||
|
requestTimeoutSeconds int,
|
||||||
|
extraBody map[string]any,
|
||||||
|
customHeaders map[string]string,
|
||||||
|
) *HTTPProvider {
|
||||||
|
return httpapi.NewHTTPProviderWithMaxTokensFieldAndRequestTimeout(
|
||||||
|
apiKey,
|
||||||
|
apiBase,
|
||||||
|
proxy,
|
||||||
|
maxTokensField,
|
||||||
|
userAgent,
|
||||||
|
requestTimeoutSeconds,
|
||||||
|
extraBody,
|
||||||
|
customHeaders,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package providers
|
package oauthprovider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package providers
|
package oauthprovider
|
||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
package providers
|
package oauthprovider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/auth"
|
||||||
anthropicprovider "github.com/sipeed/picoclaw/pkg/providers/anthropic"
|
anthropicprovider "github.com/sipeed/picoclaw/pkg/providers/anthropic"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -55,7 +56,7 @@ func (p *ClaudeProvider) GetDefaultModel() string {
|
||||||
return p.delegate.GetDefaultModel()
|
return p.delegate.GetDefaultModel()
|
||||||
}
|
}
|
||||||
|
|
||||||
func createClaudeTokenSource() func() (string, error) {
|
func CreateClaudeTokenSource(getCredential func(string) (*auth.AuthCredential, error)) func() (string, error) {
|
||||||
return func() (string, error) {
|
return func() (string, error) {
|
||||||
cred, err := getCredential("anthropic")
|
cred, err := getCredential("anthropic")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package providers
|
package oauthprovider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package providers
|
package oauthprovider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -240,7 +240,7 @@ func buildCodexParams(
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
func createCodexTokenSource() func() (string, string, error) {
|
func CreateCodexTokenSource() func() (string, string, error) {
|
||||||
return func() (string, string, error) {
|
return func() (string, string, error) {
|
||||||
cred, err := auth.GetCredential("openai")
|
cred, err := auth.GetCredential("openai")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package providers
|
package oauthprovider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
32
pkg/providers/oauth/types.go
Normal file
32
pkg/providers/oauth/types.go
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
package oauthprovider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
ToolCall = protocoltypes.ToolCall
|
||||||
|
FunctionCall = protocoltypes.FunctionCall
|
||||||
|
LLMResponse = protocoltypes.LLMResponse
|
||||||
|
UsageInfo = protocoltypes.UsageInfo
|
||||||
|
Message = protocoltypes.Message
|
||||||
|
ToolDefinition = protocoltypes.ToolDefinition
|
||||||
|
ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition
|
||||||
|
ExtraContent = protocoltypes.ExtraContent
|
||||||
|
GoogleExtra = protocoltypes.GoogleExtra
|
||||||
|
ContentBlock = protocoltypes.ContentBlock
|
||||||
|
CacheControl = protocoltypes.CacheControl
|
||||||
|
)
|
||||||
|
|
||||||
|
type LLMProvider interface {
|
||||||
|
Chat(
|
||||||
|
ctx context.Context,
|
||||||
|
messages []Message,
|
||||||
|
tools []ToolDefinition,
|
||||||
|
model string,
|
||||||
|
options map[string]any,
|
||||||
|
) (*LLMResponse, error)
|
||||||
|
GetDefaultModel() string
|
||||||
|
}
|
||||||
60
pkg/providers/oauth_facade.go
Normal file
60
pkg/providers/oauth_facade.go
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
oauthprovider "github.com/sipeed/picoclaw/pkg/providers/oauth"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
AntigravityProvider = oauthprovider.AntigravityProvider
|
||||||
|
AntigravityModelInfo = oauthprovider.AntigravityModelInfo
|
||||||
|
ClaudeProvider = oauthprovider.ClaudeProvider
|
||||||
|
CodexProvider = oauthprovider.CodexProvider
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewAntigravityProvider() *AntigravityProvider {
|
||||||
|
return oauthprovider.NewAntigravityProvider()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClaudeProvider(token string) *ClaudeProvider {
|
||||||
|
return oauthprovider.NewClaudeProvider(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClaudeProviderWithBaseURL(token, apiBase string) *ClaudeProvider {
|
||||||
|
return oauthprovider.NewClaudeProviderWithBaseURL(token, apiBase)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClaudeProviderWithTokenSource(token string, tokenSource func() (string, error)) *ClaudeProvider {
|
||||||
|
return oauthprovider.NewClaudeProviderWithTokenSource(token, tokenSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClaudeProviderWithTokenSourceAndBaseURL(
|
||||||
|
token string, tokenSource func() (string, error), apiBase string,
|
||||||
|
) *ClaudeProvider {
|
||||||
|
return oauthprovider.NewClaudeProviderWithTokenSourceAndBaseURL(token, tokenSource, apiBase)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCodexProvider(token, accountID string) *CodexProvider {
|
||||||
|
return oauthprovider.NewCodexProvider(token, accountID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCodexProviderWithTokenSource(
|
||||||
|
token, accountID string, tokenSource func() (string, string, error),
|
||||||
|
) *CodexProvider {
|
||||||
|
return oauthprovider.NewCodexProviderWithTokenSource(token, accountID, tokenSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchAntigravityProjectID(accessToken string) (string, error) {
|
||||||
|
return oauthprovider.FetchAntigravityProjectID(accessToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchAntigravityModels(accessToken, projectID string) ([]AntigravityModelInfo, error) {
|
||||||
|
return oauthprovider.FetchAntigravityModels(accessToken, projectID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createClaudeTokenSource() func() (string, error) {
|
||||||
|
return oauthprovider.CreateClaudeTokenSource(getCredential)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createCodexTokenSource() func() (string, string, error) {
|
||||||
|
return oauthprovider.CreateCodexTokenSource()
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue