mirror of
https://github.com/docker/docker-agent.git
synced 2026-11-04 10:02:15 +00:00
138 lines
4.6 KiB
Go
138 lines
4.6 KiB
Go
package runtime
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/docker/docker-agent/pkg/agent"
|
|
"github.com/docker/docker-agent/pkg/chat"
|
|
"github.com/docker/docker-agent/pkg/config/latest"
|
|
modelprovider "github.com/docker/docker-agent/pkg/model/provider"
|
|
"github.com/docker/docker-agent/pkg/model/provider/base"
|
|
"github.com/docker/docker-agent/pkg/model/provider/options"
|
|
"github.com/docker/docker-agent/pkg/modelsdev"
|
|
"github.com/docker/docker-agent/pkg/session"
|
|
"github.com/docker/docker-agent/pkg/team"
|
|
"github.com/docker/docker-agent/pkg/tools"
|
|
)
|
|
|
|
type imageOutputProvider struct {
|
|
id string
|
|
stream chat.MessageStream
|
|
calls atomic.Int64
|
|
}
|
|
|
|
func (p *imageOutputProvider) ID() modelsdev.ID { return modelsdev.ParseIDOrZero(p.id) }
|
|
|
|
func (p *imageOutputProvider) CreateChatCompletionStream(context.Context, []chat.Message, []tools.Tool) (chat.MessageStream, error) {
|
|
p.calls.Add(1)
|
|
if p.stream == nil {
|
|
return nil, errors.New("no stream configured")
|
|
}
|
|
return p.stream, nil
|
|
}
|
|
|
|
func (p *imageOutputProvider) BaseConfig() base.Config {
|
|
cfg := base.Config{ModelConfig: latest.ModelConfig{
|
|
OutputCapabilities: &latest.OutputCapabilitiesConfig{Image: new(true)},
|
|
}}
|
|
cfg.RebuildProvider = func(context.Context, *latest.ModelConfig, ...options.Opt) (modelprovider.Provider, error) {
|
|
return p, nil
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
func (p *imageOutputProvider) MaxTokens() int { return 0 }
|
|
|
|
func newTitleTestRuntime(t *testing.T, root *agent.Agent) *LocalRuntime {
|
|
t.Helper()
|
|
rt, err := NewLocalRuntime(t.Context(), team.New(team.WithAgents(root)),
|
|
WithSessionCompaction(false), WithModelStore(mockModelStore{}))
|
|
require.NoError(t, err)
|
|
return rt
|
|
}
|
|
|
|
// Image-output models are eligible title candidates because Gemini title requests
|
|
// deliberately omit response modalities and the media marker instruction.
|
|
func TestLocalRuntime_TitleGenerator_AllImageOutputCandidatesRemainEligible(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
primary := &imageOutputProvider{id: "google/image-primary"}
|
|
title := &imageOutputProvider{id: "google/image-title"}
|
|
fallback := &imageOutputProvider{id: "google/image-fallback"}
|
|
root := agent.New("root", "test",
|
|
agent.WithModel(primary),
|
|
agent.WithTitleModel(title),
|
|
agent.WithFallbackModel(fallback),
|
|
)
|
|
rt := newTitleTestRuntime(t, root)
|
|
|
|
assert.NotNil(t, rt.TitleGenerator(t.Context()),
|
|
"image-output models remain eligible for title generation")
|
|
assert.Zero(t, primary.calls.Load())
|
|
assert.Zero(t, title.calls.Load())
|
|
assert.Zero(t, fallback.calls.Load())
|
|
}
|
|
|
|
// A dedicated image-output title model remains first in the candidate order;
|
|
// title requests omit image modalities, and failure falls through to the agent model.
|
|
func TestLocalRuntime_TitleGenerator_ImageOutputTitleModelFallsThrough(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
imageTitle := &imageOutputProvider{id: "google/image-title"}
|
|
safe := &countingProvider{
|
|
id: "safe/model",
|
|
stream: newStreamBuilder().AddContent("A Title").AddStopWithUsage(5, 3).Build(),
|
|
}
|
|
root := agent.New("root", "test",
|
|
agent.WithModel(safe),
|
|
agent.WithTitleModel(imageTitle),
|
|
)
|
|
rt := newTitleTestRuntime(t, root)
|
|
|
|
gen := rt.TitleGenerator(t.Context())
|
|
require.NotNil(t, gen)
|
|
|
|
generated, err := gen.Generate(t.Context(), "sess-1", []string{"hello"})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "A Title", generated)
|
|
assert.Equal(t, int64(1), imageTitle.calls.Load(), "the image-output title model is attempted first")
|
|
assert.Equal(t, 1, safe.callCount)
|
|
}
|
|
|
|
// The title candidate list must not affect the main generation stream: an
|
|
// image-output model remains usable by both normal and title calls.
|
|
func TestRunStream_ImageOutputOnlyModel_MainStreamUnaffected(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
provider := &imageOutputProvider{
|
|
id: "google/image-model",
|
|
stream: newStreamBuilder().AddContent("here is your cat").AddStopWithUsage(10, 5).Build(),
|
|
}
|
|
root := agent.New("root", "test", agent.WithModel(provider))
|
|
rt := newTitleTestRuntime(t, root)
|
|
|
|
require.NotNil(t, rt.TitleGenerator(t.Context()))
|
|
|
|
sess := session.New(session.WithUserMessage("draw a cat"))
|
|
var content strings.Builder
|
|
for ev := range rt.RunStream(t.Context(), sess) {
|
|
switch e := ev.(type) {
|
|
case *ErrorEvent:
|
|
t.Fatalf("main stream must stay successful, got ErrorEvent %q", e.Error)
|
|
case *AgentChoiceEvent:
|
|
content.WriteString(e.Content)
|
|
}
|
|
}
|
|
|
|
assert.Equal(t, "here is your cat", content.String())
|
|
assert.Equal(t, int64(1), provider.calls.Load(), "exactly the main generation request reaches the provider")
|
|
assert.Empty(t, sess.Title, "the session keeps its default title when title generation is skipped")
|
|
}
|