返回 DeepSeek-Reasonix
provider_settings_probe.go
根目录 / desktop / provider_settings_probe.go
1 package main
2
3 import (
4 "context"
5 "fmt"
6 "strings"
7 "time"
8
9 "reasonix/internal/boot"
10 "reasonix/internal/config"
11 )
12
13 // TestProviderModel sends a bounded, tool-free probe through the configured
14 // adapter. It does not create a session or change persisted settings.
15 func (a *App) TestProviderModel(p ProviderView, model, key string) error {
16 root := a.activeWorkspaceRoot()
17 cfg, err := config.LoadForRootWithoutCredentialsReadOnly(root)
18 if err != nil {
19 return err
20 }
21 if err := saveProviderConfig(cfg, p); err != nil {
22 return err
23 }
24 entry, ok := cfg.ResolveModel(p.Name + "/" + strings.TrimSpace(model))
25 if !ok {
26 return fmt.Errorf("model is not in this provider's configured list")
27 }
28 entry.ResolveAPIKeyForRoot(root)
29 if strings.TrimSpace(key) != "" {
30 copied := entry.WithAPIKeyForProbe(key)
31 entry = &copied
32 }
33 return boot.ProbeProviderConnection(a.reqCtx(), *entry, key, withProbeDirectHost(a.networkProxySpecForRoot(root), entry.BaseURL, p.NoProxy))
34 }
35
36 // FetchProviderModelCatalogDraft discovers models using an unsaved credential.
37 // Draft secrets are request-local and never enter the settings response or cache.
38 func (a *App) FetchProviderModelCatalogDraft(p ProviderView, key string) ([]ProviderModelCapabilityView, error) {
39 root := a.activeWorkspaceRoot()
40 // Capture persisted identity separately from the editor draft. Draft routes
41 // may differ legitimately; a saved provider changing during discovery may not.
42 savedIdentity := func() string {
43 cfg, err := config.LoadForRootWithoutCredentialsReadOnly(root)
44 if err != nil {
45 return ""
46 }
47 for _, entry := range cfg.Providers {
48 if entry.Name == p.Name {
49 return providerModelCatalogFingerprint(entry)
50 }
51 }
52 return ""
53 }
54 before := savedIdentity()
55 e := config.ProviderEntry{
56 Name: p.Name,
57 Kind: p.Kind,
58 BaseURL: p.BaseURL,
59 ModelsURL: strings.TrimSpace(p.ModelsURL),
60 APIKeyEnv: p.APIKeyEnv,
61 Headers: p.Headers,
62 AuthHeader: p.AuthHeader,
63 NoProxy: p.NoProxy,
64 ChatURL: p.ChatURL,
65 RequestURL: p.RequestURL,
66 }
67 started := time.Now()
68 credentialsRevision := config.CredentialStoreRevision()
69 e.ResolveAPIKeyForRoot(root)
70 if strings.TrimSpace(key) != "" {
71 e = e.WithAPIKeyForProbe(key)
72 }
73 ctx, cancel := context.WithTimeout(a.reqCtx(), 15*time.Second)
74 defer cancel()
75 models, err := e.FetchModelCatalogWithProxy(ctx, withProbeDirectHost(a.networkProxySpecForRoot(root), e.BaseURL, p.NoProxy))
76 if err != nil {
77 return []ProviderModelCapabilityView{}, err
78 }
79 // Credential changes invalidate a result even when the endpoint stayed the same.
80 unlockConfig := config.LockUserConfigEdits()
81 defer unlockConfig()
82 unlockCredentials, err := config.LockUserCredentialEdits()
83 if err != nil {
84 return []ProviderModelCapabilityView{}, err
85 }
86 defer unlockCredentials()
87 if credentialsRevision != config.CredentialStoreRevision() || before != savedIdentity() {
88 return []ProviderModelCapabilityView{}, fmt.Errorf("model discovery configuration changed; fetch again")
89 }
90 capabilities := config.NewModelCapabilityResolver()
91 if strings.TrimSpace(key) != "" {
92 capabilities = config.NewTransientModelCapabilityResolver()
93 }
94 capabilities.PutCatalogAt(e, models, started)
95 // Only adapter facts enter the cache. User choices apply to the returned view.
96 if cfg, err := config.LoadForRootWithoutCredentialsReadOnly(root); err == nil {
97 for _, saved := range cfg.Providers {
98 if saved.Name == p.Name {
99 e.PresetID, e.Vision = saved.PresetID, saved.Vision
100 break
101 }
102 }
103 }
104 e.VisionModels = p.VisionModels
105 e.ModelOverrides = providerModelOverridesForSave(p.ModelOverrides, nil)
106 result := make([]ProviderModelCapabilityView, 0, len(models))
107 for _, model := range models {
108 entry := e
109 entry.Model = model.ID
110 resolved := capabilities.Resolve(&entry)
111 view := modelCapabilityView(resolved)
112 view.Reasoning = config.ResolveReasoningView(&entry)
113 result = append(result, view)
114 }
115 return result, nil
116 }
117
117 lines GO