Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
788
backend/sdk/api/handlers/model_execution_test.go
Normal file
788
backend/sdk/api/handlers/model_execution_test.go
Normal file
|
|
@ -0,0 +1,788 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue