Compare commits

...
Author SHA1 Message Date
m 064a650763 chore: align medium timeout task base
CI / test (push) Successful in 36s
CI / test (pull_request) Successful in 38s
2026-08-22 22:11:43 +03:00
m 6ecb431ccd feat: configure medium provider invocation timeout
CI / test (push) Failing after 28s
CI / test (pull_request) Failing after 26s
2026-08-22 22:08:32 +03:00
m b29f8775e1 Merge pull request 'docs: record medium qualification timeout' (#147) from codex/medium-qualification-evidence into main
CI / test (push) Successful in 36s
CI / test (pull_request) Successful in 35s
2026-08-22 18:59:30 +00:00
6 changed files with 72 additions and 7 deletions
+12 -1
View File
@@ -32,14 +32,25 @@ func NewOpenAICompatibleLocalAdapter(raw string, transport http.RoundTripper) (*
// NewOpenAICompatibleLocalAdapterWithAPIKey creates an adapter for a gateway
// whose bearer credential was resolved by the trusted runtime boundary.
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)
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")
}
if timeout <= 0 {
return nil, errors.New("local compatible timeout must be positive")
}
if transport == nil {
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 {
@@ -6,12 +6,29 @@ import (
"net/http"
"strings"
"testing"
"time"
)
type captureRoundTripper struct {
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) {
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
+3 -3
View File
@@ -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})
}
for _, provider := range plan.Providers {
adapter, err := newProvider(provider)
adapter, err := newProvider(provider, timeout)
if err != nil {
return benchmarkArtifact{}, err
}
@@ -158,10 +158,10 @@ func run(ctx context.Context, plan manifest, timeout time.Duration, observed tim
return artifact, nil
}
func newProvider(candidate providerCandidate) (models.LocalProvider, error) {
func newProvider(candidate providerCandidate, timeout time.Duration) (models.LocalProvider, error) {
switch candidate.ProviderKind {
case "openai-compatible-chat-v1":
return models.NewOpenAICompatibleLocalAdapter(candidate.Endpoint, nil)
return models.NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout(candidate.Endpoint, "", nil, timeout)
case "ollama-v1":
return models.NewOllamaAdapter(candidate.Endpoint, nil)
default:
+15 -1
View File
@@ -69,7 +69,8 @@ func main() {
if err != nil {
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 {
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) {
for name := range service.providers {
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
+2 -2
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/DEV-000174-medium-qualification-evidence.json",
"task_id": "DEV-000174",
"task_file": "dev/tasks/DEV-000175-medium-invocation-timeout.json",
"task_id": "DEV-000175",
"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"]
}