config: layer grok auth into provider-key resolution (add-grok phase 2)
ober
fd5568286eac8fb3259bb5dc7dff50b457387eb1
--- a/src/jcode/core/config.ss +++ b/src/jcode/core/config.ss @@ -8,6 +8,10 @@ config-provider config-get-provider-key config-default-model + provider-key-layers + detect-provider-from-list + *provider-env-vars* + *detect-provider-order* *config* *version*) @@ -15,7 +19,8 @@ :std/os/path :jcode/core/log :jcode/core/models - :jcode/core/secrets) + :jcode/core/secrets + :jcode/core/grok-auth) (def logger (make-logger "config")) @@ -32,6 +37,9 @@ ("openrouter" . "OPENROUTER_API_KEY") ("deepseek" . "DEEPSEEK_API_KEY") ("xai" . "XAI_API_KEY") + ;; "grok" reuses XAI_API_KEY as its primary env var; env-key-for-provider + ;; falls back to GROK_CODE_XAI_API_KEY when that one is unset. + ("grok" . "XAI_API_KEY") ("groq" . "GROQ_API_KEY") ("mistral" . "MISTRAL_API_KEY") ("together" . "TOGETHER_API_KEY") @@ -104,11 +112,16 @@ ((not (hash-table? obj)) #f) (else (loop (hash-get obj (car keys)) (cdr keys)))))) +(def (env-nonempty var) + (let ((v (getenv var))) (and v (not (string=? v "")) v))) + (def (env-key-for-provider provider) - (let ((entry (assoc provider *provider-env-vars*))) - (and entry - (let ((v (getenv (cdr entry)))) - (and v (not (string=? v "")) v))))) + ;; Primary: the provider's *provider-env-vars* entry. For "grok" specifically + ;; we also accept GROK_CODE_XAI_API_KEY as a fallback so users can keep a + ;; separate key for the Grok CLI session vs the xAI console. + (or (let ((entry (assoc provider *provider-env-vars*))) + (and entry (env-nonempty (cdr entry)))) + (and (equal? provider "grok") (env-nonempty "GROK_CODE_XAI_API_KEY")))) (def (encrypted-key-for-provider provider) ;; Only consult the encrypted store for providers that we know take @@ -122,10 +135,22 @@ (secret-store-exists?) (secret-store-get provider))) +(def (provider-key-layers provider env-fn store-fn config-fn grok-fn) + ;; Layered provider-key resolution: env -> encrypted store -> jcode.json + ;; "providers" hash -> grok-auth-token (only for "grok"). Lookups are passed + ;; in as closures so unit tests can stub them; live config-get-provider-key + ;; wires the real env, store, config, and grok-auth functions. + (or (env-fn provider) + (store-fn provider) + (config-fn provider) + (and (equal? provider "grok") (grok-fn)))) + (def (config-get-provider-key provider) - (or (env-key-for-provider provider) - (encrypted-key-for-provider provider) - (config-ref "providers" provider "api_key"))) + (provider-key-layers provider + env-key-for-provider + encrypted-key-for-provider + (lambda (p) (config-ref "providers" p "api_key")) + grok-auth-token)) (def (config-model) (or (config-ref "model") @@ -139,14 +164,24 @@ (config-detect-provider) "anthropic")) -(def (config-detect-provider) - ;; Auto-detect first provider with a configured key - (let loop ((names '("anthropic" "openrouter" "openai" "deepseek" "google" - "xai" "groq" "mistral" "together" "cerebras" "perplexity"))) +;; Auto-detect order. Existing API-key providers come first so a configured +;; xai/openai/etc. is preferred over an ambient ~/.grok session token; "grok" +;; is last so it's the fallback when nothing else has a key. +(def *detect-provider-order* + '("anthropic" "openrouter" "openai" "deepseek" "google" + "xai" "groq" "mistral" "together" "cerebras" "perplexity" + "grok")) + +(def (detect-provider-from-list names key-fn) + ;; Pure: return the first NAME in NAMES for which (key-fn name) is truthy. + (let loop ((names names)) (cond ((null? names) #f) - ((config-get-provider-key (car names)) (car names)) + ((key-fn (car names)) (car names)) (#t (loop (cdr names)))))) +(def (config-detect-provider) + (detect-provider-from-list *detect-provider-order* config-get-provider-key)) + (def (config-api-key) (config-get-provider-key (config-provider))) --- a/test/run.ss +++ b/test/run.ss @@ -5,6 +5,7 @@ (jcode core log) (jcode core message) (jcode core grok-auth) + (jcode core config) (jcode provider provider) (jcode tool registry) (jcode tool file) @@ -1613,6 +1614,60 @@ '(("grok-build" . "Grok Build"))) (check! "grok models-from #f → ()" (grok-models-from #f) '()) +;; ── config: grok provider-key layers + detect order ────────────── +;; The live config-get-provider-key reads real env / store / config / ~/.grok, +;; so we test the *pure* helpers it dispatches through: provider-key-layers +;; (precedence) and detect-provider-from-list (auto-detect order). + +(section "=== config: grok provider key ===") + +;; *provider-env-vars* maps both grok and xai to XAI_API_KEY (env-key-for-provider +;; layers GROK_CODE_XAI_API_KEY on top for grok only). +(check! "env-vars: grok → XAI_API_KEY" + (cdr (assoc "grok" *provider-env-vars*)) "XAI_API_KEY") +(check! "env-vars: xai still → XAI_API_KEY" + (cdr (assoc "xai" *provider-env-vars*)) "XAI_API_KEY") + +;; provider-key-layers precedence: env > store > config > grok-fn (grok only). +(check! "layers: env wins" + (provider-key-layers "grok" + (lambda (p) "env-k") (lambda (p) "store-k") (lambda (p) "cfg-k") (lambda () "tok")) + "env-k") +(check! "layers: store wins when env #f" + (provider-key-layers "grok" + (lambda (p) #f) (lambda (p) "store-k") (lambda (p) "cfg-k") (lambda () "tok")) + "store-k") +(check! "layers: config wins when env+store #f" + (provider-key-layers "grok" + (lambda (p) #f) (lambda (p) #f) (lambda (p) "cfg-k") (lambda () "tok")) + "cfg-k") +(check! "layers: grok-fn last (only for grok)" + (provider-key-layers "grok" + (lambda (p) #f) (lambda (p) #f) (lambda (p) #f) (lambda () "tok")) + "tok") +(check! "layers: grok-fn NOT used for xai" + (provider-key-layers "xai" + (lambda (p) #f) (lambda (p) #f) (lambda (p) #f) (lambda () "tok")) + #f) +(check! "layers: nothing keyed → #f" + (provider-key-layers "grok" + (lambda (p) #f) (lambda (p) #f) (lambda (p) #f) (lambda () #f)) + #f) + +;; detect-provider-from-list: grok is last so existing keyed providers win. +(check! "detect order: grok at the tail" + (car (reverse *detect-provider-order*)) "grok") +(check! "detect: skip until anthropic keyed" + (detect-provider-from-list *detect-provider-order* + (lambda (p) (equal? p "anthropic"))) + "anthropic") +(check! "detect: grok picked when nothing else keyed" + (detect-provider-from-list *detect-provider-order* + (lambda (p) (equal? p "grok"))) + "grok") +(check! "detect: nothing keyed → #f" + (detect-provider-from-list *detect-provider-order* (lambda (p) #f)) #f) + ;; ── Results ─────────────────────────────────────────────────────── (printf "~n~a passed, ~a failed~n" pass-count fail-count)