vkproxy.go

v0.5.1
Doc Versions Source
1
package vkproxy
2
3
import (
4
	"archive/zip"
5
	"fmt"
6
	"io"
7
	"os"
8
	"path/filepath"
9
	"regexp"
10
	"strconv"
11
	"strings"
12
13
	"sourcecraft.dev/bigbes/claudio/config"
14
)
15
16
const (
17
	zipFileName    = "claude-code-config.zip"
18
	shellFileName  = "install_claude_aliases.sh"
19
)
20
21
// ProxyModel holds a parsed model configuration from the VK LLM Proxy zip.
22
type ProxyModel struct {
23
	Name          string
24
	ProviderName  string
25
	BaseURL       string
26
	AuthToken     string
27
	ModelID       string
28
	ContextWindow int
29
	ProxyUser     string
30
	CustomHeaders string
31
}
32
33
// FindZip looks for claude-code-config.zip in standard locations.
34
func FindZip() string {
35
	var candidates []string
36
	cfgDir := filepath.Dir(config.Path())
37
	candidates = append(candidates, filepath.Join(cfgDir, zipFileName))
38
	if cwd, err := os.Getwd(); err == nil {
39
		candidates = append(candidates, filepath.Join(cwd, zipFileName))
40
	}
41
	if home, err := os.UserHomeDir(); err == nil {
42
		candidates = append(candidates, filepath.Join(home, zipFileName))
43
	}
44
	for _, p := range candidates {
45
		if _, err := os.Stat(p); err == nil {
46
			return p
47
		}
48
	}
49
	return ""
50
}
51
52
var (
53
	commentNameRe = regexp.MustCompile(`name='([^']+)'`)
54
	providerRe    = regexp.MustCompile(`provider_name='([^']+)'`)
55
	ctxRe         = regexp.MustCompile(`context_window=(\d+)`)
56
	envStrRe      = regexp.MustCompile(`^(\w+)="([^"]*)"`)
57
)
58
59
// ParseZip reads a zip file, extracts install_claude_aliases.sh, and parses model configs.
60
func ParseZip(path string) ([]ProxyModel, error) {
61
	r, err := zip.OpenReader(path)
62
	if err != nil {
63
		return nil, fmt.Errorf("opening zip: %w", err)
64
	}
65
	defer r.Close()
66
67
	for _, f := range r.File {
68
		if filepath.Base(f.Name) == shellFileName {
69
			rc, err := f.Open()
70
			if err != nil {
71
				return nil, err
72
			}
73
			defer rc.Close()
74
			data, err := io.ReadAll(rc)
75
			if err != nil {
76
				return nil, err
77
			}
78
			return parseScript(string(data))
79
		}
80
	}
81
	return nil, fmt.Errorf("%s not found in zip", shellFileName)
82
}
83
84
func parseScript(script string) ([]ProxyModel, error) {
85
	lines := strings.Split(script, "\n")
86
	var models []ProxyModel
87
88
	for i := 0; i < len(lines); i++ {
89
		line := strings.TrimSpace(lines[i])
90
91
		if !strings.HasPrefix(line, "# Функция для name=") {
92
			continue
93
		}
94
95
		var m ProxyModel
96
		if matches := commentNameRe.FindStringSubmatch(line); len(matches) > 1 {
97
			m.Name = matches[1]
98
		}
99
		if matches := providerRe.FindStringSubmatch(line); len(matches) > 1 {
100
			m.ProviderName = matches[1]
101
		}
102
		if matches := ctxRe.FindStringSubmatch(line); len(matches) > 1 {
103
			if v, err := strconv.Atoi(matches[1]); err == nil {
104
				m.ContextWindow = v
105
			}
106
		}
107
108
		// Parse the function body for env vars.
109
		for i++; i < len(lines); i++ {
110
			bodyLine := strings.TrimSpace(lines[i])
111
			if bodyLine == "}" {
112
				break
113
			}
114
			bodyLine = strings.TrimSuffix(bodyLine, "\\")
115
			bodyLine = strings.TrimSpace(bodyLine)
116
117
			if matches := envStrRe.FindStringSubmatch(bodyLine); len(matches) > 2 {
118
				switch matches[1] {
119
				case "ANTHROPIC_BASE_URL":
120
					m.BaseURL = matches[2]
121
				case "ANTHROPIC_AUTH_TOKEN":
122
					m.AuthToken = matches[2]
123
				case "ANTHROPIC_MODEL":
124
					m.ModelID = matches[2]
125
				case "VK_LLM_PROXY_USER":
126
					m.ProxyUser = matches[2]
127
				case "ANTHROPIC_CUSTOM_HEADERS":
128
					m.CustomHeaders = matches[2]
129
				}
130
			}
131
		}
132
133
		if m.ModelID != "" {
134
			models = append(models, m)
135
		}
136
	}
137
138
	return models, nil
139
}
140
141
// GroupByProvider groups models by their cleaned provider name, returning
142
// the provider order and grouped models.
143
func GroupByProvider(models []ProxyModel) ([]string, map[string][]ProxyModel) {
144
	var order []string
145
	seen := make(map[string]bool)
146
	grouped := make(map[string][]ProxyModel)
147
148
	for _, m := range models {
149
		name := CleanProviderName(m.ProviderName)
150
		if !seen[name] {
151
			seen[name] = true
152
			order = append(order, name)
153
		}
154
		grouped[name] = append(grouped[name], m)
155
	}
156
	return order, grouped
157
}
158
159
// CleanProviderName strips trailing _N suffixes (e.g. "DeepSeek_1" → "DeepSeek").
160
func CleanProviderName(raw string) string {
161
	parts := strings.Split(raw, "_")
162
	if len(parts) > 1 {
163
		last := parts[len(parts)-1]
164
		if _, err := strconv.Atoi(last); err == nil {
165
			return strings.Join(parts[:len(parts)-1], "_")
166
		}
167
	}
168
	return raw
169
}
170

Source Files