review

review patchsets using your default editor
Log | Files | Refs

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 }