feat(sdk): add Home credential capabilities and quota APIs

Use Home credential options as authoritative model capability
configuration for compat handling and prompt cache behavior.

Expose quota provider operations through the public plugin host API.
This commit is contained in:
hkfires
2026-09-28 19:00:46 +08:00
parent 59baf7314a
commit b9971ee3df
11 changed files with 597 additions and 14 deletions

View File

@@ -9,7 +9,7 @@ import (
// APIKeyModelIsCompat reports whether the selected API-key model enables
// compatibility handling for Claude thinking blocks.
func APIKeyModelIsCompat(req cliproxyexecutor.Request) bool {
modelInfo, ok := cliproxyauth.ResolvedAPIKeyModelInfo(req)
modelInfo, ok := cliproxyauth.ResolvedModelInfo(req)
return ok && modelInfo != nil && modelInfo.IsCompat
}

View File

@@ -367,3 +367,15 @@ func TestApplyRequestThinkingUsesSelectedPrefixedAPIKeyModel(t *testing.T) {
}
assertResponse("stream", streamPayload)
}
func TestAPIKeyModelIsCompatUsesAuthoritativeHomeCapabilities(t *testing.T) {
for _, enabled := range []bool{false, true} {
req := cliproxyexecutor.Request{Metadata: map[string]any{
"cliproxy.resolved_api_key_model_info": &registry.ModelInfo{IsCompat: !enabled},
"cliproxy.resolved_home_model_info": &registry.ModelInfo{IsCompat: enabled},
}}
if got := helps.APIKeyModelIsCompat(req); got != enabled {
t.Fatalf("Home compat=%v, got=%v", enabled, got)
}
}
}

View File

@@ -940,6 +940,33 @@ func (e *OpenAICompatExecutor) resolveCompatConfig(auth *cliproxyauth.Auth) *con
if auth == nil || e.cfg == nil {
return nil
}
if e.cfg.Home.Enabled {
// Home excludes provider credentials from the global configuration. These
// non-secret options belong to the credential selected for this attempt.
var options struct {
SupportPromptCacheKey bool `json:"support-prompt-cache-key"`
Models []config.OpenAICompatibilityModel `json:"models"`
}
present := false
if rawOptions, exists := auth.Metadata["credential_options"]; exists {
if data, errMarshal := json.Marshal(rawOptions); errMarshal == nil {
present = json.Unmarshal(data, &options) == nil
}
}
if raw, exists := auth.Attributes["support_prompt_cache_key"]; exists {
if value, errParse := strconv.ParseBool(raw); errParse == nil {
options.SupportPromptCacheKey = value
present = true
}
}
if present {
return &config.OpenAICompatibility{
Name: auth.Attributes["compat_name"],
SupportPromptCacheKey: options.SupportPromptCacheKey,
Models: options.Models,
}
}
}
if auth.AuthSourceKind() == cliproxyauth.AuthSourceConfig && auth.Attributes != nil {
if rawIndex := strings.TrimSpace(auth.Attributes["config_index"]); rawIndex != "" {
configIndex, errIndex := strconv.Atoi(rawIndex)

View File

@@ -0,0 +1,108 @@
package executor
import (
"context"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/router-for-me/CLIProxyAPI/v8/internal/config"
cpaauth "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/auth"
cpaexecutor "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/executor"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/translator"
"github.com/tidwall/gjson"
)
func TestHomeV8PromptCacheKey(t *testing.T) {
auth := &cpaauth.Auth{Provider: "compat", Attributes: map[string]string{"compat_name": "compat", "provider_key": "compat", "api_key": "fixture", "base_url": "https://example.test", "support_prompt_cache_key": "true"}, Metadata: map[string]any{"credential_options": map[string]any{"support-prompt-cache-key": true}}}
for _, home := range []bool{false, true} {
cfg := &config.Config{}
cfg.Home.Enabled = home
if !home {
cfg.OpenAICompatibility = []config.OpenAICompatibility{{Name: "compat", SupportPromptCacheKey: true}}
}
e := NewOpenAICompatExecutor("compat", cfg)
out, err := e.applyPromptCacheKey(context.Background(), auth, translator.FromString("openai"), "review-model", cpaexecutor.Request{Payload: []byte(`{"model":"review-model","prompt_cache_key":"fixture-cache-key"}`)}, cpaexecutor.Options{}, []byte(`{"model":"review-model"}`))
if err != nil {
t.Fatal(err)
}
got := gjson.GetBytes(out, "prompt_cache_key").String()
t.Logf("home=%v prompt_cache_key=%q", home, got)
if got != "fixture-cache-key" {
t.Errorf("home=%v: enabled prompt cache option is not consumed", home)
}
}
}
func TestHomeV8CompatOptionsReachUpstream(t *testing.T) {
bodies := make(chan []byte, 1)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
t.Error(err)
}
bodies <- body
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"fixture","choices":[{"message":{"role":"assistant","content":"ok"}}]}`))
}))
defer server.Close()
cfg := &config.Config{}
cfg.Home.Enabled = true
e := NewOpenAICompatExecutor("home-compat", cfg)
for _, enabled := range []bool{true, false, true} {
auth := &cpaauth.Auth{Provider: "home-compat", Attributes: map[string]string{"compat_name": "home-compat", "api_key": "fixture", "base_url": server.URL + "/v1"}, Metadata: map[string]any{"credential_options": map[string]any{
"support-prompt-cache-key": enabled,
"models": []any{map[string]any{"name": "upstream", "use-max-completion-tokens": enabled}},
}}}
payload := []byte(`{"model":"upstream","input":[{"role":"user","content":"hi"}],"max_output_tokens":512,"prompt_cache_key":"fixture-cache"}`)
_, err := e.Execute(context.Background(), auth, cpaexecutor.Request{Model: "upstream", Payload: payload}, cpaexecutor.Options{SourceFormat: translator.FromString("openai-response")})
if err != nil {
t.Fatal(err)
}
body := <-bodies
field := "max_tokens"
if enabled {
field = "max_completion_tokens"
}
if gjson.GetBytes(body, field).Int() != 512 {
t.Fatalf("enabled=%v missing %s in %s", enabled, field, body)
}
if enabled && gjson.GetBytes(body, "prompt_cache_key").String() != "fixture-cache" {
t.Fatalf("prompt cache missing: %s", body)
}
}
}
func TestHomeV8PromptCacheOverrideAndFallback(t *testing.T) {
for _, tc := range []struct {
name string
home bool
attrs map[string]string
metadata map[string]any
global, want bool
}{
{name: "explicit false", home: true, attrs: map[string]string{"support_prompt_cache_key": "false"}, metadata: map[string]any{"credential_options": map[string]any{"support-prompt-cache-key": true}}, global: true, want: false},
{name: "older Home fallback", home: true, global: true, want: true},
{name: "standalone ignores Home options", home: false, metadata: map[string]any{"credential_options": map[string]any{"support-prompt-cache-key": true}}, want: false},
{name: "metadata only", home: true, metadata: map[string]any{"credential_options": map[string]any{"support-prompt-cache-key": true}}, want: true},
} {
t.Run(tc.name, func(t *testing.T) {
cfg := &config.Config{OpenAICompatibility: []config.OpenAICompatibility{{Name: "compat", SupportPromptCacheKey: tc.global}}}
cfg.Home.Enabled = tc.home
if tc.attrs == nil {
tc.attrs = map[string]string{}
}
tc.attrs["compat_name"] = "compat"
e := NewOpenAICompatExecutor("compat", cfg)
auth := &cpaauth.Auth{Provider: "compat", Attributes: tc.attrs, Metadata: tc.metadata}
result, err := e.applyPromptCacheKey(context.Background(), auth, translator.FromString("openai"), "model", cpaexecutor.Request{Payload: []byte(`{"prompt_cache_key":"cache"}`)}, cpaexecutor.Options{}, []byte(`{"model":"model"}`))
if err != nil {
t.Fatal(err)
}
if got := gjson.GetBytes(result, "prompt_cache_key").Exists(); got != tc.want {
t.Fatalf("cache key present=%v, want=%v", got, tc.want)
}
})
}
}

View File

@@ -1,6 +1,7 @@
package auth
import (
"encoding/json"
"maps"
"strings"
@@ -81,6 +82,11 @@ func CodexAPIKeyModelIsCompat(cfg *internalconfig.Config, auth *Auth, model stri
return false
}
entry := resolveCodexAPIKeyConfig(cfg, auth)
if cfg.Home.Enabled {
if compat, configured := homeAPIKeyModelIsCompat(auth, model, model); configured {
return compat
}
}
if entry == nil || len(entry.Models) == 0 {
return false
}
@@ -112,6 +118,70 @@ func CodexAPIKeyModelIsCompat(cfg *internalconfig.Config, auth *Auth, model stri
return false
}
// homeAPIKeyModelIsCompat reads the selected credential's model configuration.
// A present models list is authoritative, including empty lists and false defaults.
func homeAPIKeyModelIsCompat(auth *Auth, model, routeModel string) (bool, bool) {
if auth == nil {
return false, false
}
raw, exists := auth.Metadata["credential_options"]
if !exists {
return false, false
}
data, errMarshal := json.Marshal(raw)
if errMarshal != nil {
return false, false
}
var options struct {
Models json.RawMessage `json:"models"`
}
if errDecode := json.Unmarshal(data, &options); errDecode != nil || len(options.Models) == 0 {
return false, false
}
var models []internalconfig.CodexModel
if errDecode := json.Unmarshal(options.Models, &models); errDecode != nil {
return false, false
}
requested := strings.TrimSpace(model)
if requested == "" {
return false, true
}
baseModel := strings.TrimSpace(thinking.ParseSuffix(requested).ModelName)
_, routeCandidates := modelAliasLookupCandidates(rewriteModelForAuth(strings.TrimSpace(routeModel), auth))
for _, route := range routeCandidates {
for _, candidate := range []string{requested, baseModel} {
for _, configured := range models {
name := strings.TrimSpace(configured.Name)
alias := strings.TrimSpace(configured.Alias)
if name == "" {
name = alias
}
if strings.EqualFold(name, candidate) && (strings.EqualFold(alias, route) || strings.EqualFold(name, route)) {
return configured.IsCompat, true
}
}
}
}
// Prefer upstream names over aliases and exact suffixes over base fallbacks.
for _, useAlias := range []bool{false, true} {
for _, candidate := range []string{requested, baseModel} {
if candidate == "" {
continue
}
for _, configured := range models {
name := strings.TrimSpace(configured.Name)
if useAlias || name == "" {
name = strings.TrimSpace(configured.Alias)
}
if strings.EqualFold(name, candidate) {
return configured.IsCompat, true
}
}
}
}
return false, true
}
func (m *Manager) attachResolvedAPIKeyModelInfo(req cliproxyexecutor.Request, auth *Auth, routeModel, upstreamModel string) cliproxyexecutor.Request {
return attachResolvedAPIKeyModelInfo(m.loadAPIKeyModelRouting(), req, auth, routeModel, upstreamModel)
}
@@ -205,23 +275,32 @@ func lookupCodexOAuthModelCapability(auth *Auth, upstreamModel string) (*registr
return nil, false
}
func attachResolvedHomeModelInfo(req cliproxyexecutor.Request, modelInfo *registry.ModelInfo, support ...*bool) cliproxyexecutor.Request {
func attachResolvedHomeModelInfo(req cliproxyexecutor.Request, auth *Auth, routeModel string, modelInfo *registry.ModelInfo, support *bool) cliproxyexecutor.Request {
if modelInfo == nil {
return req
}
selected := *modelInfo
if len(support) > 0 && support[0] != nil {
selected.SupportConfigurationUpdate = *support[0]
local, ok := ResolvedAPIKeyModelInfo(req)
if !ok {
local, ok = req.Metadata[resolvedCodexOAuthModelInfoMetadataKey].(*registry.ModelInfo)
}
sameModel := ok && local != nil && strings.EqualFold(strings.TrimSpace(thinking.ParseSuffix(local.ID).ModelName), strings.TrimSpace(thinking.ParseSuffix(selected.ID).ModelName))
if support != nil {
selected.SupportConfigurationUpdate = *support
} else {
selected.SupportConfigurationUpdate = false
local, ok := ResolvedAPIKeyModelInfo(req)
if !ok {
local, ok = req.Metadata[resolvedCodexOAuthModelInfoMetadataKey].(*registry.ModelInfo)
}
if ok && local != nil && strings.EqualFold(strings.TrimSpace(thinking.ParseSuffix(local.ID).ModelName), strings.TrimSpace(thinking.ParseSuffix(selected.ID).ModelName)) {
selected.SupportConfigurationUpdate = local.SupportConfigurationUpdate
selected.SupportConfigurationUpdate = sameModel && local.SupportConfigurationUpdate
}
upstreamModel := selected.ID
if auth != nil {
if dispatched := strings.TrimSpace(auth.Attributes[homeUpstreamModelAttributeKey]); dispatched != "" {
upstreamModel = dispatched
}
}
if compat, configured := homeAPIKeyModelIsCompat(auth, upstreamModel, routeModel); configured {
selected.IsCompat = compat
}
metadata := make(map[string]any, len(req.Metadata)+1)
maps.Copy(metadata, req.Metadata)
metadata[resolvedHomeModelInfoMetadataKey] = &selected

View File

@@ -1115,7 +1115,7 @@ func (m *Manager) executeStreamMixedOnce(ctx context.Context, providers []string
}
execReq := sanitizeDownstreamWebsocketFallbackRequest(execCtx, auth, req)
if selection != nil && !restoreExecutionModel {
execReq = attachResolvedHomeModelInfo(execReq, selection.modelInfo, selection.configurationUpdateSupport)
execReq = attachResolvedHomeModelInfo(execReq, auth, routeModel, selection.modelInfo, selection.configurationUpdateSupport)
}
streamExecutionModel := ""
if restoreExecutionModel {

View File

@@ -200,7 +200,7 @@ func (m *Manager) executeHomeOnce(ctx context.Context, providers []string, req c
}
execReq = attachResolvedExecutionModelInfo(routing, execReq, preparedAuth, routeModel, upstreamModel, restoreExecutionModel)
if !restoreExecutionModel {
execReq = attachResolvedHomeModelInfo(execReq, selection.modelInfo, selection.configurationUpdateSupport)
execReq = attachResolvedHomeModelInfo(execReq, preparedAuth, routeModel, selection.modelInfo, selection.configurationUpdateSupport)
}
if errCtx := execCtx.Err(); errCtx != nil {
releaseAttempt()

View File

@@ -0,0 +1,236 @@
package auth
import (
"encoding/json"
"testing"
internalconfig "github.com/router-for-me/CLIProxyAPI/v8/internal/config"
"github.com/router-for-me/CLIProxyAPI/v8/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/executionregistry"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/executor"
)
func TestHomeV8ModelCapabilitiesAndLegacyPayload(t *testing.T) {
for _, tc := range []struct {
name, raw string
contextLength int
compat bool
}{
{"v8", `{"id":"upstream","context_length":32768,"thinking":{"levels":["high","none"],"zero_allowed":true},"user_defined":false}`, 32768, true},
{"v8-with-redundant-fields", `{"id":"upstream","context_length":32768,"max_context_length":32768,"is_compat":false,"thinking":{"levels":["high","none"],"zero_allowed":true},"user_defined":false}`, 32768, true},
{"legacy", `{"id":"upstream","context_length":16384,"user_defined":true}`, 16384, false},
} {
t.Run(tc.name, func(t *testing.T) {
var wire homeDispatchModelInfo
if err := json.Unmarshal([]byte(tc.raw), &wire); err != nil {
t.Fatal(err)
}
auth := &Auth{}
if tc.compat {
auth.Metadata = map[string]any{"credential_options": map[string]any{"models": []internalconfig.CodexModel{{Name: "upstream", Alias: "alias", IsCompat: true}}}}
}
req := attachResolvedHomeModelInfo(cliproxyexecutor.Request{Model: "alias"}, auth, "alias", wire.registryModelInfo(), wire.SupportConfigurationUpdate)
info, ok := ResolvedModelInfo(req)
if !ok || info.MaxContextLength != 0 || info.IsCompat != tc.compat || info.ContextLength != tc.contextLength || info.UserDefined != wire.UserDefined {
t.Fatalf("resolved capabilities=%+v", info)
}
if tc.compat && (info.Thinking == nil || !info.Thinking.ZeroAllowed || len(info.Thinking.Levels) != 2) {
t.Fatalf("thinking=%+v", info.Thinking)
}
})
}
}
func TestHomeCodexCompatUsesSelectedCredentialOptions(t *testing.T) {
cfg := &internalconfig.Config{}
cfg.Home.Enabled = true
auth := &Auth{Provider: "codex", Metadata: map[string]any{"credential_options": map[string]any{"models": []any{map[string]any{"name": "upstream", "alias": "alias", "is-compat": true}}}}}
for _, model := range []string{"upstream", "alias", "upstream(high)"} {
if !CodexAPIKeyModelIsCompat(cfg, auth, model) {
t.Errorf("Home model %s lost compat flag", model)
}
}
if CodexAPIKeyModelIsCompat(cfg, &Auth{Provider: "codex"}, "upstream") {
t.Fatal("compat flag leaked to another credential")
}
cfg.Home.Enabled = false
if CodexAPIKeyModelIsCompat(cfg, auth, "upstream") {
t.Fatal("standalone began consuming Home options")
}
}
func TestHomeCompatUsesCredentialModelOptions(t *testing.T) {
for _, tc := range []struct {
name, options, upstream, localModel, route string
want bool
}{
{name: "explicit true", options: `{"models":[{"name":"model","is-compat":true}]}`, want: true},
{name: "explicit false", options: `{"models":[{"name":"model","is-compat":false}]}`},
{name: "omitted flag defaults false", options: `{"models":[{"name":"model"}]}`},
{name: "null models", options: `{"models":null}`},
{name: "empty models", options: `{"models":[]}`},
{name: "unlisted model", options: `{"models":[{"name":"other","is-compat":true}]}`},
{name: "missing options default false"},
{name: "unrelated options default false", options: `{"weight":2}`},
{name: "legacy different model", localModel: "other"},
{name: "invalid models default false", options: `{"models":"invalid"}`},
{name: "upstream wins over another alias", options: `{"models":[{"name":"other","alias":"model","is-compat":true},{"name":"model","is-compat":false}]}`},
{name: "alias fallback", options: `{"models":[{"name":"upstream","alias":"model","is-compat":true}]}`, want: true},
{name: "suffix fallback", options: `{"models":[{"name":"model","is-compat":true}]}`, upstream: "model(high)", want: true},
{name: "exact dispatched suffix", options: `{"models":[{"name":"model","is-compat":true},{"name":"model(high)","is-compat":false}]}`, upstream: "model(high)"},
{name: "same upstream with different aliases", options: `{"models":[{"name":"model","alias":"other","is-compat":false},{"name":"model","alias":"chosen","is-compat":true}]}`, upstream: "model(high)", route: "tenant/chosen(high)", want: true},
{name: "same alias with different upstreams", options: `{"models":[{"name":"other","alias":"chosen","is-compat":false},{"name":"model","alias":"chosen","is-compat":true}]}`, route: "tenant/chosen", want: true},
} {
t.Run(tc.name, func(t *testing.T) {
auth := &Auth{Prefix: "tenant", Attributes: map[string]string{homeUpstreamModelAttributeKey: tc.upstream}}
if tc.options != "" {
auth.Metadata = map[string]any{"credential_options": json.RawMessage(tc.options)}
}
localModel := tc.localModel
if localModel == "" {
localModel = "model"
}
local := &registry.ModelInfo{ID: localModel, IsCompat: true}
homeInfo := &registry.ModelInfo{ID: "model"}
req := cliproxyexecutor.Request{Model: "alias", Metadata: map[string]any{resolvedAPIKeyModelInfoMetadataKey: local}}
req = attachResolvedHomeModelInfo(req, auth, tc.route, homeInfo, nil)
info, ok := ResolvedModelInfo(req)
if !ok || info.IsCompat != tc.want {
t.Fatalf("resolved %+v, want compat=%v", info, tc.want)
}
if !local.IsCompat || homeInfo.IsCompat {
t.Fatal("resolution mutated the source model definitions")
}
})
}
}
func TestHomeCompatCredentialOptionsExecutionPaths(t *testing.T) {
for _, provider := range []string{"codex", "claude", "gemini", "custom-compat"} {
for _, path := range []string{"execute", "stream", "count"} {
t.Run(provider+"/"+path, func(t *testing.T) {
manager := NewManager(nil, nil, nil)
manager.SetConfig(&internalconfig.Config{Home: internalconfig.HomeConfig{Enabled: true}})
dispatcher := &configurationUpdateHomeDispatcher{}
manager.PublishHomeDispatch(dispatcher, executionregistry.New(), 1)
executor := &selectedCapabilityCaptureExecutor{provider: provider}
manager.RegisterExecutor(executor)
auth := configuredCapabilityTestAuth("home-compat-"+provider, "fixture-key")
auth.Provider = provider
for _, enabled := range []bool{true, false} {
auth.Metadata = map[string]any{"credential_options": map[string]any{"models": []internalconfig.CodexModel{
{Name: "upstream", Alias: "other", IsCompat: !enabled},
{Name: "upstream", Alias: "alias", IsCompat: enabled},
}}}
payload, errMarshal := json.Marshal(homeAuthDispatchResponse{
Model: "upstream(high)", Auth: *auth, ModelInfo: &homeDispatchModelInfo{ID: "upstream"},
})
if errMarshal != nil {
t.Fatal(errMarshal)
}
dispatcher.payload = payload
req := cliproxyexecutor.Request{Model: "tenant/alias(high)"}
opts := cliproxyexecutor.Options{}
switch path {
case "execute":
if _, errExecute := manager.Execute(t.Context(), []string{provider}, req, opts); errExecute != nil {
t.Fatal(errExecute)
}
case "stream":
stream, errStream := manager.ExecuteStream(t.Context(), []string{provider}, req, opts)
if errStream != nil {
t.Fatal(errStream)
}
for chunk := range stream.Chunks {
if chunk.Err != nil {
t.Fatal(chunk.Err)
}
}
case "count":
if _, errCount := manager.ExecuteCount(t.Context(), []string{provider}, req, opts); errCount != nil {
t.Fatal(errCount)
}
}
if len(executor.requests) == 0 {
t.Fatal("executor received no request")
}
info, ok := ResolvedModelInfo(executor.requests[len(executor.requests)-1])
if !ok || info.IsCompat != enabled {
t.Fatalf("resolved capabilities=%+v, want compat=%v", info, enabled)
}
}
})
}
}
}
func TestHomeCompatDoesNotInheritLocalModelExecutionPaths(t *testing.T) {
for _, tc := range []struct {
name, options string
}{
{name: "missing options"},
{name: "unrelated options", options: `{"weight":2}`},
{name: "invalid models", options: `{"models":"invalid"}`},
} {
for _, path := range []string{"execute", "stream", "count"} {
t.Run(tc.name+"/"+path, func(t *testing.T) {
manager := NewManager(nil, nil, nil)
manager.SetConfig(&internalconfig.Config{
Home: internalconfig.HomeConfig{Enabled: true},
CodexKey: []internalconfig.CodexKey{{APIKey: "fixture-key", Prefix: "tenant", Models: []internalconfig.CodexModel{
{Name: "upstream", Alias: "alias", IsCompat: true},
}}},
})
auth := configuredCapabilityTestAuth("home-no-local-compat", "fixture-key")
auth.Provider = "codex"
auth.Attributes[AttributeSource] = "config:codex[0]"
if tc.options != "" {
auth.Metadata = map[string]any{"credential_options": json.RawMessage(tc.options)}
}
payload, errMarshal := json.Marshal(homeAuthDispatchResponse{
Model: "upstream(high)", Auth: *auth, ModelInfo: &homeDispatchModelInfo{ID: "upstream"},
})
if errMarshal != nil {
t.Fatal(errMarshal)
}
manager.PublishHomeDispatch(configurationUpdateHomeDispatcher{payload: payload}, executionregistry.New(), 1)
executor := &selectedCapabilityCaptureExecutor{provider: "codex"}
manager.RegisterExecutor(executor)
req := cliproxyexecutor.Request{Model: "tenant/alias(high)"}
opts := cliproxyexecutor.Options{}
switch path {
case "execute":
if _, errExecute := manager.Execute(t.Context(), []string{"codex"}, req, opts); errExecute != nil {
t.Fatal(errExecute)
}
case "stream":
stream, errStream := manager.ExecuteStream(t.Context(), []string{"codex"}, req, opts)
if errStream != nil {
t.Fatal(errStream)
}
for chunk := range stream.Chunks {
if chunk.Err != nil {
t.Fatal(chunk.Err)
}
}
case "count":
if _, errCount := manager.ExecuteCount(t.Context(), []string{"codex"}, req, opts); errCount != nil {
t.Fatal(errCount)
}
}
if len(executor.requests) != 1 {
t.Fatalf("executor received %d requests, want 1", len(executor.requests))
}
received := executor.requests[0]
local, okLocal := ResolvedAPIKeyModelInfo(received)
if !okLocal || !local.IsCompat {
t.Fatal("fixture did not bind the conflicting local compatibility setting")
}
info, ok := ResolvedModelInfo(received)
if !ok || info.IsCompat {
t.Fatalf("Home capabilities must not inherit local is-compat: %+v", info)
}
})
}
}
}

View File

@@ -21,7 +21,7 @@ func TestHomeDispatchWebSearchCapability(t *testing.T) {
if err := json.Unmarshal([]byte(tc.raw), &wire); err != nil {
t.Fatal(err)
}
req := attachResolvedHomeModelInfo(cliproxyexecutor.Request{Model: "alias"}, wire.registryModelInfo())
req := attachResolvedHomeModelInfo(cliproxyexecutor.Request{Model: "alias"}, nil, "alias", wire.registryModelInfo(), wire.SupportConfigurationUpdate)
info, ok := ResolvedModelInfo(req)
if !ok || info.ID != "upstream" {
t.Fatalf("missing resolved model: %+v", info)

61
sdk/pluginhost/quota.go Normal file
View File

@@ -0,0 +1,61 @@
package pluginhost
import (
"context"
internalpluginhost "github.com/router-for-me/CLIProxyAPI/v8/internal/pluginhost"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginapi"
)
// RegisteredQuotaProviderInfo describes quota capabilities exposed to embedders.
type RegisteredQuotaProviderInfo = internalpluginhost.RegisteredQuotaProviderInfo
// QuotaProviders lists active quota providers using the request context.
func (h *Host) QuotaProviders(ctx context.Context) []RegisteredQuotaProviderInfo {
if h == nil || h.inner == nil {
return nil
}
return h.inner.QuotaProviders(ctx)
}
// HasQuotaProviderContext reports whether a provider handles credential quotas.
func (h *Host) HasQuotaProviderContext(ctx context.Context, provider string) bool {
return h != nil && h.inner != nil && h.inner.HasQuotaProviderContext(ctx, provider)
}
// HasQuotaProviderForPlugin reports whether a plugin exposes quota operations.
func (h *Host) HasQuotaProviderForPlugin(pluginID string) bool {
return h != nil && h.inner != nil && h.inner.HasQuotaProviderForPlugin(pluginID)
}
// FetchQuota invokes a quota provider through the public embedding API.
func (h *Host) FetchQuota(ctx context.Context, req pluginapi.QuotaFetchRequest) (pluginapi.QuotaFetchResponse, bool, error) {
if h == nil || h.inner == nil {
return pluginapi.QuotaFetchResponse{}, false, nil
}
return h.inner.FetchQuota(ctx, req)
}
// FetchQuotaByPlugin invokes a quota provider through the public embedding API.
func (h *Host) FetchQuotaByPlugin(ctx context.Context, pluginID string, req pluginapi.QuotaFetchRequest) (pluginapi.QuotaFetchResponse, bool, error) {
if h == nil || h.inner == nil {
return pluginapi.QuotaFetchResponse{}, false, nil
}
return h.inner.FetchQuotaByPlugin(ctx, pluginID, req)
}
// ResetQuota invokes a quota provider through the public embedding API.
func (h *Host) ResetQuota(ctx context.Context, req pluginapi.QuotaResetRequest) (pluginapi.QuotaResetResponse, bool, error) {
if h == nil || h.inner == nil {
return pluginapi.QuotaResetResponse{}, false, nil
}
return h.inner.ResetQuota(ctx, req)
}
// ResetQuotaByPlugin invokes a quota provider through the public embedding API.
func (h *Host) ResetQuotaByPlugin(ctx context.Context, pluginID string, req pluginapi.QuotaResetRequest) (pluginapi.QuotaResetResponse, bool, error) {
if h == nil || h.inner == nil {
return pluginapi.QuotaResetResponse{}, false, nil
}
return h.inner.ResetQuotaByPlugin(ctx, pluginID, req)
}

View File

@@ -0,0 +1,60 @@
package pluginhost
import (
"context"
"testing"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginapi"
)
type quotaSDKFixture struct{ t *testing.T }
func (p quotaSDKFixture) Identifier() string { return "fixture" }
func (p quotaSDKFixture) DescribeQuota(context.Context, pluginapi.QuotaDescribeRequest) (pluginapi.QuotaDescribeResponse, error) {
return pluginapi.QuotaDescribeResponse{SupportedProviders: []string{"fixture"}, SupportsReset: true}, nil
}
func (p quotaSDKFixture) FetchQuota(_ context.Context, req pluginapi.QuotaFetchRequest) (pluginapi.QuotaFetchResponse, error) {
if req.AuthID != "credential" || req.Metadata["fixture"] != "value" {
p.t.Fatal("SDK dropped credential context")
}
return pluginapi.QuotaFetchResponse{Subscription: &pluginapi.QuotaSubscription{Plan: "fixture-plan"}}, nil
}
func (p quotaSDKFixture) ResetQuota(_ context.Context, req pluginapi.QuotaResetRequest) (pluginapi.QuotaResetResponse, error) {
if req.AuthID != "credential" {
p.t.Fatal("SDK dropped reset credential")
}
return pluginapi.QuotaResetResponse{Success: true}, nil
}
func TestQuotaSDKEmbedding(t *testing.T) {
host := New()
host.inner.RegisterPluginForTest("fixture-plugin", pluginapi.Plugin{Capabilities: pluginapi.Capabilities{QuotaProvider: quotaSDKFixture{t}}})
ctx := context.Background()
if len(host.QuotaProviders(ctx)) != 1 || !host.HasQuotaProviderContext(ctx, "fixture") || !host.HasQuotaProviderForPlugin("fixture-plugin") {
t.Fatal("missing public quota capabilities")
}
for _, byPlugin := range []bool{false, true} {
req := pluginapi.QuotaFetchRequest{Provider: "fixture", AuthID: "credential", Metadata: map[string]any{"fixture": "value"}}
var result pluginapi.QuotaFetchResponse
var handled bool
var err error
if byPlugin {
result, handled, err = host.FetchQuotaByPlugin(ctx, "fixture-plugin", req)
} else {
result, handled, err = host.FetchQuota(ctx, req)
}
if err != nil || !handled || result.Subscription == nil || result.Subscription.Plan != "fixture-plan" {
t.Fatalf("fetch: handled=%v err=%v", handled, err)
}
reset := pluginapi.QuotaResetRequest{Provider: "fixture", AuthID: "credential"}
var resetResult pluginapi.QuotaResetResponse
if byPlugin {
resetResult, handled, err = host.ResetQuotaByPlugin(ctx, "fixture-plugin", reset)
} else {
resetResult, handled, err = host.ResetQuota(ctx, reset)
}
if err != nil || !handled || !resetResult.Success {
t.Fatalf("reset: handled=%v err=%v", handled, err)
}
}
}