fix(fallback): handle transport resets and use candidate providers
This commit is contained in:
parent
4a8a2e9c23
commit
19550e05ff
5 changed files with 125 additions and 1 deletions
|
|
@ -1044,7 +1044,12 @@ func (al *AgentLoop) runLLMIteration(
|
|||
ctx,
|
||||
activeCandidates,
|
||||
func(ctx context.Context, provider, model string) (*providers.LLMResponse, error) {
|
||||
return agent.Provider.Chat(ctx, messages, providerToolDefs, model, llmOpts)
|
||||
modelRef := provider + "/" + model
|
||||
candidateProvider, candidateModel, err := providers.CreateProviderForModelRef(al.cfg, modelRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return candidateProvider.Chat(ctx, messages, providerToolDefs, candidateModel, llmOpts)
|
||||
},
|
||||
)
|
||||
if fbErr != nil {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,14 @@ var (
|
|||
substr("timed out"),
|
||||
substr("deadline exceeded"),
|
||||
substr("context deadline exceeded"),
|
||||
substr("connection refused"),
|
||||
substr("connection reset by peer"),
|
||||
substr("connect: cannot assign requested address"),
|
||||
substr("no route to host"),
|
||||
substr("network is unreachable"),
|
||||
substr("server closed idle connection"),
|
||||
substr("unexpected eof"),
|
||||
substr("eof"),
|
||||
}
|
||||
|
||||
billingPatterns = []errorPattern{
|
||||
|
|
|
|||
|
|
@ -139,6 +139,9 @@ func TestClassifyError_TimeoutPatterns(t *testing.T) {
|
|||
"connection timed out",
|
||||
"deadline exceeded",
|
||||
"context deadline exceeded",
|
||||
`Post "http://192.168.178.66:1234/v1/chat/completions": dial tcp 192.168.178.66:1234: connect: connection refused`,
|
||||
"read tcp 10.0.0.2:1234->10.0.0.3:443: read: connection reset by peer",
|
||||
"dial tcp 10.0.0.9:1234: connect: no route to host",
|
||||
}
|
||||
|
||||
for _, msg := range patterns {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,41 @@ import (
|
|||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
)
|
||||
|
||||
// CreateProviderForModelRef resolves a model alias or full provider/model reference
|
||||
// against config.model_list and creates a provider instance for that specific entry.
|
||||
func CreateProviderForModelRef(appCfg *config.Config, modelRef string) (LLMProvider, string, error) {
|
||||
if appCfg == nil {
|
||||
return nil, "", fmt.Errorf("config is nil")
|
||||
}
|
||||
|
||||
ref := strings.TrimSpace(modelRef)
|
||||
if ref == "" {
|
||||
return nil, "", fmt.Errorf("model reference is empty")
|
||||
}
|
||||
|
||||
if mc, err := appCfg.GetModelConfig(ref); err == nil && mc != nil {
|
||||
return CreateProviderFromConfig(mc)
|
||||
}
|
||||
|
||||
refProvider, refModel := ExtractProtocol(ref)
|
||||
for i := range appCfg.ModelList {
|
||||
modelCfg := &appCfg.ModelList[i]
|
||||
candidate := strings.TrimSpace(modelCfg.Model)
|
||||
if candidate == "" {
|
||||
continue
|
||||
}
|
||||
if candidate == ref {
|
||||
return CreateProviderFromConfig(modelCfg)
|
||||
}
|
||||
candidateProvider, candidateModel := ExtractProtocol(candidate)
|
||||
if candidateProvider == refProvider && candidateModel == refModel {
|
||||
return CreateProviderFromConfig(modelCfg)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, "", fmt.Errorf("model %q not found in model_list", modelRef)
|
||||
}
|
||||
|
||||
// createClaudeAuthProvider creates a Claude provider using OAuth credentials from auth store.
|
||||
func createClaudeAuthProvider() (LLMProvider, error) {
|
||||
cred, err := getCredential("anthropic")
|
||||
|
|
|
|||
|
|
@ -235,6 +235,79 @@ func TestCreateProviderFromConfig_CodexCLI(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCreateProviderForModelRef_ResolvesAliasAndFullRef(t *testing.T) {
|
||||
aliasServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/chat/completions" {
|
||||
t.Fatalf("alias request path = %q, want %q", r.URL.Path, "/chat/completions")
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"alias"},"finish_reason":"stop"}]}`))
|
||||
}))
|
||||
defer aliasServer.Close()
|
||||
|
||||
openrouterServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/chat/completions" {
|
||||
t.Fatalf("openrouter request path = %q, want %q", r.URL.Path, "/chat/completions")
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"openrouter"},"finish_reason":"stop"}]}`))
|
||||
}))
|
||||
defer openrouterServer.Close()
|
||||
|
||||
cfg := &config.Config{
|
||||
ModelList: []config.ModelConfig{
|
||||
{
|
||||
ModelName: "lmstudio-qwen",
|
||||
Model: "openai/qwen/qwen3.5-9b",
|
||||
APIKey: "lm-studio",
|
||||
APIBase: aliasServer.URL,
|
||||
},
|
||||
{
|
||||
ModelName: "openrouter-auto",
|
||||
Model: "openrouter/auto",
|
||||
APIKey: "sk-or-v1-test",
|
||||
APIBase: openrouterServer.URL,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
provider, modelID, err := CreateProviderForModelRef(cfg, "lmstudio-qwen")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateProviderForModelRef(alias) error = %v", err)
|
||||
}
|
||||
if _, ok := provider.(*HTTPProvider); !ok {
|
||||
t.Fatalf("CreateProviderForModelRef(alias) provider = %T, want *HTTPProvider", provider)
|
||||
}
|
||||
if modelID != "qwen/qwen3.5-9b" {
|
||||
t.Fatalf("alias modelID = %q, want %q", modelID, "qwen/qwen3.5-9b")
|
||||
}
|
||||
resp, err := provider.Chat(t.Context(), []Message{{Role: "user", Content: "ping"}}, nil, modelID, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("alias Chat() error = %v", err)
|
||||
}
|
||||
if resp.Content != "alias" {
|
||||
t.Fatalf("alias response = %q, want %q", resp.Content, "alias")
|
||||
}
|
||||
|
||||
provider, modelID, err = CreateProviderForModelRef(cfg, "openrouter/auto")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateProviderForModelRef(full ref) error = %v", err)
|
||||
}
|
||||
if _, ok := provider.(*HTTPProvider); !ok {
|
||||
t.Fatalf("CreateProviderForModelRef(full ref) provider = %T, want *HTTPProvider", provider)
|
||||
}
|
||||
if modelID != "auto" {
|
||||
t.Fatalf("full ref modelID = %q, want %q", modelID, "auto")
|
||||
}
|
||||
resp, err = provider.Chat(t.Context(), []Message{{Role: "user", Content: "ping"}}, nil, modelID, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("full ref Chat() error = %v", err)
|
||||
}
|
||||
if resp.Content != "openrouter" {
|
||||
t.Fatalf("full ref response = %q, want %q", resp.Content, "openrouter")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProviderFromConfig_MissingAPIKey(t *testing.T) {
|
||||
cfg := &config.ModelConfig{
|
||||
ModelName: "test-no-key",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue