Allow unauthenticated optional API key requests

This commit is contained in:
2026-08-25 00:40:49 +00:00
parent 3239567297
commit a11c80291e
3 changed files with 60 additions and 23 deletions

View File

@@ -222,6 +222,8 @@ Stage 2 is complete when the built-in transport sends unauthenticated requests
for optional missing sources, still enforces explicit requirements, and all for optional missing sources, still enforces explicit requirements, and all
existing provider-response behavior remains green. existing provider-response behavior remains green.
**Status:** Complete.
## Stage 3: Align the Public Contract, Durable Documentation, and Full Validation ## Stage 3: Align the Public Contract, Durable Documentation, and Full Validation
### Objective ### Objective

View File

@@ -133,13 +133,18 @@ func (c *OpenAICompatibleClient) Generate(ctx context.Context, req domain.Genera
return nil, fmt.Errorf("%w: failed to create request: %v", ErrRequestFailed, err) return nil, fmt.Errorf("%w: failed to create request: %v", ErrRequestFailed, err)
} }
httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Content-Type", "application/json")
if apiKey := strings.TrimSpace(req.Target.APIKey); apiKey != "" { apiKey := strings.TrimSpace(req.Target.APIKey)
httpReq.Header.Set("Authorization", "Bearer "+apiKey) envName := strings.TrimSpace(req.Target.APIKeyEnv)
} else if envName := strings.TrimSpace(req.Target.APIKeyEnv); envName != "" { if apiKey == "" && envName != "" {
apiKey := strings.TrimSpace(os.Getenv(envName)) apiKey = strings.TrimSpace(os.Getenv(envName))
if apiKey == "" { }
if apiKey == "" && req.Target.APIKeyRequired {
if envName != "" {
return nil, fmt.Errorf("%w: api key environment variable %q is not set", ErrInvalidRequest, envName) return nil, fmt.Errorf("%w: api key environment variable %q is not set", ErrInvalidRequest, envName)
} }
return nil, fmt.Errorf("%w: api key is required", ErrInvalidRequest)
}
if apiKey != "" {
httpReq.Header.Set("Authorization", "Bearer "+apiKey) httpReq.Header.Set("Authorization", "Bearer "+apiKey)
} }

View File

@@ -501,36 +501,61 @@ func checkCompleteRequestAndResponseMapping(t *testing.T) {
func TestOpenAICompatibleClientAuthentication(t *testing.T) { func TestOpenAICompatibleClientAuthentication(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
configureEnv func(*testing.T) configureEnv func(*testing.T)
target domain.ExecutionTarget target domain.ExecutionTarget
wantAuth string wantAuthorization string
wantErr error wantError error
wantCallCount int wantCallCount int
}{ }{
{ {
name: "direct key takes precedence over environment", name: "direct key takes precedence over environment",
configureEnv: func(t *testing.T) { configureEnv: func(t *testing.T) {
t.Setenv("PROMPTKIT_TEST_API_KEY", "env-key") t.Setenv("PROMPTKIT_TEST_API_KEY", " env-key ")
}, },
target: domain.ExecutionTarget{ target: domain.ExecutionTarget{
APIKeyEnv: "PROMPTKIT_TEST_API_KEY", APIKeyEnv: "PROMPTKIT_TEST_API_KEY",
APIKey: "direct-llm-key", APIKey: " direct-llm-key ",
}, },
wantAuth: "Bearer direct-llm-key", wantAuthorization: "Bearer direct-llm-key",
wantCallCount: 1, wantCallCount: 1,
},
{
name: "environment key supplies authorization",
configureEnv: func(t *testing.T) {
t.Setenv("PROMPTKIT_TEST_API_KEY", " env-key ")
},
target: domain.ExecutionTarget{APIKeyEnv: "PROMPTKIT_TEST_API_KEY"},
wantAuthorization: "Bearer env-key",
wantCallCount: 1,
}, },
{ {
name: "no key omits authorization", name: "no key omits authorization",
wantCallCount: 1, wantCallCount: 1,
}, },
{ {
name: "missing environment key fails before transport", name: "optional missing environment omits authorization",
configureEnv: func(t *testing.T) { configureEnv: func(t *testing.T) {
t.Setenv("PROMPTKIT_MISSING_KEY", "") t.Setenv("PROMPTKIT_MISSING_KEY", "")
}, },
target: domain.ExecutionTarget{APIKeyEnv: "PROMPTKIT_MISSING_KEY"}, target: domain.ExecutionTarget{APIKeyEnv: "PROMPTKIT_MISSING_KEY"},
wantErr: ErrInvalidRequest, wantCallCount: 1,
},
{
name: "required missing environment fails before transport",
configureEnv: func(t *testing.T) {
t.Setenv("PROMPTKIT_MISSING_KEY", "")
},
target: domain.ExecutionTarget{
APIKeyEnv: "PROMPTKIT_MISSING_KEY",
APIKeyRequired: true,
},
wantError: ErrInvalidRequest,
},
{
name: "required target without source fails before transport",
target: domain.ExecutionTarget{APIKeyRequired: true},
wantError: ErrInvalidRequest,
}, },
} }
@@ -545,9 +570,9 @@ func TestOpenAICompatibleClientAuthentication(t *testing.T) {
request.Target = tc.target request.Target = tc.target
_, err := client.Generate(context.Background(), request) _, err := client.Generate(context.Background(), request)
if tc.wantErr != nil { if tc.wantError != nil {
if !errors.Is(err, tc.wantErr) { if !errors.Is(err, tc.wantError) {
t.Fatalf("error = %v, want %v", err, tc.wantErr) t.Fatalf("error = %v, want %v", err, tc.wantError)
} }
} else if err != nil { } else if err != nil {
t.Fatalf("generate: %v", err) t.Fatalf("generate: %v", err)
@@ -556,8 +581,13 @@ func TestOpenAICompatibleClientAuthentication(t *testing.T) {
t.Fatalf("provider calls = %d, want %d", got, tc.wantCallCount) t.Fatalf("provider calls = %d, want %d", got, tc.wantCallCount)
} }
if tc.wantCallCount == 1 { if tc.wantCallCount == 1 {
if got := provider.lastRequest(t).header.Get("Authorization"); got != tc.wantAuth { values := provider.lastRequest(t).header.Values("Authorization")
t.Fatalf("Authorization = %q, want %q", got, tc.wantAuth) if tc.wantAuthorization == "" {
if len(values) != 0 {
t.Fatalf("Authorization values = %q, want absent", values)
}
} else if len(values) != 1 || values[0] != tc.wantAuthorization {
t.Fatalf("Authorization values = %q, want [%q]", values, tc.wantAuthorization)
} }
} }
}) })