Move authentication probes into the outbound adapter
This commit is contained in:
105
internal/llm/openrouter_authentication.go
Normal file
105
internal/llm/openrouter_authentication.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"go.maximumdirect.net/promptkit/internal/domain"
|
||||
)
|
||||
|
||||
const maxAuthenticationResponseBytes = 1 << 20
|
||||
|
||||
// AuthenticationEvidence describes the observable result of a key probe.
|
||||
type AuthenticationEvidence struct {
|
||||
HTTP HTTPExecutionEvidence
|
||||
Accepted *bool
|
||||
Overflow bool
|
||||
Unrecognized bool
|
||||
}
|
||||
|
||||
// OpenRouterKeyEndpoint derives the default key endpoint from a base endpoint.
|
||||
func OpenRouterKeyEndpoint(base string) (string, error) {
|
||||
return url.JoinPath(base, "key")
|
||||
}
|
||||
|
||||
// ProbeOpenRouterKey executes and interprets an OpenRouter key request.
|
||||
func (c *OpenAICompatibleClient) ProbeOpenRouterKey(ctx context.Context, endpoint string, timeoutSeconds int, target domain.ExecutionTarget) (AuthenticationEvidence, error) {
|
||||
parsed, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return AuthenticationEvidence{}, err
|
||||
}
|
||||
parsed.User = nil
|
||||
response, evidence, requestErr := c.GetWithAuthenticationEvidence(ctx, parsed.String(), timeoutSeconds, target)
|
||||
result := AuthenticationEvidence{HTTP: evidence}
|
||||
if response != nil && response.Body != nil {
|
||||
defer response.Body.Close()
|
||||
}
|
||||
if !evidence.ResponseReceived && response == nil {
|
||||
return result, requestErr
|
||||
}
|
||||
status := evidence.HTTPStatus
|
||||
if status == nil && response != nil {
|
||||
status = &response.StatusCode
|
||||
}
|
||||
if status == nil {
|
||||
result.Unrecognized = true
|
||||
return result, nil
|
||||
}
|
||||
switch *status {
|
||||
case http.StatusUnauthorized:
|
||||
accepted := false
|
||||
result.Accepted = &accepted
|
||||
case http.StatusOK:
|
||||
if response == nil || response.Body == nil {
|
||||
result.Unrecognized = true
|
||||
return result, nil
|
||||
}
|
||||
contents, err := io.ReadAll(io.LimitReader(response.Body, maxAuthenticationResponseBytes+1))
|
||||
if len(contents) > maxAuthenticationResponseBytes {
|
||||
result.Overflow = true
|
||||
return result, nil
|
||||
}
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
if recognizedOpenRouterKeyResponse(contents) {
|
||||
accepted := true
|
||||
result.Accepted = &accepted
|
||||
} else {
|
||||
result.Unrecognized = true
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func recognizedOpenRouterKeyResponse(contents []byte) bool {
|
||||
var envelope map[string]json.RawMessage
|
||||
if err := json.Unmarshal(contents, &envelope); err != nil || envelope == nil {
|
||||
return false
|
||||
}
|
||||
var data map[string]json.RawMessage
|
||||
if err := json.Unmarshal(envelope["data"], &data); err != nil || data == nil {
|
||||
return false
|
||||
}
|
||||
label := bytes.TrimSpace(data["label"])
|
||||
var labelValue string
|
||||
if len(label) == 0 || label[0] != '"' || json.Unmarshal(label, &labelValue) != nil {
|
||||
return false
|
||||
}
|
||||
freeTier := bytes.TrimSpace(data["is_free_tier"])
|
||||
if !bytes.Equal(freeTier, []byte("true")) && !bytes.Equal(freeTier, []byte("false")) {
|
||||
return false
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(data["usage"]))
|
||||
decoder.UseNumber()
|
||||
var number any
|
||||
if decoder.Decode(&number) != nil {
|
||||
return false
|
||||
}
|
||||
_, ok := number.(json.Number)
|
||||
return ok
|
||||
}
|
||||
146
internal/llm/openrouter_authentication_test.go
Normal file
146
internal/llm/openrouter_authentication_test.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"go.maximumdirect.net/promptkit/internal/domain"
|
||||
)
|
||||
|
||||
func TestProbeOpenRouterKey(t *testing.T) {
|
||||
valid := `{"data":{"label":"x","is_free_tier":false,"usage":0}}`
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
status int
|
||||
body string
|
||||
key string
|
||||
accepted *bool
|
||||
unrecognized, overflow bool
|
||||
}{
|
||||
{name: "recognized", status: 200, body: valid, key: "secret", accepted: boolValue(true)},
|
||||
{name: "exact limit", status: 200, body: valid + strings.Repeat(" ", maxAuthenticationResponseBytes-len(valid)), accepted: boolValue(true)},
|
||||
{name: "unknown", status: 200, body: `{"data":{"label":"x","is_free_tier":false,"usage":"0"}}`, unrecognized: true},
|
||||
{name: "trailing value", status: 200, body: valid + ` {}`, unrecognized: true},
|
||||
{name: "overflow", status: 200, body: strings.Repeat("x", maxAuthenticationResponseBytes+1), overflow: true},
|
||||
{name: "unauthorized anonymous", status: 401, accepted: boolValue(false)},
|
||||
{name: "other status", status: 503},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var authorization string
|
||||
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{HTTPClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
if req.URL.User != nil {
|
||||
t.Fatal("request retained URL credentials")
|
||||
}
|
||||
authorization = req.Header.Get("Authorization")
|
||||
return &http.Response{StatusCode: tc.status, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(tc.body))}, nil
|
||||
})}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := client.ProbeOpenRouterKey(context.Background(), "https://user:password@example.invalid/key", 0, domain.ExecutionTarget{APIKey: tc.key})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.HTTP.Endpoint != "https://example.invalid/key" || got.HTTP.HTTPStatus == nil || *got.HTTP.HTTPStatus != tc.status || got.Unrecognized != tc.unrecognized || got.Overflow != tc.overflow {
|
||||
t.Fatalf("evidence = %+v", got)
|
||||
}
|
||||
if (got.Accepted == nil) != (tc.accepted == nil) || got.Accepted != nil && *got.Accepted != *tc.accepted {
|
||||
t.Fatalf("acceptance = %v", got.Accepted)
|
||||
}
|
||||
if (authorization != "") != (tc.key != "") || got.HTTP.CredentialsSupplied != (tc.key != "") {
|
||||
t.Fatalf("credential evidence = %+v", got.HTTP)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func boolValue(value bool) *bool { return &value }
|
||||
|
||||
type authenticationFailingBody struct{ err error }
|
||||
|
||||
func (b authenticationFailingBody) Read([]byte) (int, error) { return 0, b.err }
|
||||
func (b authenticationFailingBody) Close() error { return nil }
|
||||
|
||||
func TestProbeOpenRouterKeyRetainsBodyFailure(t *testing.T) {
|
||||
want := errors.New("interrupted")
|
||||
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{HTTPClient: &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{StatusCode: 200, Header: make(http.Header), Body: authenticationFailingBody{err: want}}, nil
|
||||
})}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := client.ProbeOpenRouterKey(context.Background(), "https://example.invalid/key", 0, domain.ExecutionTarget{})
|
||||
if !errors.Is(err, want) || !got.HTTP.ResponseReceived {
|
||||
t.Fatalf("evidence = %+v, error = %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
type observedAuthenticationBody struct {
|
||||
reader io.Reader
|
||||
read int
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (b *observedAuthenticationBody) Read(p []byte) (int, error) {
|
||||
n, err := b.reader.Read(p)
|
||||
b.read += n
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (b *observedAuthenticationBody) Close() error {
|
||||
b.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestProbeOpenRouterKeyBoundsAndClosesBodies(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
status int
|
||||
wantRead int
|
||||
}{
|
||||
{"overflow", http.StatusOK, maxAuthenticationResponseBytes + 1},
|
||||
{"unauthorized", http.StatusUnauthorized, 0},
|
||||
{"other status", http.StatusServiceUnavailable, 0},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
body := &observedAuthenticationBody{reader: strings.NewReader(strings.Repeat("x", 2*maxAuthenticationResponseBytes))}
|
||||
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{HTTPClient: &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{StatusCode: tc.status, ContentLength: 1, Header: make(http.Header), Body: body}, nil
|
||||
})}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := client.ProbeOpenRouterKey(context.Background(), "https://example.invalid/key", 0, domain.ExecutionTarget{})
|
||||
if err != nil || body.read != tc.wantRead || !body.closed || got.Overflow != (tc.status == http.StatusOK) {
|
||||
t.Fatalf("evidence = %+v, read = %d, closed = %v, error = %v", got, body.read, body.closed, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenRouterKeyEndpoint(t *testing.T) {
|
||||
got, err := OpenRouterKeyEndpoint("https://example.invalid/api/v1/")
|
||||
if err != nil || got != "https://example.invalid/api/v1/key" {
|
||||
t.Fatalf("endpoint = %q, error = %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeOpenRouterKeyPreservesCancellation(t *testing.T) {
|
||||
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{HTTPClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
<-req.Context().Done()
|
||||
return nil, req.Context().Err()
|
||||
})}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
got, err := client.ProbeOpenRouterKey(ctx, "https://example.invalid/key", 0, domain.ExecutionTarget{})
|
||||
if !errors.Is(err, context.Canceled) || got.Accepted != nil || got.HTTP.ResponseReceived {
|
||||
t.Fatalf("evidence = %+v, error = %v", got, err)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"go.maximumdirect.net/promptkit/internal/domain"
|
||||
@@ -13,11 +10,11 @@ import (
|
||||
"go.maximumdirect.net/promptkit/internal/llm"
|
||||
)
|
||||
|
||||
type authenticationProbe struct {
|
||||
client smokeHTTPGetter
|
||||
type smokeAuthenticationClient interface {
|
||||
ProbeOpenRouterKey(context.Context, string, int, domain.ExecutionTarget) (llm.AuthenticationEvidence, error)
|
||||
}
|
||||
|
||||
const maxAuthenticationResponseBytes = 1 << 20
|
||||
type authenticationProbe struct{ client smokeAuthenticationClient }
|
||||
|
||||
type authenticationProbeOutcome struct {
|
||||
supported bool
|
||||
@@ -28,16 +25,33 @@ type authenticationProbeOutcome struct {
|
||||
failure domain.ExecutionFailureKind
|
||||
}
|
||||
|
||||
func (p authenticationProbe) run(
|
||||
ctx context.Context,
|
||||
backend domain.Backend,
|
||||
target domain.SmokeTestTarget,
|
||||
) (authenticationProbeOutcome, error) {
|
||||
func (p authenticationProbe) run(ctx context.Context, backend domain.Backend, target domain.SmokeTestTarget) (authenticationProbeOutcome, error) {
|
||||
outcome, err := p.support(backend)
|
||||
if err != nil || !outcome.supported || (outcome.applicable != nil && !*outcome.applicable) {
|
||||
return outcome, err
|
||||
}
|
||||
return p.runOpenRouterKey(ctx, target)
|
||||
result, requestErr := p.client.ProbeOpenRouterKey(ctx, target.Endpoint, target.EffectiveSettings.TimeoutSeconds, target.EffectiveSettings)
|
||||
outcome.evidence = result.HTTP
|
||||
outcome.credentialsAccepted = result.Accepted
|
||||
if result.Overflow {
|
||||
outcome.reason = domain.SmokeTestReasonAuthenticationResponseLimitExceeded
|
||||
outcome.failure = domain.ExecutionFailureOther
|
||||
} else if result.Unrecognized {
|
||||
outcome.reason = domain.SmokeTestReasonAuthenticationResponseUnrecognized
|
||||
outcome.failure = domain.ExecutionFailureMalformedResponse
|
||||
} else if requestErr != nil {
|
||||
if !result.HTTP.DispatchInvoked {
|
||||
return outcome, fmt.Errorf("execute authentication request: %w", requestErr)
|
||||
}
|
||||
outcome.failure = httpProbeFailure(ctx, requestErr)
|
||||
} else if result.HTTP.ResponseReceived {
|
||||
if result.HTTP.HTTPStatus != nil && *result.HTTP.HTTPStatus != http.StatusOK && *result.HTTP.HTTPStatus != http.StatusUnauthorized {
|
||||
outcome.failure = generation.ClassifyFailure(llm.ErrUnexpectedStatus)
|
||||
}
|
||||
} else {
|
||||
outcome.failure = httpProbeFailure(ctx, nil)
|
||||
}
|
||||
return outcome, nil
|
||||
}
|
||||
|
||||
func (p authenticationProbe) support(backend domain.Backend) (authenticationProbeOutcome, error) {
|
||||
@@ -50,11 +64,7 @@ func (p authenticationProbe) support(backend domain.Backend) (authenticationProb
|
||||
}
|
||||
switch *authentication.Kind {
|
||||
case "none":
|
||||
return authenticationProbeOutcome{
|
||||
supported: true,
|
||||
applicable: boolPointer(false),
|
||||
reason: domain.SmokeTestReasonAuthenticationNotApplicable,
|
||||
}, nil
|
||||
return authenticationProbeOutcome{supported: true, applicable: boolPointer(false), reason: domain.SmokeTestReasonAuthenticationNotApplicable}, nil
|
||||
case "openrouter_key":
|
||||
if p.client == nil {
|
||||
return authenticationProbeOutcome{reason: domain.SmokeTestReasonHTTPDiagnosticsUnavailable}, nil
|
||||
@@ -64,139 +74,3 @@ func (p authenticationProbe) support(backend domain.Backend) (authenticationProb
|
||||
return authenticationProbeOutcome{}, fmt.Errorf("%w: unsupported authentication probe kind %q", ErrInvalidRequest, *authentication.Kind)
|
||||
}
|
||||
}
|
||||
|
||||
func (p authenticationProbe) runOpenRouterKey(
|
||||
ctx context.Context,
|
||||
target domain.SmokeTestTarget,
|
||||
) (authenticationProbeOutcome, error) {
|
||||
endpoint, err := endpointWithoutUserInfo(target.Endpoint)
|
||||
if err != nil {
|
||||
return authenticationProbeOutcome{}, fmt.Errorf("prepare authentication endpoint: %w", err)
|
||||
}
|
||||
response, evidence, requestErr := p.client.GetWithAuthenticationEvidence(
|
||||
ctx,
|
||||
endpoint,
|
||||
target.EffectiveSettings.TimeoutSeconds,
|
||||
target.EffectiveSettings,
|
||||
)
|
||||
if response != nil && response.Body != nil {
|
||||
defer response.Body.Close()
|
||||
}
|
||||
outcome := authenticationProbeOutcome{
|
||||
supported: true,
|
||||
applicable: boolPointer(true),
|
||||
evidence: evidence,
|
||||
}
|
||||
if evidence.ResponseReceived || response != nil {
|
||||
status := evidence.HTTPStatus
|
||||
if status == nil && response != nil {
|
||||
status = &response.StatusCode
|
||||
}
|
||||
if status == nil {
|
||||
outcome.failure = domain.ExecutionFailureMalformedResponse
|
||||
outcome.reason = domain.SmokeTestReasonAuthenticationResponseUnrecognized
|
||||
return outcome, nil
|
||||
}
|
||||
switch *status {
|
||||
case http.StatusUnauthorized:
|
||||
outcome.credentialsAccepted = boolPointer(false)
|
||||
case http.StatusOK:
|
||||
if response == nil || response.Body == nil {
|
||||
outcome.reason = domain.SmokeTestReasonAuthenticationResponseUnrecognized
|
||||
outcome.failure = domain.ExecutionFailureMalformedResponse
|
||||
return outcome, nil
|
||||
}
|
||||
contents, bodyErr := io.ReadAll(io.LimitReader(response.Body, maxAuthenticationResponseBytes+1))
|
||||
if len(contents) > maxAuthenticationResponseBytes {
|
||||
outcome.reason = domain.SmokeTestReasonAuthenticationResponseLimitExceeded
|
||||
outcome.failure = domain.ExecutionFailureOther
|
||||
return outcome, nil
|
||||
}
|
||||
if bodyErr != nil {
|
||||
outcome.failure = httpProbeFailure(ctx, bodyErr)
|
||||
return outcome, nil
|
||||
}
|
||||
recognized, bodyErr := recognizedOpenRouterKeyResponse(bytes.NewReader(contents))
|
||||
if bodyErr != nil {
|
||||
outcome.failure = httpProbeFailure(ctx, bodyErr)
|
||||
return outcome, nil
|
||||
}
|
||||
if recognized {
|
||||
outcome.credentialsAccepted = boolPointer(true)
|
||||
} else {
|
||||
outcome.reason = domain.SmokeTestReasonAuthenticationResponseUnrecognized
|
||||
outcome.failure = domain.ExecutionFailureMalformedResponse
|
||||
}
|
||||
default:
|
||||
outcome.failure = generation.ClassifyFailure(llm.ErrUnexpectedStatus)
|
||||
}
|
||||
return outcome, nil
|
||||
}
|
||||
|
||||
if evidence.DispatchInvoked {
|
||||
if requestErr != nil {
|
||||
outcome.failure = httpProbeFailure(ctx, requestErr)
|
||||
} else {
|
||||
outcome.failure = httpProbeFailure(ctx, nil)
|
||||
}
|
||||
return outcome, nil
|
||||
}
|
||||
if requestErr != nil {
|
||||
return outcome, fmt.Errorf("execute authentication request: %w", requestErr)
|
||||
}
|
||||
outcome.failure = generation.ClassifyFailure(llm.ErrTransport)
|
||||
return outcome, nil
|
||||
}
|
||||
|
||||
func recognizedOpenRouterKeyResponse(body io.Reader) (bool, error) {
|
||||
contents, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
var envelope map[string]json.RawMessage
|
||||
if err := json.Unmarshal(contents, &envelope); err != nil || envelope == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
dataJSON, ok := envelope["data"]
|
||||
if !ok || !isJSONObject(dataJSON) {
|
||||
return false, nil
|
||||
}
|
||||
var data map[string]json.RawMessage
|
||||
if err := json.Unmarshal(dataJSON, &data); err != nil || data == nil {
|
||||
return false, nil
|
||||
}
|
||||
label, labelOK := data["label"]
|
||||
freeTier, freeTierOK := data["is_free_tier"]
|
||||
usage, usageOK := data["usage"]
|
||||
if !labelOK || !isJSONString(label) || !freeTierOK || !isJSONBoolean(freeTier) || !usageOK {
|
||||
return false, nil
|
||||
}
|
||||
var number any
|
||||
usageDecoder := json.NewDecoder(bytes.NewReader(usage))
|
||||
usageDecoder.UseNumber()
|
||||
if usageDecoder.Decode(&number) != nil {
|
||||
return false, nil
|
||||
}
|
||||
_, ok = number.(json.Number)
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
func isJSONObject(value json.RawMessage) bool {
|
||||
value = bytes.TrimSpace(value)
|
||||
return len(value) > 0 && value[0] == '{'
|
||||
}
|
||||
|
||||
func isJSONString(value json.RawMessage) bool {
|
||||
value = bytes.TrimSpace(value)
|
||||
if len(value) == 0 || value[0] != '"' {
|
||||
return false
|
||||
}
|
||||
var parsed string
|
||||
return json.Unmarshal(value, &parsed) == nil
|
||||
}
|
||||
|
||||
func isJSONBoolean(value json.RawMessage) bool {
|
||||
value = bytes.TrimSpace(value)
|
||||
return bytes.Equal(value, []byte("true")) || bytes.Equal(value, []byte("false"))
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ func (b *measuredAuthenticationBody) Read(p []byte) (int, error) {
|
||||
func (b *measuredAuthenticationBody) Close() error { b.closed = true; return nil }
|
||||
|
||||
func TestAuthenticationResponseRecognitionBound(t *testing.T) {
|
||||
const maxAuthenticationResponseBytes = 1 << 20
|
||||
valid := `{"data":{"label":"member","is_free_tier":false,"usage":0}}`
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
@@ -211,21 +212,26 @@ func TestAuthenticationProbeInterpretsOpenRouterResponses(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthenticationProbeRecognizesOnlyOneCompleteJSONObject(t *testing.T) {
|
||||
for _, body := range []string{
|
||||
`{"data":{"label":"member","is_free_tier":false,"usage":1}} {}`,
|
||||
`{"data":[]}`,
|
||||
`{"data":{"label":"member","is_free_tier":false,"usage":null}}`,
|
||||
`{"data":{"label":"member","is_free_tier":0,"usage":1}}`,
|
||||
`{"data":{"is_free_tier":false,"usage":1}}`,
|
||||
`{"data":{"label":null,"is_free_tier":false,"usage":1}}`,
|
||||
for _, tc := range []struct {
|
||||
body string
|
||||
accepted bool
|
||||
}{
|
||||
{`{"data":{"label":"member","is_free_tier":false,"usage":1}} {}`, false},
|
||||
{`{"data":[]}`, false},
|
||||
{`{"data":{"label":"member","is_free_tier":false,"usage":null}}`, false},
|
||||
{`{"data":{"label":"member","is_free_tier":0,"usage":1}}`, false},
|
||||
{`{"data":{"is_free_tier":false,"usage":1}}`, false},
|
||||
{`{"data":{"label":null,"is_free_tier":false,"usage":1}}`, false},
|
||||
{`{"data":{"label":"","is_free_tier":false,"usage":1e2,"ignored":true},"ignored":true}`, true},
|
||||
} {
|
||||
if recognized, err := recognizedOpenRouterKeyResponse(strings.NewReader(body)); err != nil || recognized {
|
||||
t.Errorf("recognized invalid account response %s", body)
|
||||
client := reachabilityClient(t, func(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{StatusCode: http.StatusOK, Header: make(http.Header), Body: ioNopCloser(tc.body)}, nil
|
||||
})
|
||||
outcome, err := (authenticationProbe{client: client}).run(context.Background(), backendWithAuthenticationKind("openrouter_key"), domain.SmokeTestTarget{Endpoint: "https://example.invalid/key"})
|
||||
if err != nil || (outcome.credentialsAccepted != nil && *outcome.credentialsAccepted) != tc.accepted {
|
||||
t.Errorf("account response %s: outcome = %+v, error = %v", tc.body, outcome, err)
|
||||
}
|
||||
}
|
||||
if recognized, err := recognizedOpenRouterKeyResponse(strings.NewReader(`{"data":{"label":"","is_free_tier":false,"usage":1e2,"ignored":true},"ignored":true}`)); err != nil || !recognized {
|
||||
t.Fatal("valid minimal identifying response was rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticationProbeTreatsInterruptedResponseBodyAsTransportFailure(t *testing.T) {
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
|
||||
type smokeHTTPGetter interface {
|
||||
GetWithEvidence(context.Context, string, int) (*http.Response, llm.HTTPExecutionEvidence, error)
|
||||
GetWithAuthenticationEvidence(context.Context, string, int, domain.ExecutionTarget) (*http.Response, llm.HTTPExecutionEvidence, error)
|
||||
}
|
||||
|
||||
type reachabilityProbe struct {
|
||||
|
||||
@@ -3,10 +3,10 @@ package usecase
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"go.maximumdirect.net/promptkit/internal/defaults"
|
||||
"go.maximumdirect.net/promptkit/internal/domain"
|
||||
"go.maximumdirect.net/promptkit/internal/llm"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -176,7 +176,7 @@ func resolveSmokeTestEndpoint(kind domain.SmokeTestKind, baseEndpoint string, sm
|
||||
return *authentication.Endpoint, nil
|
||||
}
|
||||
if authentication.Kind != nil && *authentication.Kind == "openrouter_key" {
|
||||
return url.JoinPath(baseEndpoint, "key")
|
||||
return llm.OpenRouterKeyEndpoint(baseEndpoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
type SmokeTester struct {
|
||||
runner *Runner
|
||||
httpClient smokeHTTPGetter
|
||||
authClient smokeAuthenticationClient
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
@@ -24,18 +25,22 @@ type SmokeTester struct {
|
||||
// leaves HTTP operations unsupported rather than falling back to another client.
|
||||
func NewSmokeTester(runner *Runner, httpClient *llm.OpenAICompatibleClient) *SmokeTester {
|
||||
var getter smokeHTTPGetter
|
||||
var authentication smokeAuthenticationClient
|
||||
if httpClient != nil {
|
||||
getter = httpClient
|
||||
authentication = httpClient
|
||||
}
|
||||
return &SmokeTester{runner: runner, httpClient: getter, now: time.Now}
|
||||
return &SmokeTester{runner: runner, httpClient: getter, authClient: authentication, now: time.Now}
|
||||
}
|
||||
|
||||
func newSmokeTesterWithClock(runner *Runner, httpClient *llm.OpenAICompatibleClient, now func() time.Time) *SmokeTester {
|
||||
var getter smokeHTTPGetter
|
||||
var authentication smokeAuthenticationClient
|
||||
if httpClient != nil {
|
||||
getter = httpClient
|
||||
authentication = httpClient
|
||||
}
|
||||
return &SmokeTester{runner: runner, httpClient: getter, now: now}
|
||||
return &SmokeTester{runner: runner, httpClient: getter, authClient: authentication, now: now}
|
||||
}
|
||||
|
||||
// Run selects and executes one diagnostic, retaining only factual results.
|
||||
@@ -126,7 +131,7 @@ func (t *SmokeTester) Run(ctx context.Context, request domain.SmokeTestRequest)
|
||||
return t.finishHTTP(ctx, report, finish)
|
||||
case domain.SmokeTestAuthentication:
|
||||
requestStartedAt := now().UTC()
|
||||
outcome, err := (authenticationProbe{client: t.httpClient}).run(ctx, selection.backend, selection.target)
|
||||
outcome, err := (authenticationProbe{client: t.authClient}).run(ctx, selection.backend, selection.target)
|
||||
requestDuration := nonnegativeDuration(now().UTC().Sub(requestStartedAt))
|
||||
if err != nil {
|
||||
if contextErr := ctx.Err(); contextErr != nil {
|
||||
@@ -349,7 +354,7 @@ func (t *SmokeTester) setSupport(report *domain.SmokeTestReport, selection *smok
|
||||
report.Applicable = boolPointer(true)
|
||||
return nil
|
||||
case domain.SmokeTestAuthentication:
|
||||
outcome, err := (authenticationProbe{client: t.httpClient}).support(selection.backend)
|
||||
outcome, err := (authenticationProbe{client: t.authClient}).support(selection.backend)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user