Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
064a650763 | ||
|
|
6ecb431ccd | ||
|
|
b29f8775e1 |
@@ -32,14 +32,25 @@ func NewOpenAICompatibleLocalAdapter(raw string, transport http.RoundTripper) (*
|
|||||||
// NewOpenAICompatibleLocalAdapterWithAPIKey creates an adapter for a gateway
|
// NewOpenAICompatibleLocalAdapterWithAPIKey creates an adapter for a gateway
|
||||||
// whose bearer credential was resolved by the trusted runtime boundary.
|
// whose bearer credential was resolved by the trusted runtime boundary.
|
||||||
func NewOpenAICompatibleLocalAdapterWithAPIKey(raw, apiKey string, transport http.RoundTripper) (*OpenAICompatibleLocalAdapter, error) {
|
func NewOpenAICompatibleLocalAdapterWithAPIKey(raw, apiKey string, transport http.RoundTripper) (*OpenAICompatibleLocalAdapter, error) {
|
||||||
|
return NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout(raw, apiKey, transport, 10*time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout creates a local gateway
|
||||||
|
// adapter with an explicit invocation deadline. The request context remains
|
||||||
|
// authoritative for per-call cancellation; this client deadline prevents a
|
||||||
|
// provider from holding a worker indefinitely.
|
||||||
|
func NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout(raw, apiKey string, transport http.RoundTripper, timeout time.Duration) (*OpenAICompatibleLocalAdapter, error) {
|
||||||
base, err := url.Parse(raw)
|
base, err := url.Parse(raw)
|
||||||
if err != nil || base.Scheme != "http" || base.Host == "" || strings.TrimSuffix(base.Path, "/") != "/v1" || base.RawQuery != "" || base.Fragment != "" || base.User != nil {
|
if err != nil || base.Scheme != "http" || base.Host == "" || strings.TrimSuffix(base.Path, "/") != "/v1" || base.RawQuery != "" || base.Fragment != "" || base.User != nil {
|
||||||
return nil, errors.New("invalid local compatible endpoint")
|
return nil, errors.New("invalid local compatible endpoint")
|
||||||
}
|
}
|
||||||
|
if timeout <= 0 {
|
||||||
|
return nil, errors.New("local compatible timeout must be positive")
|
||||||
|
}
|
||||||
if transport == nil {
|
if transport == nil {
|
||||||
transport = http.DefaultTransport
|
transport = http.DefaultTransport
|
||||||
}
|
}
|
||||||
return &OpenAICompatibleLocalAdapter{base: base, client: &http.Client{Transport: transport, Timeout: 10 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}, apiKey: strings.TrimSpace(apiKey)}, nil
|
return &OpenAICompatibleLocalAdapter{base: base, client: &http.Client{Transport: transport, Timeout: timeout, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}, apiKey: strings.TrimSpace(apiKey)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type compatibleModelsResponse struct {
|
type compatibleModelsResponse struct {
|
||||||
|
|||||||
@@ -6,12 +6,29 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type captureRoundTripper struct {
|
type captureRoundTripper struct {
|
||||||
authorization string
|
authorization string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOpenAICompatibleLocalAdapterUsesConfiguredTimeout(t *testing.T) {
|
||||||
|
adapter, err := NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout("http://gateway/v1", "", &captureRoundTripper{}, 47*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if adapter.client.Timeout != 47*time.Second {
|
||||||
|
t.Fatalf("unexpected client timeout: %s", adapter.client.Timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpenAICompatibleLocalAdapterRejectsNonPositiveTimeout(t *testing.T) {
|
||||||
|
if _, err := NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout("http://gateway/v1", "", nil, 0); err == nil {
|
||||||
|
t.Fatal("expected non-positive timeout to be rejected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (t *captureRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
|
func (t *captureRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||||
t.authorization = request.Header.Get("Authorization")
|
t.authorization = request.Header.Get("Authorization")
|
||||||
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`{"data":[]}`)), Header: make(http.Header), Request: request}, nil
|
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`{"data":[]}`)), Header: make(http.Header), Request: request}, nil
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ func run(ctx context.Context, plan manifest, timeout time.Duration, observed tim
|
|||||||
artifact.Corpus = append(artifact.Corpus, corpusRecord{ID: item.id, Prompt: item.prompt, PromptSHA256: digest(item.prompt), Critical: item.critical})
|
artifact.Corpus = append(artifact.Corpus, corpusRecord{ID: item.id, Prompt: item.prompt, PromptSHA256: digest(item.prompt), Critical: item.critical})
|
||||||
}
|
}
|
||||||
for _, provider := range plan.Providers {
|
for _, provider := range plan.Providers {
|
||||||
adapter, err := newProvider(provider)
|
adapter, err := newProvider(provider, timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return benchmarkArtifact{}, err
|
return benchmarkArtifact{}, err
|
||||||
}
|
}
|
||||||
@@ -158,10 +158,10 @@ func run(ctx context.Context, plan manifest, timeout time.Duration, observed tim
|
|||||||
return artifact, nil
|
return artifact, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newProvider(candidate providerCandidate) (models.LocalProvider, error) {
|
func newProvider(candidate providerCandidate, timeout time.Duration) (models.LocalProvider, error) {
|
||||||
switch candidate.ProviderKind {
|
switch candidate.ProviderKind {
|
||||||
case "openai-compatible-chat-v1":
|
case "openai-compatible-chat-v1":
|
||||||
return models.NewOpenAICompatibleLocalAdapter(candidate.Endpoint, nil)
|
return models.NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout(candidate.Endpoint, "", nil, timeout)
|
||||||
case "ollama-v1":
|
case "ollama-v1":
|
||||||
return models.NewOllamaAdapter(candidate.Endpoint, nil)
|
return models.NewOllamaAdapter(candidate.Endpoint, nil)
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
mediumAdapter, adapterErr := models.NewOpenAICompatibleLocalAdapterWithAPIKey(strings.TrimRight(mediumURL, "/"), mediumAPIKey, nil)
|
mediumTimeout := durationEnv("LOCAL_MEDIUM_PROVIDER_TIMEOUT", 60*time.Second)
|
||||||
|
mediumAdapter, adapterErr := models.NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout(strings.TrimRight(mediumURL, "/"), mediumAPIKey, nil, mediumTimeout)
|
||||||
if adapterErr != nil {
|
if adapterErr != nil {
|
||||||
log.Fatal(adapterErr)
|
log.Fatal(adapterErr)
|
||||||
}
|
}
|
||||||
@@ -160,6 +161,19 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func durationEnv(name string, fallback time.Duration) time.Duration {
|
||||||
|
value := strings.TrimSpace(os.Getenv(name))
|
||||||
|
if value == "" {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
parsed, err := time.ParseDuration(value)
|
||||||
|
if err != nil || parsed <= 0 {
|
||||||
|
log.Printf("invalid %s=%q; using %s", name, value, fallback)
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
|
||||||
func refreshProviderDiscoveries(resolver bindingResolver, service discoveryService) {
|
func refreshProviderDiscoveries(resolver bindingResolver, service discoveryService) {
|
||||||
for name := range service.providers {
|
for name := range service.providers {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"task_file": "dev/tasks/DEV-000174-medium-qualification-evidence.json",
|
"task_file": "dev/tasks/DEV-000175-medium-invocation-timeout.json",
|
||||||
"task_id": "DEV-000174",
|
"task_id": "DEV-000175",
|
||||||
"task_revision": 1
|
"task_revision": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"task_id": "DEV-000175",
|
||||||
|
"task_revision": 1,
|
||||||
|
"architecture_revision": "ADR-0018",
|
||||||
|
"base_repo_revision": "b29f8775e1217506098d5c9a0e99c1046494cba3",
|
||||||
|
"risk_tier": "E",
|
||||||
|
"intent": "Make local OpenAI-compatible provider invocation deadlines explicit per provider so slow but reachable fleet members can be qualified through the existing adapter path.",
|
||||||
|
"requirement_ids": ["WP-200", "WP-301", "AUDIT-COMPLETION-TRUTH"],
|
||||||
|
"allowed_change_scope": {"modules": ["cognitive adapter", "cognitive worker", "benchmark runtime"], "paths": ["adapters/models/openai_compatible_local.go", "adapters/models/openai_compatible_local_test.go", "cmd/cognitive-worker/main.go", "cmd/cognitive-benchmark/main.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000175-medium-invocation-timeout.json"]},
|
||||||
|
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "contracts/**", "modules/**", "adapters/postgres/**", ".gitea/**"]},
|
||||||
|
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
|
||||||
|
"contracts": {"consume": ["existing provider-neutral cognitive adapter contract", "existing provider environment configuration"], "may_change": ["provider-specific invocation timeout configuration"]},
|
||||||
|
"semantic_owner": "cognitive adapter/runtime",
|
||||||
|
"canonical_state": ["adapter request deadline"],
|
||||||
|
"state_transitions": ["fixed local adapter deadline -> explicit positive provider-specific deadline"],
|
||||||
|
"side_effects_and_privileges": ["none beyond existing provider requests"],
|
||||||
|
"persistence_and_migrations": ["none"],
|
||||||
|
"failure_retry_restart": ["non-positive configuration falls back to safe default; request context remains authoritative"],
|
||||||
|
"assumptions": ["the existing Medium runtime is reachable but may require more than 10 seconds for a complete response", "default CPU behavior remains 10 seconds unless separately configured"],
|
||||||
|
"acceptance_scenarios": ["adapter accepts positive explicit timeout", "adapter rejects non-positive timeout", "benchmark passes its case timeout to the adapter", "production Medium defaults to 60 seconds", "no provider call bypasses the adapter"],
|
||||||
|
"required_tests": ["go test ./...", "go vet ./...", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl/independent", "git diff --check", "CI"],
|
||||||
|
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user