Files
2026-07-13 12:35:43 +08:00

789 lines
30 KiB
Go

package handlers
import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
"sync"
"testing"
"time"
"github.com/router-for-me/CLIProxyAPI/v7/internal/constant"
"github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
coreexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
)
type modelExecutionCaptureExecutor struct {
provider string
mu sync.Mutex
lastRequest coreexecutor.Request
lastOptions coreexecutor.Options
execute func(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error)
stream func(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (*coreexecutor.StreamResult, error)
}
type modelExecutionStatusHeaderError struct {
statusCode int
message string
headers http.Header
}
type modelExecutionSkipHost struct {
beforeSkip string
afterSkip string
respSkip string
streamSkip []string
}
func (h *modelExecutionSkipHost) InterceptRequestBeforeAuth(context.Context, pluginapi.RequestInterceptRequest) pluginapi.RequestInterceptResponse {
panic("InterceptRequestBeforeAuth called without skip")
}
func (h *modelExecutionSkipHost) InterceptRequestAfterAuth(context.Context, pluginapi.RequestInterceptRequest) pluginapi.RequestInterceptResponse {
panic("InterceptRequestAfterAuth called without skip")
}
func (h *modelExecutionSkipHost) InterceptResponse(context.Context, pluginapi.ResponseInterceptRequest) pluginapi.ResponseInterceptResponse {
panic("InterceptResponse called without skip")
}
func (h *modelExecutionSkipHost) InterceptStreamChunk(context.Context, pluginapi.StreamChunkInterceptRequest) pluginapi.StreamChunkInterceptResponse {
panic("InterceptStreamChunk called without skip")
}
func (h *modelExecutionSkipHost) InterceptRequestBeforeAuthExcept(ctx context.Context, req pluginapi.RequestInterceptRequest, skipPluginID string) pluginapi.RequestInterceptResponse {
h.beforeSkip = skipPluginID
return pluginapi.RequestInterceptResponse{
Headers: cloneHeader(req.Headers),
Body: cloneBytes(req.Body),
}
}
func (h *modelExecutionSkipHost) InterceptRequestAfterAuthExcept(ctx context.Context, req pluginapi.RequestInterceptRequest, skipPluginID string) pluginapi.RequestInterceptResponse {
h.afterSkip = skipPluginID
return pluginapi.RequestInterceptResponse{
Headers: cloneHeader(req.Headers),
Body: cloneBytes(req.Body),
}
}
func (h *modelExecutionSkipHost) InterceptResponseExcept(ctx context.Context, req pluginapi.ResponseInterceptRequest, skipPluginID string) pluginapi.ResponseInterceptResponse {
h.respSkip = skipPluginID
return pluginapi.ResponseInterceptResponse{
Headers: cloneHeader(req.ResponseHeaders),
Body: cloneBytes(req.Body),
}
}
func (h *modelExecutionSkipHost) InterceptStreamChunkExcept(ctx context.Context, req pluginapi.StreamChunkInterceptRequest, skipPluginID string) pluginapi.StreamChunkInterceptResponse {
h.streamSkip = append(h.streamSkip, skipPluginID)
return pluginapi.StreamChunkInterceptResponse{
Headers: cloneHeader(req.ResponseHeaders),
Body: cloneBytes(req.Body),
}
}
func (e modelExecutionStatusHeaderError) Error() string {
return e.message
}
func (e modelExecutionStatusHeaderError) StatusCode() int {
return e.statusCode
}
func (e modelExecutionStatusHeaderError) Headers() http.Header {
return e.headers
}
func (e *modelExecutionCaptureExecutor) Identifier() string {
if e.provider != "" {
return e.provider
}
return "codex"
}
func (e *modelExecutionCaptureExecutor) Execute(ctx context.Context, auth *coreauth.Auth, req coreexecutor.Request, opts coreexecutor.Options) (coreexecutor.Response, error) {
e.capture(req, opts)
if e.execute != nil {
return e.execute(ctx, auth, req, opts)
}
return coreexecutor.Response{Payload: []byte("model-execution-ok")}, nil
}
func (e *modelExecutionCaptureExecutor) ExecuteStream(ctx context.Context, auth *coreauth.Auth, req coreexecutor.Request, opts coreexecutor.Options) (*coreexecutor.StreamResult, error) {
e.capture(req, opts)
if e.stream != nil {
return e.stream(ctx, auth, req, opts)
}
chunks := make(chan coreexecutor.StreamChunk)
close(chunks)
return &coreexecutor.StreamResult{Chunks: chunks}, nil
}
func (e *modelExecutionCaptureExecutor) Refresh(ctx context.Context, auth *coreauth.Auth) (*coreauth.Auth, error) {
return auth, nil
}
func (e *modelExecutionCaptureExecutor) CountTokens(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) {
return coreexecutor.Response{Payload: []byte("0")}, nil
}
func (e *modelExecutionCaptureExecutor) HttpRequest(context.Context, *coreauth.Auth, *http.Request) (*http.Response, error) {
return nil, &coreauth.Error{Code: "not_implemented", Message: "HttpRequest not implemented", HTTPStatus: http.StatusNotImplemented}
}
func (e *modelExecutionCaptureExecutor) capture(req coreexecutor.Request, opts coreexecutor.Options) {
e.mu.Lock()
defer e.mu.Unlock()
e.lastRequest = coreexecutor.Request{
Model: req.Model,
Payload: cloneBytes(req.Payload),
Format: req.Format,
Metadata: req.Metadata,
}
e.lastOptions = coreexecutor.Options{
Stream: opts.Stream,
Alt: opts.Alt,
Headers: cloneHeader(opts.Headers),
Query: cloneURLValues(opts.Query),
OriginalRequest: cloneBytes(opts.OriginalRequest),
SourceFormat: opts.SourceFormat,
ResponseFormat: opts.ResponseFormat,
Metadata: opts.Metadata,
}
}
func (e *modelExecutionCaptureExecutor) captured() (coreexecutor.Request, coreexecutor.Options) {
e.mu.Lock()
defer e.mu.Unlock()
return e.lastRequest, e.lastOptions
}
func newModelExecutionHandler(t *testing.T, model string, executor *modelExecutionCaptureExecutor, cfg *sdkconfig.SDKConfig) *BaseAPIHandler {
t.Helper()
manager := coreauth.NewManager(nil, nil, nil)
manager.RegisterExecutor(executor)
auth := &coreauth.Auth{
ID: "model-execution-" + model,
Provider: executor.Identifier(),
Status: coreauth.StatusActive,
Metadata: map[string]any{"email": model + "@example.com"},
}
if _, errRegister := manager.Register(context.Background(), auth); errRegister != nil {
t.Fatalf("manager.Register(): %v", errRegister)
}
registry.GetGlobalRegistry().RegisterClient(auth.ID, auth.Provider, []*registry.ModelInfo{{ID: model}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(auth.ID)
})
return NewBaseAPIHandlers(cfg, manager)
}
func TestExecuteModelCarriesEntryAndExitProtocols(t *testing.T) {
model := "model-execution-nonstream-model"
requestBody := []byte(fmt.Sprintf(`{"model":%q}`, model))
executor := &modelExecutionCaptureExecutor{
execute: func(ctx context.Context, auth *coreauth.Auth, req coreexecutor.Request, opts coreexecutor.Options) (coreexecutor.Response, error) {
return coreexecutor.Response{
Payload: []byte(`{"ok":true}`),
Headers: http.Header{
"X-Upstream": []string{"nonstream"},
},
}, nil
},
}
handler := newModelExecutionHandler(t, model, executor, &sdkconfig.SDKConfig{PassthroughHeaders: true})
resp, errMsg := handler.ExecuteModel(context.Background(), ModelExecutionRequest{
EntryProtocol: "openai",
ExitProtocol: "claude",
Model: model,
Body: requestBody,
Headers: http.Header{"X-Callback": []string{"nonstream"}},
Query: url.Values{"q": []string{"callback"}},
})
if errMsg != nil {
t.Fatalf("ExecuteModel() error = %+v", errMsg)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d, want %d", resp.StatusCode, http.StatusOK)
}
if string(resp.Body) != `{"ok":true}` {
t.Fatalf("body = %q, want executor response", resp.Body)
}
if resp.Headers.Get("X-Upstream") != "nonstream" {
t.Fatalf("headers = %#v, want upstream header", resp.Headers)
}
gotReq, gotOpts := executor.captured()
if gotReq.Model != model {
t.Fatalf("executor model = %q, want %q", gotReq.Model, model)
}
if string(gotReq.Payload) != string(requestBody) {
t.Fatalf("executor payload = %q, want %q", gotReq.Payload, requestBody)
}
if gotOpts.Stream {
t.Fatal("executor stream option = true, want false")
}
if gotOpts.SourceFormat != sdktranslator.FormatOpenAI {
t.Fatalf("SourceFormat = %q, want %q", gotOpts.SourceFormat, sdktranslator.FormatOpenAI)
}
if gotOpts.ResponseFormat != sdktranslator.FormatClaude {
t.Fatalf("ResponseFormat = %q, want %q", gotOpts.ResponseFormat, sdktranslator.FormatClaude)
}
if gotOpts.Metadata[coreexecutor.RequestedModelMetadataKey] != model {
t.Fatalf("requested model metadata = %#v, want %q", gotOpts.Metadata[coreexecutor.RequestedModelMetadataKey], model)
}
if gotOpts.Metadata[modelExecutionMetadataSourceKey] != modelExecutionInternalSource {
t.Fatalf("source metadata = %#v, want %q", gotOpts.Metadata[modelExecutionMetadataSourceKey], modelExecutionInternalSource)
}
if gotOpts.Headers.Get("X-Callback") != "nonstream" {
t.Fatalf("executor headers = %#v, want callback header", gotOpts.Headers)
}
if gotOpts.Query.Get("q") != "callback" {
t.Fatalf("executor query = %#v, want callback query", gotOpts.Query)
}
}
func TestExecuteModelSkipsOriginatingPluginInterceptors(t *testing.T) {
model := "model-execution-skip-origin-model"
requestBody := []byte(fmt.Sprintf(`{"model":%q}`, model))
executor := &modelExecutionCaptureExecutor{}
handler := newModelExecutionHandler(t, model, executor, &sdkconfig.SDKConfig{})
skipHost := &modelExecutionSkipHost{}
handler.SetPluginHost(skipHost)
resp, errMsg := handler.ExecuteModel(context.Background(), ModelExecutionRequest{
EntryProtocol: "openai",
ExitProtocol: "openai",
Model: model,
Body: requestBody,
SkipInterceptorPluginID: "origin-plugin",
})
if errMsg != nil {
t.Fatalf("ExecuteModel() error = %+v", errMsg)
}
if string(resp.Body) != "model-execution-ok" {
t.Fatalf("body = %q, want executor response", resp.Body)
}
if skipHost.beforeSkip != "origin-plugin" || skipHost.afterSkip != "origin-plugin" || skipHost.respSkip != "origin-plugin" {
t.Fatalf("skip ids = before:%q after:%q response:%q, want origin-plugin", skipHost.beforeSkip, skipHost.afterSkip, skipHost.respSkip)
}
}
func TestExecuteModelStream(t *testing.T) {
model := "model-execution-stream-model"
requestBody := []byte(fmt.Sprintf(`{"model":%q,"stream":true}`, model))
executor := &modelExecutionCaptureExecutor{
stream: func(ctx context.Context, auth *coreauth.Auth, req coreexecutor.Request, opts coreexecutor.Options) (*coreexecutor.StreamResult, error) {
chunks := make(chan coreexecutor.StreamChunk, 1)
chunks <- coreexecutor.StreamChunk{Payload: []byte("stream-one")}
close(chunks)
return &coreexecutor.StreamResult{
Headers: http.Header{"X-Upstream": []string{"stream"}},
Chunks: chunks,
}, nil
},
}
handler := newModelExecutionHandler(t, model, executor, &sdkconfig.SDKConfig{PassthroughHeaders: true})
stream, errMsg := handler.ExecuteModelStream(context.Background(), ModelExecutionRequest{
EntryProtocol: "openai",
ExitProtocol: "claude",
Model: model,
Stream: true,
Body: requestBody,
Headers: http.Header{"X-Callback": []string{"stream"}},
})
if errMsg != nil {
t.Fatalf("ExecuteModelStream() error = %+v", errMsg)
}
if stream.StatusCode != http.StatusOK {
t.Fatalf("status = %d, want %d", stream.StatusCode, http.StatusOK)
}
if stream.Headers.Get("X-Upstream") != "stream" {
t.Fatalf("headers = %#v, want upstream header", stream.Headers)
}
chunk, ok := <-stream.Chunks
if !ok {
t.Fatal("stream chunks closed before payload")
}
if chunk.Err != nil {
t.Fatalf("stream chunk error = %+v", chunk.Err)
}
if string(chunk.Payload) != "stream-one" {
t.Fatalf("stream chunk payload = %q, want stream-one", chunk.Payload)
}
if chunk, ok = <-stream.Chunks; ok {
t.Fatalf("unexpected extra stream chunk: %+v", chunk)
}
gotReq, gotOpts := executor.captured()
if gotReq.Model != model {
t.Fatalf("executor model = %q, want %q", gotReq.Model, model)
}
if string(gotReq.Payload) != string(requestBody) {
t.Fatalf("executor payload = %q, want %q", gotReq.Payload, requestBody)
}
if !gotOpts.Stream {
t.Fatal("executor stream option = false, want true")
}
if gotOpts.SourceFormat != sdktranslator.FormatOpenAI {
t.Fatalf("SourceFormat = %q, want %q", gotOpts.SourceFormat, sdktranslator.FormatOpenAI)
}
if gotOpts.ResponseFormat != sdktranslator.FormatClaude {
t.Fatalf("ResponseFormat = %q, want %q", gotOpts.ResponseFormat, sdktranslator.FormatClaude)
}
if gotOpts.Metadata[coreexecutor.RequestedModelMetadataKey] != model {
t.Fatalf("requested model metadata = %#v, want %q", gotOpts.Metadata[coreexecutor.RequestedModelMetadataKey], model)
}
if gotOpts.Metadata[modelExecutionMetadataSourceKey] != modelExecutionInternalSource {
t.Fatalf("source metadata = %#v, want %q", gotOpts.Metadata[modelExecutionMetadataSourceKey], modelExecutionInternalSource)
}
if gotOpts.Headers.Get("X-Callback") != "stream" {
t.Fatalf("executor headers = %#v, want callback header", gotOpts.Headers)
}
}
func TestExecuteModelStreamSkipsOriginatingPluginInterceptors(t *testing.T) {
model := "model-execution-stream-skip-origin-model"
requestBody := []byte(fmt.Sprintf(`{"model":%q,"stream":true}`, model))
executor := &modelExecutionCaptureExecutor{
stream: func(ctx context.Context, auth *coreauth.Auth, req coreexecutor.Request, opts coreexecutor.Options) (*coreexecutor.StreamResult, error) {
chunks := make(chan coreexecutor.StreamChunk, 1)
chunks <- coreexecutor.StreamChunk{Payload: []byte("stream-one")}
close(chunks)
return &coreexecutor.StreamResult{Chunks: chunks}, nil
},
}
handler := newModelExecutionHandler(t, model, executor, &sdkconfig.SDKConfig{})
skipHost := &modelExecutionSkipHost{}
handler.SetPluginHost(skipHost)
stream, errMsg := handler.ExecuteModelStream(context.Background(), ModelExecutionRequest{
EntryProtocol: "openai",
ExitProtocol: "openai",
Model: model,
Stream: true,
Body: requestBody,
SkipInterceptorPluginID: "origin-plugin",
})
if errMsg != nil {
t.Fatalf("ExecuteModelStream() error = %+v", errMsg)
}
chunk, ok := <-stream.Chunks
if !ok {
t.Fatal("stream chunks closed before payload")
}
if string(chunk.Payload) != "stream-one" {
t.Fatalf("stream chunk payload = %q, want stream-one", chunk.Payload)
}
if skipHost.beforeSkip != "origin-plugin" || skipHost.afterSkip != "origin-plugin" {
t.Fatalf("request skip ids = before:%q after:%q, want origin-plugin", skipHost.beforeSkip, skipHost.afterSkip)
}
if len(skipHost.streamSkip) == 0 {
t.Fatal("stream interceptor was not called with skip")
}
for _, skipID := range skipHost.streamSkip {
if skipID != "origin-plugin" {
t.Fatalf("stream skip id = %q, want origin-plugin", skipID)
}
}
}
func TestExecuteModelStreamStartupError(t *testing.T) {
model := "model-execution-stream-startup-error-model"
requestBody := []byte(fmt.Sprintf(`{"model":%q,"stream":true}`, model))
executor := &modelExecutionCaptureExecutor{
stream: func(ctx context.Context, auth *coreauth.Auth, req coreexecutor.Request, opts coreexecutor.Options) (*coreexecutor.StreamResult, error) {
chunks := make(chan coreexecutor.StreamChunk, 1)
chunks <- coreexecutor.StreamChunk{Err: fmt.Errorf("startup failed")}
close(chunks)
return &coreexecutor.StreamResult{Chunks: chunks}, nil
},
}
handler := newModelExecutionHandler(t, model, executor, &sdkconfig.SDKConfig{})
stream, errMsg := handler.ExecuteModelStream(context.Background(), ModelExecutionRequest{
EntryProtocol: "openai",
ExitProtocol: "claude",
Model: model,
Stream: true,
Body: requestBody,
})
if errMsg == nil {
t.Fatal("ExecuteModelStream() error = nil, want startup error")
}
if errMsg.StatusCode != http.StatusInternalServerError {
t.Fatalf("status = %d, want %d", errMsg.StatusCode, http.StatusInternalServerError)
}
if errMsg.Error == nil || errMsg.Error.Error() != "startup failed" {
t.Fatalf("error = %v, want startup failed", errMsg.Error)
}
if stream.Chunks != nil {
t.Fatal("stream chunks created for startup error")
}
}
func TestExecuteModelStreamTerminalError(t *testing.T) {
model := "model-execution-stream-terminal-error-model"
requestBody := []byte(fmt.Sprintf(`{"model":%q,"stream":true}`, model))
errorHeaders := http.Header{"X-Stream-Error": []string{"terminal"}}
executor := &modelExecutionCaptureExecutor{
stream: func(ctx context.Context, auth *coreauth.Auth, req coreexecutor.Request, opts coreexecutor.Options) (*coreexecutor.StreamResult, error) {
chunks := make(chan coreexecutor.StreamChunk, 2)
chunks <- coreexecutor.StreamChunk{Payload: []byte("stream-before-error")}
chunks <- coreexecutor.StreamChunk{Err: modelExecutionStatusHeaderError{
statusCode: http.StatusTooManyRequests,
message: "rate limited",
headers: errorHeaders,
}}
close(chunks)
return &coreexecutor.StreamResult{Chunks: chunks}, nil
},
}
handler := newModelExecutionHandler(t, model, executor, &sdkconfig.SDKConfig{})
stream, errMsg := handler.ExecuteModelStream(context.Background(), ModelExecutionRequest{
EntryProtocol: "openai",
ExitProtocol: "claude",
Model: model,
Stream: true,
Body: requestBody,
})
if errMsg != nil {
t.Fatalf("ExecuteModelStream() error = %+v", errMsg)
}
chunk, ok := <-stream.Chunks
if !ok {
t.Fatal("stream chunks closed before payload")
}
if chunk.Err != nil {
t.Fatalf("first stream chunk error = %+v", chunk.Err)
}
if string(chunk.Payload) != "stream-before-error" {
t.Fatalf("first stream chunk payload = %q, want stream-before-error", chunk.Payload)
}
chunk, ok = <-stream.Chunks
if !ok {
t.Fatal("stream chunks closed before terminal error")
}
if len(chunk.Payload) != 0 {
t.Fatalf("terminal stream chunk payload = %q, want empty", chunk.Payload)
}
if chunk.Err == nil {
t.Fatal("terminal stream chunk error = nil")
}
if chunk.Err.StatusCode != http.StatusTooManyRequests {
t.Fatalf("terminal status = %d, want %d", chunk.Err.StatusCode, http.StatusTooManyRequests)
}
if chunk.Err.Message != "rate limited" {
t.Fatalf("terminal message = %q, want rate limited", chunk.Err.Message)
}
if chunk.Err.Error() != "rate limited" {
t.Fatalf("terminal Error() = %q, want rate limited", chunk.Err.Error())
}
if chunk.Err.Headers.Get("X-Stream-Error") != "terminal" {
t.Fatalf("terminal headers = %#v, want stream error header", chunk.Err.Headers)
}
if chunk, ok = <-stream.Chunks; ok {
t.Fatalf("unexpected extra stream chunk: %+v", chunk)
}
}
func TestExecuteModelStreamContextCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
dataChan := make(chan []byte)
errChan := make(chan *interfaces.ErrorMessage)
chunks := wrapModelExecutionChunks(ctx, dataChan, errChan, nil)
cancel()
timeout := time.NewTimer(time.Second)
defer timeout.Stop()
select {
case chunk, ok := <-chunks:
if ok {
t.Fatalf("stream chunks yielded after cancel: %+v", chunk)
}
case <-timeout.C:
t.Fatal("stream chunks did not close after context cancellation")
}
}
func TestExecuteProtocolWithAuthManagerUsesForcedProvider(t *testing.T) {
model := "interactions-agent-target"
requestBody := []byte(`{"agent":"agents/test-agent","input":"hi"}`)
executor := &modelExecutionCaptureExecutor{
provider: "gemini",
execute: func(ctx context.Context, auth *coreauth.Auth, req coreexecutor.Request, opts coreexecutor.Options) (coreexecutor.Response, error) {
return coreexecutor.Response{Payload: []byte(`{"id":"interaction_1"}`)}, nil
},
}
handler := newModelExecutionHandler(t, model, executor, &sdkconfig.SDKConfig{})
resp, errMsg := handler.ExecuteProtocolWithAuthManager(context.Background(), ProtocolExecutionRequest{
EntryProtocol: "interactions",
ExitProtocol: "interactions",
ForcedProvider: "gemini",
Model: model,
Body: requestBody,
})
if errMsg != nil {
t.Fatalf("ExecuteProtocolWithAuthManager() error = %+v", errMsg)
}
if string(resp.Body) != `{"id":"interaction_1"}` {
t.Fatalf("body = %q, want native interactions response", resp.Body)
}
gotReq, gotOpts := executor.captured()
if gotReq.Model != model {
t.Fatalf("executor model = %q, want %q", gotReq.Model, model)
}
if gotOpts.SourceFormat != sdktranslator.FormatInteractions {
t.Fatalf("SourceFormat = %q, want %q", gotOpts.SourceFormat, sdktranslator.FormatInteractions)
}
if gotOpts.ResponseFormat != sdktranslator.FormatInteractions {
t.Fatalf("ResponseFormat = %q, want %q", gotOpts.ResponseFormat, sdktranslator.FormatInteractions)
}
if gotOpts.Metadata[coreexecutor.RequestedModelMetadataKey] != model {
t.Fatalf("requested model metadata = %#v, want %q", gotOpts.Metadata[coreexecutor.RequestedModelMetadataKey], model)
}
}
func TestPreferExecutionProviderMovesPreferredFirst(t *testing.T) {
providers := preferExecutionProvider([]string{"gemini", "gemini-interactions", "claude"}, "gemini-interactions")
want := []string{"gemini-interactions", "gemini", "claude"}
if len(providers) != len(want) {
t.Fatalf("providers = %#v, want %#v", providers, want)
}
for i := range want {
if providers[i] != want[i] {
t.Fatalf("providers = %#v, want %#v", providers, want)
}
}
}
func TestAdjustExecutionProvidersExcludesInteractionsProviderForUnsupportedEntry(t *testing.T) {
providers := adjustExecutionProvidersForEntryProtocol("codex", []string{"gemini-interactions", "codex"})
want := []string{"codex"}
if len(providers) != len(want) {
t.Fatalf("providers = %#v, want %#v", providers, want)
}
for i := range want {
if providers[i] != want[i] {
t.Fatalf("providers = %#v, want %#v", providers, want)
}
}
}
func TestAdjustExecutionProvidersKeepsInteractionsProviderForSupportedNativeInteractionsEntries(t *testing.T) {
for _, entryProtocol := range []string{constant.OpenAI, constant.OpenaiResponse, constant.Claude, constant.Gemini} {
t.Run(entryProtocol, func(t *testing.T) {
providers := adjustExecutionProvidersForEntryProtocol(entryProtocol, []string{"gemini-interactions"})
want := []string{"gemini-interactions"}
if len(providers) != len(want) {
t.Fatalf("providers = %#v, want %#v", providers, want)
}
for i := range want {
if providers[i] != want[i] {
t.Fatalf("providers = %#v, want %#v", providers, want)
}
}
})
}
}
func TestExecuteModelStreamKeepsInteractionsProviderForOpenAIEntry(t *testing.T) {
model := "gemini-3.1-flash-lite"
requestBody := []byte(`{"model":"gemini-3.1-flash-lite","stream":true,"messages":[{"role":"user","content":"hi"}]}`)
executor := &modelExecutionCaptureExecutor{
provider: constant.GeminiInteractions,
stream: func(ctx context.Context, auth *coreauth.Auth, req coreexecutor.Request, opts coreexecutor.Options) (*coreexecutor.StreamResult, error) {
chunks := make(chan coreexecutor.StreamChunk, 1)
chunks <- coreexecutor.StreamChunk{Payload: []byte(`{"id":"chunk_1","object":"chat.completion.chunk","choices":[]}`)}
close(chunks)
return &coreexecutor.StreamResult{Chunks: chunks}, nil
},
}
handler := newModelExecutionHandler(t, model, executor, &sdkconfig.SDKConfig{})
stream, errMsg := handler.ExecuteModelStream(context.Background(), ModelExecutionRequest{
EntryProtocol: constant.OpenAI,
ExitProtocol: constant.OpenAI,
Model: model,
Stream: true,
Body: requestBody,
})
if errMsg != nil {
t.Fatalf("ExecuteModelStream() error = %+v", errMsg)
}
for range stream.Chunks {
}
gotReq, gotOpts := executor.captured()
if gotReq.Model != model {
t.Fatalf("executor model = %q, want %q", gotReq.Model, model)
}
if gotOpts.SourceFormat != sdktranslator.FormatOpenAI {
t.Fatalf("SourceFormat = %q, want %q", gotOpts.SourceFormat, sdktranslator.FormatOpenAI)
}
}
func TestExecuteProtocolWithAuthManagerAgentUsesSelectionModelForAuth(t *testing.T) {
selectionModel := "gemini-2.5-flash"
agentModel := "agents/test-agent"
requestBody := []byte(`{"agent":"agents/test-agent","input":"hi"}`)
executor := &modelExecutionCaptureExecutor{
provider: constant.GeminiInteractions,
execute: func(ctx context.Context, auth *coreauth.Auth, req coreexecutor.Request, opts coreexecutor.Options) (coreexecutor.Response, error) {
return coreexecutor.Response{Payload: []byte(`{"id":"interaction_1"}`)}, nil
},
}
manager := coreauth.NewManager(nil, nil, nil)
manager.RegisterExecutor(executor)
auth := &coreauth.Auth{
ID: "model-execution-agent-selection",
Provider: constant.GeminiInteractions,
Status: coreauth.StatusActive,
Metadata: map[string]any{"email": "agent-selection@example.com"},
}
registry.GetGlobalRegistry().RegisterClient(auth.ID, auth.Provider, []*registry.ModelInfo{{ID: selectionModel}, {ID: agentModel}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(auth.ID)
})
if _, errRegister := manager.Register(context.Background(), auth); errRegister != nil {
t.Fatalf("manager.Register(): %v", errRegister)
}
manager.RefreshSchedulerEntry(auth.ID)
handler := NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
resp, errMsg := handler.ExecuteProtocolWithAuthManager(context.Background(), ProtocolExecutionRequest{
EntryProtocol: "interactions",
ExitProtocol: "interactions",
ForcedProvider: constant.GeminiInteractions,
AuthSelectionModel: selectionModel,
Model: agentModel,
Body: requestBody,
})
if errMsg != nil {
t.Fatalf("ExecuteProtocolWithAuthManager() error = %+v", errMsg)
}
if string(resp.Body) != `{"id":"interaction_1"}` {
t.Fatalf("body = %q, want native interactions response", resp.Body)
}
gotReq, gotOpts := executor.captured()
if gotReq.Model != agentModel {
t.Fatalf("executor model = %q, want %q", gotReq.Model, agentModel)
}
if string(gotReq.Payload) != string(requestBody) {
t.Fatalf("executor payload = %q, want %q", gotReq.Payload, requestBody)
}
if gotOpts.Metadata[coreexecutor.AuthSelectionModelMetadataKey] != selectionModel {
t.Fatalf("auth selection metadata = %#v, want %q", gotOpts.Metadata[coreexecutor.AuthSelectionModelMetadataKey], selectionModel)
}
}
func TestExecuteProtocolStreamWithAuthManagerAgentUsesSelectionModelForAuth(t *testing.T) {
selectionModel := "gemini-2.5-flash"
agentModel := "agents/test-agent"
requestBody := []byte(`{"agent":"agents/test-agent","input":"hi","stream":true}`)
executor := &modelExecutionCaptureExecutor{
provider: constant.GeminiInteractions,
stream: func(ctx context.Context, auth *coreauth.Auth, req coreexecutor.Request, opts coreexecutor.Options) (*coreexecutor.StreamResult, error) {
chunks := make(chan coreexecutor.StreamChunk, 1)
chunks <- coreexecutor.StreamChunk{Payload: []byte(`{"id":"interaction_1"}`)}
close(chunks)
return &coreexecutor.StreamResult{Chunks: chunks}, nil
},
}
manager := coreauth.NewManager(nil, nil, nil)
manager.RegisterExecutor(executor)
auth := &coreauth.Auth{
ID: "model-execution-agent-stream-selection",
Provider: constant.GeminiInteractions,
Status: coreauth.StatusActive,
Metadata: map[string]any{"email": "agent-stream-selection@example.com"},
}
registry.GetGlobalRegistry().RegisterClient(auth.ID, auth.Provider, []*registry.ModelInfo{{ID: selectionModel}, {ID: agentModel}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(auth.ID)
})
if _, errRegister := manager.Register(context.Background(), auth); errRegister != nil {
t.Fatalf("manager.Register(): %v", errRegister)
}
manager.RefreshSchedulerEntry(auth.ID)
handler := NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
stream, errMsg := handler.ExecuteProtocolStreamWithAuthManager(context.Background(), ProtocolExecutionRequest{
EntryProtocol: "interactions",
ExitProtocol: "interactions",
ForcedProvider: constant.GeminiInteractions,
AuthSelectionModel: selectionModel,
Model: agentModel,
Stream: true,
Body: requestBody,
})
if errMsg != nil {
t.Fatalf("ExecuteProtocolStreamWithAuthManager() error = %+v", errMsg)
}
chunk, ok := <-stream.Chunks
if !ok {
t.Fatal("stream chunks closed before payload")
}
if chunk.Err != nil {
t.Fatalf("stream chunk error = %+v", chunk.Err)
}
if string(chunk.Payload) != `{"id":"interaction_1"}` {
t.Fatalf("stream chunk payload = %q, want native interactions response", chunk.Payload)
}
gotReq, gotOpts := executor.captured()
if gotReq.Model != agentModel {
t.Fatalf("executor model = %q, want %q", gotReq.Model, agentModel)
}
if string(gotReq.Payload) != string(requestBody) {
t.Fatalf("executor payload = %q, want %q", gotReq.Payload, requestBody)
}
if gotOpts.Metadata[coreexecutor.AuthSelectionModelMetadataKey] != selectionModel {
t.Fatalf("auth selection metadata = %#v, want %q", gotOpts.Metadata[coreexecutor.AuthSelectionModelMetadataKey], selectionModel)
}
}
func TestProvidersForExecutionForcedGeminiRejectsRouterProvider(t *testing.T) {
handler := NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, nil)
decision := modelRouteDecision{Provider: "claude", Model: "claude-sonnet-4"}
_, _, errMsg := handler.providersForExecution("agents/test-agent", "agents/test-agent", false, decision, modelExecutionOptions{ForcedProvider: "gemini"})
if errMsg == nil {
t.Fatal("providersForExecution() error = nil, want native interactions error")
}
if errMsg.StatusCode != http.StatusBadRequest {
t.Fatalf("status = %d, want %d", errMsg.StatusCode, http.StatusBadRequest)
}
if errMsg.Error == nil || !strings.Contains(errMsg.Error.Error(), "native interactions") {
t.Fatalf("error = %v, want native interactions message", errMsg.Error)
}
}
func TestProvidersForExecutionForcedGeminiUsesGeminiProvider(t *testing.T) {
handler := NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, nil)
providers, model, errMsg := handler.providersForExecution("agents/test-agent", "agents/test-agent", false, modelRouteDecision{}, modelExecutionOptions{ForcedProvider: "gemini"})
if errMsg != nil {
t.Fatalf("providersForExecution() error = %+v", errMsg)
}
if len(providers) != 1 || providers[0] != "gemini" {
t.Fatalf("providers = %#v, want [gemini]", providers)
}
if model != "agents/test-agent" {
t.Fatalf("model = %q, want agents/test-agent", model)
}
}