Skip to content

Commit 3bf7248

Browse files
Support API key env for recall extraction endpoints (#1268)
## Summary - add `api_key_env` to `[recall.extract.servers.<name>]` so authenticated OpenAI-compatible recall extraction endpoints can use bearer tokens without embedding credentials in URLs - wire the resolved server key into the extraction client and keep endpoint-provided diagnostics withheld when a bearer credential is configured - document an Atlas Cloud HTTPS example using `ATLASCLOUD_API_KEY` and `deepseek-ai/deepseek-v4-pro` ## Testing - `git diff --check` - Atlas Cloud live model catalog: `GET https://api.atlascloud.ai/v1/models` returned HTTP 200 with 121 models; confirmed `deepseek-ai/deepseek-v4-pro` and `qwen/qwen3.5-flash` - Not run: `gofmt` / `go test ./internal/config ./internal/recall/extract ./cmd/agentsview` because this machine has no Go toolchain in PATH (`go` and `gofmt` command not found); Docker is installed but the daemon is not running Co-authored-by: nb213 <binyangzhu000-sudo@users.noreply.github.com>
1 parent 0b01ff8 commit 3bf7248

7 files changed

Lines changed: 94 additions & 9 deletions

File tree

cmd/agentsview/recall_extract.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ func resolveExtractDistillation(
9898
return extractDistillation{
9999
Client: &extract.Client{
100100
BaseURL: server.Endpoint,
101+
APIKey: server.APIKey(),
101102
Model: cfg.Model,
102103
HTTPClient: httpClient,
103104
Request: request,

cmd/agentsview/recall_extract_test.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ func TestRecallExtractPreviewSubcommandBuildsChunks(t *testing.T) {
315315

316316
func TestResolveExtractDistillationAppliesOverrides(t *testing.T) {
317317
temp := 0.3
318+
t.Setenv("AGENTSVIEW_TEST_RECALL_API_KEY", "atlas-secret")
318319
cfg := config.RecallExtractConfig{
319320
Enabled: true,
320321
Model: "qwen3.5-27b",
@@ -325,7 +326,11 @@ func TestResolveExtractDistillationAppliesOverrides(t *testing.T) {
325326
BackstopInterval: "1h",
326327
FailureBackoff: "2h",
327328
Servers: map[string]config.RecallExtractServerConfig{
328-
"local": {Endpoint: "http://127.0.0.1:30000/v1", Timeout: "120s"},
329+
"local": {
330+
Endpoint: "http://127.0.0.1:30000/v1",
331+
APIKeyEnv: "AGENTSVIEW_TEST_RECALL_API_KEY",
332+
Timeout: "120s",
333+
},
329334
},
330335
Request: config.RecallExtractRequestConfig{
331336
Temperature: &temp,
@@ -338,6 +343,7 @@ func TestResolveExtractDistillationAppliesOverrides(t *testing.T) {
338343
assert.Equal(t, "qwen", dist.Profile,
339344
"model prefix must select the qwen profile")
340345
assert.Equal(t, "http://127.0.0.1:30000/v1", dist.Client.BaseURL)
346+
assert.Equal(t, "atlas-secret", dist.Client.APIKey)
341347
assert.Equal(t, 0.3, dist.Client.Request.Temperature)
342348
assert.Equal(t, 512, dist.Client.Request.MaxTokens)
343349
assert.Equal(t, map[string]any{"custom": true},

docs/recall.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,28 @@ model = "your-model-name"
9898
endpoint = "http://127.0.0.1:30000/v1"
9999
```
100100

101+
For a remote OpenAI-compatible provider such as Atlas Cloud, keep the key in the
102+
environment and point the server entry at the provider's `/v1` base URL:
103+
104+
```toml
105+
[recall.extract]
106+
enabled = true
107+
model = "deepseek-ai/deepseek-v4-pro"
108+
server = "atlascloud"
109+
110+
[recall.extract.servers.atlascloud]
111+
endpoint = "https://api.atlascloud.ai/v1"
112+
api_key_env = "ATLASCLOUD_API_KEY"
113+
timeout = "120s"
114+
```
115+
101116
Optional keys: `deployment` (labels which serving instance produced the corpus),
102117
`server` (selects among multiple named servers), `quiet_period` (default `"30m"`
103118
— how long a session must have been ended before extraction),
104119
`backstop_interval` (default `"1h"`), `failure_backoff` (default `"1h"`),
105-
`max_window_chars` (default 50000), `max_tokens`, a `[recall.extract.prompts]`
106-
table (`profile`, `dir`), and a `[recall.extract.request]` table (`temperature`,
107-
`extra_body`).
120+
`max_window_chars` (default 50000), `max_tokens`, per-server `api_key_env`, a
121+
`[recall.extract.prompts]` table (`profile`, `dir`), and a
122+
`[recall.extract.request]` table (`temperature`, `extra_body`).
108123

109124
Non-loopback endpoints must use HTTPS: extraction sends transcript content to
110125
the endpoint, and plaintext HTTP off the machine could be intercepted. A server

internal/config/config_recall_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,17 @@ func TestRecallExtractConfigValidate(t *testing.T) {
214214
}
215215
}
216216

217+
func TestRecallExtractServerConfigAPIKeyEnv(t *testing.T) {
218+
var server RecallExtractServerConfig
219+
assert.Equal(t, "", server.APIKey(), "no env var configured")
220+
221+
server.APIKeyEnv = "AGENTSVIEW_TEST_RECALL_API_KEY"
222+
assert.Equal(t, "", server.APIKey(), "configured env var not set in environment")
223+
224+
t.Setenv("AGENTSVIEW_TEST_RECALL_API_KEY", "secret-123")
225+
assert.Equal(t, "secret-123", server.APIKey())
226+
}
227+
217228
// TestRecallExtractValidationRedactsEndpointCredentials pins that
218229
// validation errors never echo endpoint credentials: config errors land on
219230
// stderr and in CI logs, and endpoints may carry Basic-auth userinfo or

internal/config/recall.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"net"
66
"net/url"
7+
"os"
78
"slices"
89
"sort"
910
"strings"
@@ -64,6 +65,9 @@ type RecallExtractServerConfig struct {
6465
// Endpoint is the OpenAI-compatible base URL, e.g.
6566
// "http://127.0.0.1:30000/v1".
6667
Endpoint string `toml:"endpoint" json:"endpoint"`
68+
// APIKeyEnv names the environment variable holding the API key.
69+
// Empty means anonymous access.
70+
APIKeyEnv string `toml:"api_key_env" json:"api_key_env,omitempty"`
6771
// Timeout is a parseable duration string applied to each model call.
6872
// Distillation calls on local models are slow; default "120s".
6973
Timeout string `toml:"timeout" json:"timeout"`
@@ -73,6 +77,15 @@ type RecallExtractServerConfig struct {
7377
AllowHTTP bool `toml:"allow_http" json:"allow_http,omitempty"`
7478
}
7579

80+
// APIKey reads the API key from the environment variable named by
81+
// APIKeyEnv. Returns "" when APIKeyEnv is unset.
82+
func (s RecallExtractServerConfig) APIKey() string {
83+
if s.APIKeyEnv == "" {
84+
return ""
85+
}
86+
return os.Getenv(s.APIKeyEnv)
87+
}
88+
7689
// RecallExtractPromptsConfig selects the prompt profile and optional
7790
// per-role override files.
7891
type RecallExtractPromptsConfig struct {

internal/recall/extract/client.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,10 @@ var entrySchema = map[string]any{
245245
// fingerprint.
246246
type Client struct {
247247
BaseURL string
248-
Model string
248+
// APIKey is sent as a bearer token when non-empty. Keep credentials
249+
// outside BaseURL so redacted endpoint logs stay useful.
250+
APIKey string
251+
Model string
249252
// RetryBackoff seeds the exponential wait between transient retries;
250253
// zero means 500ms. It shapes latency, not output, so it stays outside
251254
// RequestShape and the fingerprint.
@@ -408,6 +411,9 @@ func (c *Client) distill(
408411
return nil, Usage{}, fmt.Errorf("building distill request: %w", err)
409412
}
410413
request.Header.Set("Content-Type", "application/json")
414+
if c.APIKey != "" {
415+
request.Header.Set("Authorization", "Bearer "+c.APIKey)
416+
}
411417

412418
response, err := c.httpClient().Do(request)
413419
if err != nil {
@@ -609,14 +615,17 @@ func (c *Client) distill(
609615
return entries, parsed.Usage, nil
610616
}
611617

612-
// credentialedEndpoint reports whether the configured endpoint URL
613-
// carries credential material: userinfo, or any raw query segment whose
614-
// key is not the api-version surface selector (mirroring the config
615-
// redactor's fail-closed allowlist). Raw wire segments, no parser:
618+
// credentialedEndpoint reports whether the configured request carries
619+
// credential material: a bearer token, URL userinfo, or any raw query
620+
// segment whose key is not the api-version surface selector (mirroring
621+
// the config redactor's fail-closed allowlist). Raw wire segments, no parser:
616622
// url.ParseQuery would reject exactly the malformed queries that still
617623
// travel verbatim, and a rejection must not fail open. An unparseable URL
618624
// counts as credentialed for the same reason.
619625
func (c *Client) credentialedEndpoint() bool {
626+
if c.APIKey != "" {
627+
return true
628+
}
620629
endpoint, err := url.Parse(c.BaseURL)
621630
if err != nil {
622631
return true

internal/recall/extract/client_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,36 @@ func TestClientDistillParsesEntriesAndSendsShape(t *testing.T) {
146146
}
147147
}
148148

149+
func TestClientDistillSendsBearerToken(t *testing.T) {
150+
var gotAuth string
151+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
152+
gotAuth = r.Header.Get("Authorization")
153+
_ = json.NewEncoder(w).Encode(map[string]any{
154+
"choices": []map[string]any{{
155+
"finish_reason": "stop",
156+
"message": map[string]any{
157+
"role": "assistant",
158+
"content": entriesJSON(t, "one"),
159+
},
160+
}},
161+
"usage": map[string]any{
162+
"prompt_tokens": 7,
163+
"completion_tokens": 3,
164+
},
165+
})
166+
}))
167+
defer server.Close()
168+
169+
client := testClient(server.URL)
170+
client.APIKey = "secret-key"
171+
_, _, err := client.DistillWithRecovery(
172+
context.Background(), "system prompt", "unit text", 1,
173+
)
174+
require.NoError(t, err)
175+
176+
assert.Equal(t, "Bearer secret-key", gotAuth)
177+
}
178+
149179
func TestClientTrailingSlashBaseURL(t *testing.T) {
150180
var requests []map[string]any
151181
server := newScriptedServer(t, []scriptedResponse{

0 commit comments

Comments
 (0)