launch/opencode: share one timeout across model capability probes

Avoid paying the full probe timeout once per remembered OpenCode model
when rebuilding inline config on launch.

Use a single shared timeout context for the entire capability probe pass,
so slow or unreachable Show() calls degrade to missing modalities metadata
without multiplying startup delay by the number of models.

Add a regression test covering the shared-timeout behavior.
This commit is contained in:
Eva Ho 2026-05-12 15:17:26 -04:00
parent 49e6507bf4
commit 538d90a896
2 changed files with 52 additions and 10 deletions

View file

@ -241,19 +241,21 @@ func readModelJSONModels() []string {
}
func buildModelEntries(ctx context.Context, client *api.Client, modelList []string) map[string]any {
if client != nil {
var cancel context.CancelFunc
if _, hasDeadline := ctx.Deadline(); !hasDeadline {
ctx, cancel = context.WithTimeout(ctx, openCodeModelShowTimeout)
defer cancel()
}
}
models := make(map[string]any)
for _, modelID := range modelList {
entry := map[string]any{
"name": modelID,
}
if client != nil {
showCtx := ctx
var cancel context.CancelFunc
if _, hasDeadline := ctx.Deadline(); !hasDeadline {
showCtx, cancel = context.WithTimeout(ctx, openCodeModelShowTimeout)
}
if resp, err := client.Show(showCtx, &api.ShowRequest{Model: modelID}); err == nil {
if resp, err := client.Show(ctx, &api.ShowRequest{Model: modelID}); err == nil {
if slices.Contains(resp.Capabilities, model.CapabilityVision) {
entry["modalities"] = map[string]any{
"input": []string{"text", "image"},
@ -261,9 +263,6 @@ func buildModelEntries(ctx context.Context, client *api.Client, modelList []stri
}
}
}
if cancel != nil {
cancel()
}
}
if isCloudModelName(modelID) {
if l, ok := lookupCloudModelLimit(modelID); ok {

View file

@ -11,7 +11,9 @@ import (
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"time"
"github.com/ollama/ollama/api"
)
@ -222,6 +224,47 @@ func TestBuildModelEntries(t *testing.T) {
t.Fatalf("modalities should not be set without an API client, got %v", entry["modalities"])
}
})
t.Run("uses one timeout budget across capability probes", func(t *testing.T) {
u, err := url.Parse("http://ollama.example")
if err != nil {
t.Fatalf("parse test URL: %v", err)
}
var mu sync.Mutex
waited := 0
client := api.NewClient(u, &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
mu.Lock()
if req.Context().Err() == nil {
waited++
}
mu.Unlock()
<-req.Context().Done()
return nil, req.Context().Err()
})})
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Millisecond)
defer cancel()
models := buildModelEntries(ctx, client, []string{"slow-1", "slow-2"})
for _, modelID := range []string{"slow-1", "slow-2"} {
entry, _ := models[modelID].(map[string]any)
if entry["name"] != modelID {
t.Fatalf("name for %q = %v, want %q", modelID, entry["name"], modelID)
}
if _, ok := entry["modalities"]; ok {
t.Fatalf("modalities for %q should not be set after probe timeout, got %v", modelID, entry["modalities"])
}
}
mu.Lock()
defer mu.Unlock()
if waited != 1 {
t.Fatalf("expected shared timeout to block one probe, waited on %d probes", waited)
}
})
}
func TestOpenCodeModels_ReturnsNil(t *testing.T) {