chain.odin (2601B)
1 package provider 2 3 // A chain is a priority order of providers. The first one that answers a 4 // probe serves the whole reading, so a provider over its limit costs one 5 // cheap ask rather than a failed review. Which provider is reachable is a 6 // property of the hour: a team limit exhausted, a gateway asleep, a key 7 // unset — the chain is how the tool rides that out without being told. 8 9 import "core:fmt" 10 import "core:os" 11 import "core:strings" 12 13 // Entry is one provider in the order, under the name it is reported as. 14 Entry :: struct { 15 name: string, 16 provider: Provider, 17 } 18 19 // default_chain is the configured preference: the direct API, then the 20 // coding assistant on the path, then the local gateway as the reading of 21 // last resort. The order is a choice about where an answer is likeliest 22 // to be had with the least fuss, not a claim about any of them; a probe 23 // decides. REVIEW_MODEL re-points the first slot. 24 default_chain :: proc(allocator := context.temp_allocator) -> []Entry { 25 model := os.get_env("REVIEW_MODEL", allocator) 26 if model == "" { 27 model = default_api_model 28 } 29 entries := make([]Entry, 3, allocator) 30 entries[0] = Entry{strings.concatenate({"api/", model}, allocator), Provider{kind = .Api, model = model}} 31 entries[1] = Entry{strings.concatenate({"claude/", default_claude_model}, allocator), Provider{kind = .Claude, model = default_claude_model}} 32 entries[2] = Entry{"pi/maple/glm-5-3-flash", Provider{kind = .Pi, model = "glm-5-3-flash", upstream = "maple"}} 33 return entries 34 } 35 36 // pick asks every entry a trivial question, in order, and returns the 37 // first that answers. Every failure is told through warn, so a review run 38 // on the second choice says why the first was passed over, in one line. 39 pick :: proc( 40 chain: []Entry, 41 warn: proc(msg: string), 42 allocator := context.allocator, 43 ) -> ( 44 p: Provider, 45 picked: string, 46 err: string, 47 ) { 48 reasons := make([dynamic]string, context.temp_allocator) 49 for e in chain { 50 answer, ask_err := ask( 51 e.provider, 52 "Answer the user's message.", 53 "Answer with the single word OK.", 54 context.temp_allocator, 55 ) 56 if ask_err == "" && strings.trim_space(answer.text) != "" { 57 return e.provider, strings.clone(e.name, allocator), "" 58 } 59 why := ask_err if ask_err != "" else "answered nothing" 60 if i := strings.index_any(why, "\r\n"); i >= 0 { 61 why = why[:i] 62 } 63 warn(fmt.tprintf("skipping %s: %s", e.name, why)) 64 append(&reasons, fmt.tprintf("%s: %s", e.name, why)) 65 } 66 return {}, "", fmt.aprintf("no provider answered: %s", strings.join(reasons[:], "; ", context.temp_allocator), allocator = allocator) 67 }