Add OpenRouter authentication diagnostics
This commit is contained in:
187
internal/usecase/smoke_authentication.go
Normal file
187
internal/usecase/smoke_authentication.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
||||
"gitea.maximumdirect.net/eric/promptkit/internal/generation"
|
||||
"gitea.maximumdirect.net/eric/promptkit/internal/llm"
|
||||
)
|
||||
|
||||
type authenticationProbe struct {
|
||||
client smokeHTTPGetter
|
||||
}
|
||||
|
||||
type authenticationProbeOutcome struct {
|
||||
supported bool
|
||||
applicable *bool
|
||||
reason domain.SmokeTestReason
|
||||
evidence llm.HTTPExecutionEvidence
|
||||
credentialsAccepted *bool
|
||||
failure domain.ExecutionFailureKind
|
||||
}
|
||||
|
||||
func (p authenticationProbe) run(
|
||||
ctx context.Context,
|
||||
backend domain.Backend,
|
||||
target domain.SmokeTestTarget,
|
||||
) (authenticationProbeOutcome, error) {
|
||||
if backend.SmokeTests == nil || backend.SmokeTests.Authentication == nil {
|
||||
return authenticationProbeOutcome{reason: domain.SmokeTestReasonProbeNotConfigured}, nil
|
||||
}
|
||||
authentication := backend.SmokeTests.Authentication
|
||||
if authentication.Kind == nil {
|
||||
return authenticationProbeOutcome{}, fmt.Errorf("%w: authentication probe kind is missing", ErrInvalidRequest)
|
||||
}
|
||||
switch *authentication.Kind {
|
||||
case "none":
|
||||
return authenticationProbeOutcome{
|
||||
supported: true,
|
||||
applicable: boolPointer(false),
|
||||
reason: domain.SmokeTestReasonAuthenticationNotApplicable,
|
||||
}, nil
|
||||
case "openrouter_key":
|
||||
return p.runOpenRouterKey(ctx, target)
|
||||
default:
|
||||
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) {
|
||||
applicable := boolPointer(true)
|
||||
if p.client == nil {
|
||||
return authenticationProbeOutcome{
|
||||
applicable: applicable,
|
||||
reason: domain.SmokeTestReasonHTTPDiagnosticsUnavailable,
|
||||
}, nil
|
||||
}
|
||||
|
||||
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: applicable,
|
||||
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
|
||||
}
|
||||
recognized, bodyErr := recognizedOpenRouterKeyResponse(response.Body)
|
||||
if bodyErr != nil {
|
||||
outcome.failure = generation.ClassifyFailure(fmt.Errorf("%w: %w", llm.ErrTransport, 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 = generation.ClassifyFailure(fmt.Errorf("%w: %w", llm.ErrTransport, requestErr))
|
||||
} else {
|
||||
outcome.failure = generation.ClassifyFailure(llm.ErrTransport)
|
||||
}
|
||||
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"))
|
||||
}
|
||||
Reference in New Issue
Block a user