Skip to content

Commit 3905ea4

Browse files
Add API key env for recall extraction providers
1 parent 6a0e546 commit 3905ea4

7 files changed

Lines changed: 93 additions & 8 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
@@ -63,13 +63,28 @@ model = "your-model-name"
6363
endpoint = "http://127.0.0.1:30000/v1"
6464
```
6565

66+
For a remote OpenAI-compatible provider such as Atlas Cloud, keep the key in the
67+
environment and point the server entry at the provider's `/v1` base URL:
68+
69+
```toml
70+
[recall.extract]
71+
enabled = true
72+
model = "deepseek-ai/deepseek-v4-pro"
73+
server = "atlascloud"
74+
75+
[recall.extract.servers.atlascloud]
76+
endpoint = "https://api.atlascloud.ai/v1"
77+
api_key_env = "ATLASCLOUD_API_KEY"
78+
timeout = "120s"
79+
```
80+
6681
Optional keys: `deployment` (labels which serving instance produced the corpus),
6782
`server` (selects among multiple named servers), `quiet_period` (default `"30m"`
6883
— how long a session must have been ended before extraction),
6984
`backstop_interval` (default `"1h"`), `failure_backoff` (default `"1h"`),
70-
`max_window_chars` (default 50000), `max_tokens`, a `[recall.extract.prompts]`
71-
table (`profile`, `dir`), and a `[recall.extract.request]` table (`temperature`,
72-
`extra_body`).
85+
`max_window_chars` (default 50000), `max_tokens`, per-server `api_key_env`, a
86+
`[recall.extract.prompts]` table (`profile`, `dir`), and a
87+
`[recall.extract.request]` table (`temperature`, `extra_body`).
7388

7489
Non-loopback endpoints must use HTTPS: extraction sends transcript content to
7590
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: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,9 @@ var entrySchema = map[string]any{
245245
// fingerprint.
246246
type Client struct {
247247
BaseURL 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
248251
Model string
249252
// RetryBackoff seeds the exponential wait between transient retries;
250253
// zero means 500ms. It shapes latency, not output, so it stays outside
@@ -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+
writeJSON(t, w, http.StatusOK, 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)