Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ captain whoami --no-cache

Lists every AI adapter (API providers and CLI agents: `anthropic`, `openai`, `gemini`, `claude-cli`, `claude-agent`, `codex-cli`, `gemini-cli`), their authentication method, binary availability, and a live model listing.

Provider model listings are resolved through the persisted cache in `~/.config/captain/models.json` (24h TTL, invalidated when the set of configured API keys changes), and priced from the OpenRouter snapshot in `~/Library/Caches/flanksource/openrouter-pricing.json` (24h TTL). `--no-cache` skips both, re-queries every provider's model endpoint plus OpenRouter pricing, and rewrites each cache with the fresh result. Add `-v` to see an access line per request, `-vv` for headers and query params, `-vvv` for request bodies, `-vvvv` for response bodies; credentials are redacted at every rung. Failed requests (status >= 400 or a transport error) are logged at the default verbosity. `-Plog.level.http=<level>` raises HTTP logging alone, and `-Phttp.log.base-level=<level>` (or `HTTP_LOG_BASE_LEVEL`) shifts the whole ladder.
Provider model listings are resolved through credential- and endpoint-scoped entries under `~/.config/captain/models/` (24h TTL), and priced from the OpenRouter snapshot in `~/Library/Caches/flanksource/openrouter-pricing.json` (24h TTL). The model cache uses a machine-local keyed namespace, so different provider accounts cannot reuse one another's availability. `--no-cache` skips both caches, re-queries every provider's model endpoint plus OpenRouter pricing, and rewrites only the affected model-cache entries with the fresh result. Add `-v` to see an access line per request, `-vv` for headers and query params, `-vvv` for request bodies, `-vvvv` for response bodies; credentials are redacted at every rung. Failed requests (status >= 400 or a transport error) are logged at the default verbosity. `-Plog.level.http=<level>` raises HTTP logging alone, and `-Phttp.log.base-level=<level>` (or `HTTP_LOG_BASE_LEVEL`) shifts the whole ladder.

To keep the traffic instead of watching it scroll past, `-Phttp.har=<path>` writes every request/response pair — including redirect hops and retries — to a HAR 1.2 archive you can open in browser DevTools. It applies to any command, not just `whoami`, and the file is written even when the command fails. `-Phttp.har.level=metadata` records headers, query strings and timings without bodies; `-Phttp.har.maxBodySize=<bytes>` changes the 64 KB per-body cap (`0` for none). Credentials are masked the same way as in the wire log, which means the archive is safe to attach to a bug report but cannot be replayed — `-Phttp.har.sensitive=true` keeps them verbatim and writes the file `0600`. Use `-Phttp.captain.har=<path>` to capture captain's own traffic when a shared `http.har` is already set.

Expand Down
17 changes: 13 additions & 4 deletions pkg/ai/adapter_models.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ var resolveModelRows = ResolveModels
// The resolver is Captain's cached model path, so repeated probes reuse a fresh
// cache instead of hitting providers every time; refresh bypasses that cache and
// re-queries every provider listing.
func fetchAPIModels(backends []Backend, probe AuthProbe, refresh bool) map[Backend]modelFetch {
func fetchAPIModels(backends []Backend, credentials CredentialSnapshot, apiURLs map[Backend]string, refresh bool) map[Backend]modelFetch {
apis := map[Backend]bool{}
for _, b := range backends {
if b.Kind() != "api" {
Expand All @@ -35,7 +35,7 @@ func fetchAPIModels(backends []Backend, probe AuthProbe, refresh bool) map[Backe
if source == "" {
continue
}
if effectiveAPIKey(source, probe) != "" {
if strings.TrimSpace(credentials.APIKey(source).Token) != "" {
apis[source] = true
}
}
Expand All @@ -47,7 +47,13 @@ func fetchAPIModels(backends []Backend, probe AuthProbe, refresh bool) map[Backe
wg.Add(1)
go func(backend Backend) {
defer wg.Done()
rows, err := resolveModelRows(context.Background(), ResolveOptions{Backend: backend, UseTokens: true, Refresh: refresh})
rows, err := resolveModelRows(context.Background(), ResolveOptions{
Backend: backend,
UseTokens: true,
Refresh: refresh,
Credentials: credentials,
APIURL: apiURLs[backend],
})
m := liveModelDefs(rows, backend)
mu.Lock()
out[backend] = modelFetch{models: m, err: err}
Expand Down Expand Up @@ -140,7 +146,7 @@ func applyModels(st *AdapterStatus, b Backend, cache map[Backend]modelFetch, cod
}

envVars := AuthEnvVars(source)
if effectiveAPIKey(source, probe) == "" {
if strings.TrimSpace(effectiveAPIKey(source, probe)) == "" {
st.ModelError = "configure a Captain vault token or set " + strings.Join(envVars, " or ") + " to list models"
return
}
Expand All @@ -157,6 +163,9 @@ func applyModels(st *AdapterStatus, b Backend, cache map[Backend]modelFetch, cod
}

func effectiveAPIKey(backend Backend, probe AuthProbe) string {
if probe.credentials.supplied {
return probe.credentials.APIKey(backend).Token
}
if probe.APICredentials != nil {
return probe.APICredentials[backend].Token
}
Expand Down
Loading
Loading