Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions internal/runtime/executor/openai_compat_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,22 @@ func NewOpenAICompatExecutor(provider string, cfg *config.Config) *OpenAICompatE
return &OpenAICompatExecutor{provider: provider, cfg: cfg}
}

func openAICompatThinkingModel(requestedModel, resolvedModel string) string {
requestedModel = strings.TrimSpace(requestedModel)
if requestedModel == "" {
return strings.TrimSpace(resolvedModel)
}
resolvedSuffix := thinking.ParseSuffix(resolvedModel)
if resolvedSuffix.HasSuffix && resolvedSuffix.RawSuffix != "" {
requestedBase := strings.TrimSpace(thinking.ParseSuffix(requestedModel).ModelName)
if requestedBase == "" {
requestedBase = requestedModel
}
return requestedBase + "(" + resolvedSuffix.RawSuffix + ")"
}
return requestedModel
}

// Identifier implements cliproxyauth.ProviderExecutor.
func (e *OpenAICompatExecutor) Identifier() string { return e.provider }

Expand Down Expand Up @@ -113,13 +129,14 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
originalPayload := originalPayloadSource
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, opts.Stream)
translated := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, opts.Stream)
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
thinkingModel := openAICompatThinkingModel(requestedModel, req.Model)

translated, err = thinking.ApplyThinking(translated, req.Model, from.String(), to.String(), e.Identifier())
translated, err = thinking.ApplyThinking(translated, thinkingModel, from.String(), to.String(), e.Identifier())
if err != nil {
return resp, err
}

requestedModel := helps.PayloadRequestedModel(opts, req.Model)
requestPath := helps.PayloadRequestPath(opts)
translated = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, to.String(), from.String(), "", translated, originalTranslated, requestedModel, requestPath, opts.Headers)
if opts.Alt == "responses/compact" {
Expand Down Expand Up @@ -314,13 +331,14 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
originalPayload := originalPayloadSource
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
translated := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, true)
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
thinkingModel := openAICompatThinkingModel(requestedModel, req.Model)

translated, err = thinking.ApplyThinking(translated, req.Model, from.String(), to.String(), e.Identifier())
translated, err = thinking.ApplyThinking(translated, thinkingModel, from.String(), to.String(), e.Identifier())
if err != nil {
return nil, err
}

requestedModel := helps.PayloadRequestedModel(opts, req.Model)
requestPath := helps.PayloadRequestPath(opts)
translated = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, to.String(), from.String(), "", translated, originalTranslated, requestedModel, requestPath, opts.Headers)

Expand Down Expand Up @@ -585,7 +603,9 @@ func (e *OpenAICompatExecutor) CountTokens(ctx context.Context, auth *cliproxyau

modelForCounting := baseModel

translated, err := thinking.ApplyThinking(translated, req.Model, from.String(), to.String(), e.Identifier())
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
thinkingModel := openAICompatThinkingModel(requestedModel, req.Model)
translated, err := thinking.ApplyThinking(translated, thinkingModel, from.String(), to.String(), e.Identifier())
if err != nil {
return cliproxyexecutor.Response{}, err
}
Expand Down
91 changes: 91 additions & 0 deletions internal/runtime/executor/openai_compat_executor_compact_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"testing"

"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
Expand Down Expand Up @@ -106,6 +107,96 @@ func TestOpenAICompatExecutorPayloadOverrideWinsOverThinkingSuffix(t *testing.T)
}
}

func TestOpenAICompatExecutorThinkingUsesRequestedAliasCapabilities(t *testing.T) {
var gotBody []byte
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
gotBody = body
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"chatcmpl_1","object":"chat.completion","choices":[{"index":0,"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}]}`))
}))
defer server.Close()

reg := registry.GetGlobalRegistry()
reg.RegisterClient("test-openai-compat-requested-alias-thinking", "openai-compatibility", []*registry.ModelInfo{{
ID: "shared-model",
Object: "model",
Type: "openai-compatibility",
Thinking: &registry.ThinkingSupport{Levels: []string{"low", "medium", "high"}},
}})
t.Cleanup(func() {
reg.UnregisterClient("test-openai-compat-requested-alias-thinking")
})

executor := NewOpenAICompatExecutor("openai-compatibility", &config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"base_url": server.URL + "/v1",
"api_key": "test",
}}
payload := []byte(`{"model":"shared-model(xhigh)","messages":[{"role":"user","content":"hi"}]}`)
_, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "gpt-5.5(xhigh)",
Payload: payload,
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FromString("openai"),
Stream: false,
Metadata: map[string]any{
cliproxyexecutor.RequestedModelMetadataKey: "shared-model(xhigh)",
},
})
if err == nil {
t.Fatalf("Execute error = nil, want xhigh rejected for requested alias")
}
if gotBody != nil {
t.Fatalf("upstream received body despite invalid requested alias thinking: %s", string(gotBody))
}
}

func TestOpenAICompatExecutorThinkingPreservesResolvedSuffixForAlias(t *testing.T) {
var gotBody []byte
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
gotBody = body
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"chatcmpl_1","object":"chat.completion","choices":[{"index":0,"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}]}`))
}))
defer server.Close()

reg := registry.GetGlobalRegistry()
reg.RegisterClient("test-openai-compat-resolved-suffix-thinking", "openai-compatibility", []*registry.ModelInfo{{
ID: "fast",
Object: "model",
Type: "openai-compatibility",
Thinking: &registry.ThinkingSupport{Levels: []string{"low", "medium", "high", "xhigh"}},
}})
t.Cleanup(func() {
reg.UnregisterClient("test-openai-compat-resolved-suffix-thinking")
})

executor := NewOpenAICompatExecutor("openai-compatibility", &config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"base_url": server.URL + "/v1",
"api_key": "test",
}}
payload := []byte(`{"model":"fast","messages":[{"role":"user","content":"hi"}]}`)
_, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "gpt-5.5(xhigh)",
Payload: payload,
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FromString("openai"),
Stream: false,
Metadata: map[string]any{
cliproxyexecutor.RequestedModelMetadataKey: "fast",
},
})
if err != nil {
t.Fatalf("Execute error: %v", err)
}
if got := gjson.GetBytes(gotBody, "reasoning_effort").String(); got != "xhigh" {
t.Fatalf("reasoning_effort = %q, want xhigh; body=%s", got, string(gotBody))
}
}

func TestOpenAICompatExecutorImagesGenerationsPassthrough(t *testing.T) {
var gotPath string
var gotBody []byte
Expand Down
117 changes: 110 additions & 7 deletions sdk/cliproxy/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor"
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
"github.com/router-for-me/CLIProxyAPI/v7/internal/watcher"
"github.com/router-for-me/CLIProxyAPI/v7/internal/watcher/diff"
Expand Down Expand Up @@ -2237,24 +2238,28 @@ func buildOpenAICompatibilityConfigModels(compat *config.OpenAICompatibility) []
if compat == nil || len(compat.Models) == 0 {
return nil
}
modelCounts := make(map[string]int, len(compat.Models))
for i := range compat.Models {
if modelID := openAICompatibilityModelID(compat.Models[i]); modelID != "" {
modelCounts[modelID]++
}
}
pooledThinkingLevels := openAICompatibilityPooledThinkingLevels(compat.Models, modelCounts)
now := time.Now().Unix()
models := make([]*ModelInfo, 0, len(compat.Models))
for i := range compat.Models {
model := compat.Models[i]
modelID := strings.TrimSpace(model.Alias)
if modelID == "" {
modelID = strings.TrimSpace(model.Name)
}
modelID := openAICompatibilityModelID(model)
if modelID == "" {
continue
}
modelType := "openai-compatibility"
if model.Image {
modelType = registry.OpenAIImageModelType
}
thinking := model.Thinking
if thinking == nil && !model.Image {
thinking = &registry.ThinkingSupport{Levels: []string{"low", "medium", "high"}}
thinking := openAICompatibilityModelThinking(model, modelID)
if levels, ok := pooledThinkingLevels[modelID]; ok && !model.Image {
thinking = &registry.ThinkingSupport{Levels: append([]string(nil), levels...)}
}
models = append(models, &ModelInfo{
ID: modelID,
Expand All @@ -2270,6 +2275,104 @@ func buildOpenAICompatibilityConfigModels(compat *config.OpenAICompatibility) []
return models
}

func openAICompatibilityModelID(model config.OpenAICompatibilityModel) string {
modelID := strings.TrimSpace(model.Alias)
if modelID == "" {
modelID = strings.TrimSpace(model.Name)
}
return modelID
}

func openAICompatibilityPooledThinkingLevels(models []config.OpenAICompatibilityModel, modelCounts map[string]int) map[string][]string {
pooled := make(map[string][]string)
for i := range models {
model := models[i]
modelID := openAICompatibilityModelID(model)
if modelID == "" || modelCounts[modelID] <= 1 || model.Image {
continue
}
levels := openAICompatibilityModelThinkingLevels(model, modelID)
if existing, ok := pooled[modelID]; ok {
pooled[modelID] = intersectThinkingLevels(existing, levels)
} else {
pooled[modelID] = append([]string(nil), levels...)
}
}
for modelID, levels := range pooled {
if len(levels) == 0 {
pooled[modelID] = defaultOpenAICompatibilityThinkingLevels()
}
}
return pooled
}

func openAICompatibilityModelThinking(model config.OpenAICompatibilityModel, modelID string) *registry.ThinkingSupport {
if model.Image {
return nil
}
if model.Thinking != nil {
return cloneThinkingSupport(model.Thinking)
}
staticModelID := strings.TrimSpace(model.Name)
if staticModelID == "" {
staticModelID = strings.TrimSpace(modelID)
}
staticModelID = strings.TrimSpace(thinking.ParseSuffix(staticModelID).ModelName)
if upstream := registry.LookupStaticModelInfo(staticModelID); upstream != nil && upstream.Thinking != nil {
if len(upstream.Thinking.Levels) > 0 {
return cloneThinkingLevels(upstream.Thinking)
}
}
return &registry.ThinkingSupport{Levels: defaultOpenAICompatibilityThinkingLevels()}
}

func openAICompatibilityModelThinkingLevels(model config.OpenAICompatibilityModel, modelID string) []string {
thinking := openAICompatibilityModelThinking(model, modelID)
if thinking == nil || len(thinking.Levels) == 0 {
return defaultOpenAICompatibilityThinkingLevels()
}
return append([]string(nil), thinking.Levels...)
}

func intersectThinkingLevels(left, right []string) []string {
out := make([]string, 0, len(left))
for _, candidate := range left {
candidate = strings.TrimSpace(candidate)
if candidate == "" {
continue
}
for _, supported := range right {
if strings.EqualFold(candidate, strings.TrimSpace(supported)) {
out = append(out, candidate)
break
}
}
}
return out
}

func defaultOpenAICompatibilityThinkingLevels() []string {
return []string{"low", "medium", "high"}
}

func cloneThinkingLevels(thinking *registry.ThinkingSupport) *registry.ThinkingSupport {
if thinking == nil {
return nil
}
return &registry.ThinkingSupport{Levels: append([]string(nil), thinking.Levels...)}
}

func cloneThinkingSupport(thinking *registry.ThinkingSupport) *registry.ThinkingSupport {
if thinking == nil {
return nil
}
cloned := *thinking
if thinking.Levels != nil {
cloned.Levels = append([]string(nil), thinking.Levels...)
}
return &cloned
}

func buildConfigModels[T modelEntry](models []T, ownedBy, modelType string) []*ModelInfo {
if len(models) == 0 {
return nil
Expand Down
Loading
Loading