Repository navigation
Expand file tree
/
Copy pathchatv3_credits.go
More file actions
222 lines (202 loc) · 6.5 KB
/
Copy pathchatv3_credits.go
File metadata and controls
222 lines (202 loc) · 6.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
package main
import (
"context"
"errors"
"strings"
"sync"
"time"
"github.com/Agent-Field/codeaf/internal/config"
"github.com/Agent-Field/codeaf/internal/credits"
"github.com/Agent-Field/codeaf/internal/env"
"github.com/Agent-Field/codeaf/internal/guard"
"github.com/Agent-Field/codeaf/internal/provider"
)
var errCreditsSuperseded = errors.New("credit read superseded by a changed key")
// v3ReadAndStoreCredits commits only a reading for the key still in force.
//
// THE READING IS DATED WHEN THE READ BEGAN, taken before the request goes out:
// a balance that answers after a payment refusal was looked up before it, and
// must not lift it.
func v3ReadAndStoreCredits(ctx context.Context, profileDir, base, key string) (credits.Reading, error) {
observedAt := time.Now()
reading, err := credits.Read(ctx, nil, base, key)
if err != nil {
return credits.Reading{}, err
}
if key != config.APIKeyAt(profileDir) {
return credits.Reading{}, errCreditsSuperseded
}
if err := config.WriteCreditsReadingAt(profileDir, key, reading, observedAt); err != nil {
return credits.Reading{}, err
}
return reading, nil
}
// v3CreditReader binds the key and address at read time. A key pasted after
// first run must be the one the lookup asks about and fingerprints on disk.
func v3CreditReader(proc *v3Process) func(context.Context) (credits.Reading, error) {
return func(ctx context.Context) (credits.Reading, error) {
key, sources := proc.currentAccount()
if strings.TrimSpace(key) == "" {
return credits.Reading{}, errors.New("no default provider key")
}
base := sources.Default().Address
if base == "" {
base = proc.Settings.BaseURL
}
return v3ReadAndStoreCredits(ctx, proc.ProfileDir, base, key)
}
}
// v3LocalCreditReader uses the terminal's resolved account on the engine road.
func v3LocalCreditReader(settings config.Config) func(context.Context) (credits.Reading, error) {
return func(ctx context.Context) (credits.Reading, error) {
key := config.APIKeyAt(settings.ProfileDir)
if key == "" {
return credits.Reading{}, errors.New("no default provider key")
}
base := settings.Sources.Default().Address
if base == "" {
base = settings.BaseURL
}
return v3ReadAndStoreCredits(ctx, settings.ProfileDir, base, key)
}
}
type v3CreditListener struct{ call func() }
// v3CreditWatcher owns the provider's one hook for the process lifetime. An
// engine has no surface listener; its completed record is the surface's door.
type v3CreditWatcher struct {
proc *v3Process
trigger *credits.Trigger
mu sync.Mutex
listener *v3CreditListener
stopHook func()
closed bool
wait sync.WaitGroup
}
func newV3CreditWatcher(proc *v3Process) *v3CreditWatcher {
w := &v3CreditWatcher{proc: proc, trigger: credits.NewTrigger(time.Now)}
w.stopHook = provider.SetPaymentRequiredHook(w.refused)
return w
}
func (w *v3CreditWatcher) refused(base string) {
if !v3DefaultCreditBase(w.proc, base) {
return
}
if !w.begin() {
return
}
guard.Go("chatv3/payment-credits", func() {
defer w.wait.Done()
defer w.trigger.End()
ctx := w.proc.processCtx
if ctx == nil {
ctx = context.Background()
}
_, err := v3CreditReader(w.proc)(ctx)
if err != nil {
return
}
if listener := w.currentListener(); listener != nil {
listener.call()
}
})
}
func (w *v3CreditWatcher) begin() bool {
w.mu.Lock()
defer w.mu.Unlock()
if w.closed || !w.trigger.Begin(credits.Refusal) {
return false
}
w.wait.Add(1)
return true
}
func (w *v3CreditWatcher) currentListener() *v3CreditListener {
w.mu.Lock()
defer w.mu.Unlock()
return w.listener
}
func (w *v3CreditWatcher) listen(wake func()) func() {
listener := &v3CreditListener{call: wake}
w.setListener(listener)
return func() { w.clearListener(listener) }
}
func (w *v3CreditWatcher) setListener(listener *v3CreditListener) {
w.mu.Lock()
defer w.mu.Unlock()
if !w.closed {
w.listener = listener
}
}
func (w *v3CreditWatcher) clearListener(listener *v3CreditListener) {
w.mu.Lock()
defer w.mu.Unlock()
if w.listener == listener {
w.listener = nil
}
}
func (w *v3CreditWatcher) markClosed() {
w.mu.Lock()
defer w.mu.Unlock()
w.closed = true
w.listener = nil
}
func (w *v3CreditWatcher) close() {
w.markClosed()
w.stopHook()
w.wait.Wait()
}
// v3PaymentRefusals lets a local surface hear a completed process read.
func v3PaymentRefusals(proc *v3Process) func(func()) func() {
return func(wake func()) func() {
return proc.creditWatcher.listen(wake)
}
}
func v3DefaultCreditBase(proc *v3Process, base string) bool {
_, sources := proc.currentAccount()
current := sources.Default().Address
if current == "" {
current = proc.Settings.BaseURL
}
return strings.TrimRight(base, "/") == strings.TrimRight(current, "/")
}
// v3FreshDefault is the launch a NEW conversation opens on, asked again of the
// balance check at the moment it opens rather than at the moment the process
// booted (#1439). A key read low after boot must not hand /new the paid default
// the boot resolved, and a top-up read after boot must not keep handing out the
// free one.
//
// IT MOVES ONLY THE BUILD'S OWN DEFAULT. A --model flag, CODEAF_MODEL, or a
// saved talk row is somebody's choice and the launch keeps it exactly as it was
// — including the older behaviour that /new opens on the model the process
// booted on — so the only thing this changes is which of the two implicit
// defaults a fresh conversation starts on.
//
// AND THE WINDOW MOVES WITH THE MODEL. The launch's ContextWindow was measured
// for the model the boot chose; a conversation opened on the other default with
// that figure would compact against a window its model does not have (the free
// default takes 262k, the paid one 1.31M). The catalog's answer for the new
// model replaces it, and nothing — which leaves the session's conservative
// default — when the catalog cannot say.
func v3FreshDefault(launch *v3Launch, flag string) *v3Launch {
if launch == nil || strings.TrimSpace(flag) != "" || strings.TrimSpace(env.Get(config.ModelEnv)) != "" {
return launch
}
profileDir := launch.Settings.ProfileDir
if config.ChatModelAt(profileDir) != "" {
return launch
}
if launch.Model != config.DefaultModel && launch.Model != config.FreeChatModel {
return launch
}
want := config.ChatDefaultAt(profileDir)
if want == launch.Model {
return launch
}
current := *launch
current.Model = want
current.Config.Model = want
current.Config.ContextWindow = 0
if current.Config.ContextWindowFor != nil {
current.Config.ContextWindow = current.Config.ContextWindowFor(want)
}
return ¤t
}