diff --git a/examples/plugin/README.md b/examples/plugin/README.md index e763a20ce..7dcb28ac0 100644 --- a/examples/plugin/README.md +++ b/examples/plugin/README.md @@ -12,6 +12,7 @@ This directory contains standard dynamic library plugin examples for the CLIProx - `protocol-format/`: minimal executor focused on input/output format declarations. - `request-translator/`: request translation capability only. - `request-normalizer/`: request normalization capability only. +- `codex-service-tier/`: Go-only request normalizer that sets Codex `gpt-5.4` requests to the priority service tier when enabled. - `response-translator/`: response translation capability only. - `response-normalizer/`: response normalization capability only. - `thinking/`: thinking applier capability only. @@ -20,7 +21,20 @@ This directory contains standard dynamic library plugin examples for the CLIProx - `management-api/`: Management API capability only. - `host-callback/`: minimal Management API route that demonstrates host callbacks. -Each example directory contains `go/`, `c/`, and `rust/` subdirectories. +Most standard capability examples contain `go/`, `c/`, and `rust/` subdirectories. Specialized examples may provide only the implementation language they need. + +## Codex Service Tier + +`codex-service-tier` declares the request normalization capability. When `fast` is `true`, it sets `service_tier` to `priority` for requests where `req.ToFormat` is `codex` and `req.Model` is `gpt-5.4`. + +```yaml +plugins: + configs: + codex-service-tier: + enabled: true + priority: 1 + fast: false +``` ## Build All Examples diff --git a/examples/plugin/README_CN.md b/examples/plugin/README_CN.md index fc8605590..9841b8bc2 100644 --- a/examples/plugin/README_CN.md +++ b/examples/plugin/README_CN.md @@ -12,6 +12,7 @@ - `protocol-format/`:使用最小执行器重点演示输入和输出格式声明。 - `request-translator/`:只演示请求转换能力。 - `request-normalizer/`:只演示请求规整能力。 +- `codex-service-tier/`:仅 Go 实现的请求规整插件,启用后会将 Codex `gpt-5.4` 请求设置为 priority service tier。 - `response-translator/`:只演示响应转换能力。 - `response-normalizer/`:只演示响应规整能力。 - `thinking/`:只演示 Thinking 处理能力。 @@ -20,7 +21,20 @@ - `management-api/`:只演示 Management API 扩展能力。 - `host-callback/`:使用最小 Management API 路由演示宿主回调。 -每个示例目录都包含 `go/`、`c/` 和 `rust/` 三个子目录。 +多数标准能力示例都包含 `go/`、`c/` 和 `rust/` 三个子目录。专用示例可能只提供所需的实现语言。 + +## Codex Service Tier + +`codex-service-tier` 声明请求规整能力。当 `fast` 为 `true` 时,如果 `req.ToFormat` 为 `codex` 且 `req.Model` 为 `gpt-5.4`,它会将 `service_tier` 设置为 `priority`。 + +```yaml +plugins: + configs: + codex-service-tier: + enabled: true + priority: 1 + fast: false +``` ## 构建全部示例 diff --git a/examples/plugin/codex-service-tier/README.md b/examples/plugin/codex-service-tier/README.md new file mode 100644 index 000000000..3c1bcddfb --- /dev/null +++ b/examples/plugin/codex-service-tier/README.md @@ -0,0 +1,25 @@ +# Codex Service Tier Plugin + +This plugin is a request normalizer for Codex outbound requests. + +When the plugin is enabled and `fast` is set to `true`, it sets the top-level `service_tier` field to `priority` for requests where: + +- `req.ToFormat` is `codex` +- `req.Model` is `gpt-5.5` + +Requests that do not match these conditions are returned unchanged. + +## Configuration + +Add the plugin under `plugins.configs`: + +```yaml +plugins: + configs: + codex-service-tier: + enabled: true + priority: 1 + fast: false +``` + +`fast` is a boolean field. Set it to `true` to enable priority service tier shaping for matching Codex `gpt-5.5` requests. diff --git a/examples/plugin/codex-service-tier/go/go.mod b/examples/plugin/codex-service-tier/go/go.mod new file mode 100644 index 000000000..599588ee1 --- /dev/null +++ b/examples/plugin/codex-service-tier/go/go.mod @@ -0,0 +1,17 @@ +module github.com/router-for-me/CLIProxyAPI/v7/examples/plugin/codex-service-tier/go + +go 1.26.0 + +require ( + github.com/router-for-me/CLIProxyAPI/v7 v7.0.0 + github.com/tidwall/sjson v1.2.5 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect +) + +replace github.com/router-for-me/CLIProxyAPI/v7 => ../../../.. diff --git a/examples/plugin/codex-service-tier/go/go.sum b/examples/plugin/codex-service-tier/go/go.sum new file mode 100644 index 000000000..9186dfd80 --- /dev/null +++ b/examples/plugin/codex-service-tier/go/go.sum @@ -0,0 +1,13 @@ +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/plugin/codex-service-tier/go/main.go b/examples/plugin/codex-service-tier/go/main.go new file mode 100644 index 000000000..09726d165 --- /dev/null +++ b/examples/plugin/codex-service-tier/go/main.go @@ -0,0 +1,246 @@ +package main + +/* +#include +#include + +typedef struct { + void* ptr; + size_t len; +} cliproxy_buffer; + +typedef struct { + uint32_t abi_version; + void* host_ctx; + void* call; + void* free_buffer; +} cliproxy_host_api; + +typedef int (*cliproxy_plugin_call_fn)(char*, uint8_t*, size_t, cliproxy_buffer*); +typedef void (*cliproxy_plugin_free_fn)(void*, size_t); +typedef void (*cliproxy_plugin_shutdown_fn)(void); + +typedef struct { + uint32_t abi_version; + cliproxy_plugin_call_fn call; + cliproxy_plugin_free_fn free_buffer; + cliproxy_plugin_shutdown_fn shutdown; +} cliproxy_plugin_api; + +extern int cliproxyPluginCall(char*, uint8_t*, size_t, cliproxy_buffer*); +extern void cliproxyPluginFree(void*, size_t); +extern void cliproxyPluginShutdown(void); +*/ +import "C" + +import ( + "encoding/json" + "strings" + "sync/atomic" + "unsafe" + + "github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginabi" + "github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi" + "github.com/tidwall/sjson" + "gopkg.in/yaml.v3" +) + +var fastEnabled atomic.Bool + +type envelope struct { + OK bool `json:"ok"` + Result json.RawMessage `json:"result,omitempty"` + Error *envelopeError `json:"error,omitempty"` +} + +type envelopeError struct { + Code string `json:"code"` + Message string `json:"message"` +} + +type lifecycleRequest struct { + ConfigYAML []byte `json:"config_yaml"` +} + +type pluginConfig struct { + Fast bool `yaml:"fast"` +} + +type registration struct { + SchemaVersion uint32 `json:"schema_version"` + Metadata pluginapi.Metadata `json:"metadata"` + Capabilities registrationCapability `json:"capabilities"` +} + +type registrationCapability struct { + RequestNormalizer bool `json:"request_normalizer"` +} + +func main() {} + +//export cliproxy_plugin_init +func cliproxy_plugin_init(_ *C.cliproxy_host_api, plugin *C.cliproxy_plugin_api) C.int { + if plugin == nil { + return 1 + } + plugin.abi_version = C.uint32_t(pluginabi.ABIVersion) + plugin.call = C.cliproxy_plugin_call_fn(C.cliproxyPluginCall) + plugin.free_buffer = C.cliproxy_plugin_free_fn(C.cliproxyPluginFree) + plugin.shutdown = C.cliproxy_plugin_shutdown_fn(C.cliproxyPluginShutdown) + return 0 +} + +//export cliproxyPluginCall +func cliproxyPluginCall(method *C.char, request *C.uint8_t, requestLen C.size_t, response *C.cliproxy_buffer) C.int { + if response != nil { + response.ptr = nil + response.len = 0 + } + if method == nil { + writeResponse(response, errorEnvelope("invalid_method", "method is required")) + return 1 + } + var requestBytes []byte + if request != nil && requestLen > 0 { + requestBytes = C.GoBytes(unsafe.Pointer(request), C.int(requestLen)) + } + raw, errHandle := handleMethod(C.GoString(method), requestBytes) + if errHandle != nil { + writeResponse(response, errorEnvelope("plugin_error", errHandle.Error())) + return 1 + } + writeResponse(response, raw) + return 0 +} + +//export cliproxyPluginFree +func cliproxyPluginFree(ptr unsafe.Pointer, len C.size_t) { + if ptr != nil { + C.free(ptr) + } +} + +//export cliproxyPluginShutdown +func cliproxyPluginShutdown() {} + +func handleMethod(method string, request []byte) ([]byte, error) { + switch method { + case pluginabi.MethodPluginRegister, pluginabi.MethodPluginReconfigure: + if errConfigure := configure(request); errConfigure != nil { + return nil, errConfigure + } + return okEnvelope(pluginRegistration()) + case pluginabi.MethodRequestNormalize: + return normalizeRequest(request) + default: + return errorEnvelope("unknown_method", "unknown method: "+method), nil + } +} + +func configure(raw []byte) error { + var req lifecycleRequest + if len(raw) > 0 { + if errUnmarshal := json.Unmarshal(raw, &req); errUnmarshal != nil { + return errUnmarshal + } + } + + cfg := pluginConfig{} + if len(req.ConfigYAML) > 0 { + fast, errDecodeFast := decodeFastConfig(req.ConfigYAML) + if errDecodeFast != nil { + return errDecodeFast + } + cfg.Fast = fast + } + fastEnabled.Store(cfg.Fast) + return nil +} + +func pluginRegistration() registration { + return registration{ + SchemaVersion: pluginabi.SchemaVersion, + Metadata: pluginapi.Metadata{ + Name: "codex-service-tier", + Version: "0.1.0", + Author: "router-for-me", + GitHubRepository: "https://github.com/router-for-me/CLIProxyAPI", + Logo: "https://raw.githubusercontent.com/router-for-me/CLIProxyAPI/main/docs/logo.png", + ConfigFields: []pluginapi.ConfigField{{ + Name: "fast", + Type: pluginapi.ConfigFieldTypeBoolean, + Description: "Sets Codex gpt-5.5 Responses requests to the priority service tier.", + }}, + }, + Capabilities: registrationCapability{ + RequestNormalizer: true, + }, + } +} + +func normalizeRequest(raw []byte) ([]byte, error) { + var req pluginapi.RequestTransformRequest + if errUnmarshal := json.Unmarshal(raw, &req); errUnmarshal != nil { + return nil, errUnmarshal + } + body := req.Body + if !shouldSetPriorityServiceTier(req) { + return okEnvelope(pluginapi.PayloadResponse{Body: body}) + } + updated, okSet := setPriorityServiceTier(body) + if !okSet { + return okEnvelope(pluginapi.PayloadResponse{Body: body}) + } + return okEnvelope(pluginapi.PayloadResponse{Body: updated}) +} + +func shouldSetPriorityServiceTier(req pluginapi.RequestTransformRequest) bool { + if !fastEnabled.Load() { + return false + } + if !strings.EqualFold(req.ToFormat, "codex") { + return false + } + return req.Model == "gpt-5.5" +} + +func decodeFastConfig(configYAML []byte) (bool, error) { + var cfg pluginConfig + if errUnmarshal := yaml.Unmarshal(configYAML, &cfg); errUnmarshal != nil { + return false, errUnmarshal + } + return cfg.Fast, nil +} + +func setPriorityServiceTier(body []byte) ([]byte, bool) { + updated, errSet := sjson.SetBytes(body, "service_tier", "priority") + if errSet != nil { + return nil, false + } + return updated, true +} + +func okEnvelope(v any) ([]byte, error) { + raw, errMarshal := json.Marshal(v) + if errMarshal != nil { + return nil, errMarshal + } + return json.Marshal(envelope{OK: true, Result: raw}) +} + +func errorEnvelope(code, message string) []byte { + raw, _ := json.Marshal(envelope{OK: false, Error: &envelopeError{Code: code, Message: message}}) + return raw +} + +func writeResponse(response *C.cliproxy_buffer, raw []byte) { + if response == nil || len(raw) == 0 { + return + } + ptr := C.CBytes(raw) + if ptr == nil { + return + } + response.ptr = ptr + response.len = C.size_t(len(raw)) +}