Repository navigation
Expand file tree
/
Copy pathlearn.go
More file actions
83 lines (75 loc) · 2.63 KB
/
Copy pathlearn.go
File metadata and controls
83 lines (75 loc) · 2.63 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
package llm
import (
"sync"
"sync/atomic"
)
// LearnKind identifies which learn-once fallback flag engaged.
type LearnKind string
const (
// LearnBuffered: streaming was rejected by the provider (explicit
// "streaming not supported" 400) or a 2xx response arrived without an
// SSE content type. Every later generation on this provider is buffered.
LearnBuffered LearnKind = "buffered"
// LearnResponses: the provider requires /v1/responses for
// reasoning_effort combined with function tools.
LearnResponses LearnKind = "responses"
// LearnNoneEffort: the provider rejected reasoning_effort with tools;
// effort is pinned to "none" for later requests.
LearnNoneEffort LearnKind = "none_effort"
// LearnDropStreamOpts: the provider rejected stream_options; the field
// is omitted from later requests.
LearnDropStreamOpts LearnKind = "drop_stream_options"
)
// LearnEvent reports one learn-once constraint engaging. Status is the HTTP
// status that triggered the fallback (0 when none applied, e.g. a 2xx
// response that arrived without SSE framing); Message carries the provider
// error text when one exists.
type LearnEvent struct {
Kind LearnKind
Provider string
Status int
Message string
}
var (
learnObserverMu sync.RWMutex
learnObserver func(LearnEvent)
)
// SetLearnObserver registers a process-wide callback fired once per engaged
// learn flag, at the moment the fallback decision is made. Nil (the default)
// disables observation entirely. Delivery is at-most-once: a callback
// registered after a flag engaged (or swapped during the engaging call) does
// not retroactively receive that engagement. The callback runs on the request
// goroutine: keep it fast and non-blocking; it must never call back into the
// SDK and must not panic.
func SetLearnObserver(fn func(LearnEvent)) {
learnObserverMu.Lock()
learnObserver = fn
learnObserverMu.Unlock()
}
func notifyLearn(ev LearnEvent) {
learnObserverMu.RLock()
fn := learnObserver
learnObserverMu.RUnlock()
if fn != nil {
fn(ev)
}
}
// engageLearn flips a learn-once flag exactly once (CAS) and fires the
// observer on the engaging call only. apiErr may be nil when no HTTP error
// triggered the fallback. Behavior is otherwise identical to Store(true):
// the caller's case guards remain the routing decision.
func (pc *providerClient) engageLearn(flag *atomic.Bool, kind LearnKind, apiErr *APIError) {
if !flag.CompareAndSwap(false, true) {
return
}
ev := LearnEvent{Kind: kind, Provider: pc.cfg.ID}
if apiErr != nil {
ev.Status = apiErr.Status
ev.Message = apiErr.Message
}
if pc.opts.observer != nil {
pc.opts.observer(ev)
return
}
notifyLearn(ev)
}