- 使用 SQLite 保存关联后的请求生命周期和用量记录 - 支持长上下文阶梯价格和可配置的 Fast 计费倍率 - 添加管理接口和可配置列的用量面板 - 保留失败、取消、重试和 compact 请求,便于计费核对
137 lines
4.0 KiB
Go
137 lines
4.0 KiB
Go
package plugin
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
func lifecycleRequest(t *testing.T, schema uint32, config string) []byte {
|
|
t.Helper()
|
|
raw, err := json.Marshal(LifecycleRequest{ConfigYAML: []byte(config), SchemaVersion: schema})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return raw
|
|
}
|
|
|
|
func testConfig(t *testing.T, config string) string {
|
|
t.Helper()
|
|
return fmt.Sprintf("database_path: %q\n%s", filepath.Join(t.TempDir(), "usage.db"), config)
|
|
}
|
|
|
|
func TestRegisterNegotiatesSchemaAndDeclaresOnlyUsage(t *testing.T) {
|
|
app := NewApp()
|
|
raw, err := app.HandleMethod(MethodPluginRegister, lifecycleRequest(t, 99, testConfig(t, "enabled: true\ncodex_only: true\n")))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var env Envelope
|
|
if err := json.Unmarshal(raw, &env); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !env.OK {
|
|
t.Fatalf("register failed: %s", raw)
|
|
}
|
|
var got Registration
|
|
if err := json.Unmarshal(env.Result, &got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.SchemaVersion != SchemaVersion || !got.Capabilities.RequestLifecyclePlugin || !got.Capabilities.UsagePlugin || !got.Capabilities.ManagementAPI {
|
|
t.Fatalf("unexpected registration: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestLifecycleConfigYAMLUsesBase64WireEncoding(t *testing.T) {
|
|
raw := lifecycleRequest(t, SchemaVersion, "enabled: true\n")
|
|
if !json.Valid(raw) {
|
|
t.Fatal("invalid JSON")
|
|
}
|
|
var wire map[string]any
|
|
_ = json.Unmarshal(raw, &wire)
|
|
want := base64.StdEncoding.EncodeToString([]byte("enabled: true\n"))
|
|
if wire["config_yaml"] != want {
|
|
t.Fatalf("config_yaml = %v, want %q", wire["config_yaml"], want)
|
|
}
|
|
}
|
|
|
|
func TestReconfigureIsAtomicAndFiltersUsage(t *testing.T) {
|
|
app := NewApp()
|
|
if _, err := app.HandleMethod(MethodPluginRegister, lifecycleRequest(t, SchemaVersion, testConfig(t, "enabled: true\ncodex_only: true\n"))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, record := range []UsageRecord{{Provider: "codex", Model: "gpt-5.5"}, {Provider: "gemini", Model: "gemini-pro"}} {
|
|
raw, _ := json.Marshal(record)
|
|
if _, err := app.HandleMethod(MethodUsageHandle, raw); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if got := app.Seen(); got != 1 {
|
|
t.Fatalf("seen = %d, want 1", got)
|
|
}
|
|
if _, err := app.HandleMethod(MethodPluginReconfigure, lifecycleRequest(t, SchemaVersion, "enabled: [invalid")); err == nil {
|
|
t.Fatal("invalid reconfiguration unexpectedly succeeded")
|
|
}
|
|
raw, _ := json.Marshal(UsageRecord{Provider: "codex", Model: "gpt-5.5"})
|
|
_, _ = app.HandleMethod(MethodUsageHandle, raw)
|
|
if got := app.Seen(); got != 2 {
|
|
t.Fatalf("last valid config was not retained: seen = %d", got)
|
|
}
|
|
}
|
|
|
|
func TestConcurrentUsage(t *testing.T) {
|
|
app := NewApp()
|
|
if _, err := app.HandleMethod(MethodPluginRegister, lifecycleRequest(t, SchemaVersion, testConfig(t, "enabled: true\ncodex_only: true\n"))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, _ := json.Marshal(UsageRecord{Provider: "codex", Model: "gpt-5.5"})
|
|
var wg sync.WaitGroup
|
|
for range 100 {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
if _, err := app.HandleMethod(MethodUsageHandle, raw); err != nil {
|
|
t.Errorf("handle usage: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
if got := app.Seen(); got != 100 {
|
|
t.Fatalf("seen = %d, want 100", got)
|
|
}
|
|
}
|
|
|
|
func TestUnknownMethodReturnsErrorEnvelope(t *testing.T) {
|
|
raw, err := NewApp().HandleMethod("missing", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var env Envelope
|
|
if err := json.Unmarshal(raw, &env); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if env.OK || env.Error == nil || env.Error.Code != "unknown_method" {
|
|
t.Fatalf("unexpected envelope: %s", raw)
|
|
}
|
|
}
|
|
|
|
func TestUsageRequestTypeMatchesKeeperEndpointSemantics(t *testing.T) {
|
|
tests := []struct {
|
|
endpoint string
|
|
want string
|
|
}{
|
|
{endpoint: "POST /v1/responses", want: "SSE"},
|
|
{endpoint: "POST /v1/chat/completions", want: "SSE"},
|
|
{endpoint: "GET /v1/responses", want: "WS"},
|
|
{endpoint: "/v1/responses", want: ""},
|
|
}
|
|
for _, test := range tests {
|
|
if got := usageRequestType(test.endpoint); got != test.want {
|
|
t.Fatalf("usageRequestType(%q) = %q, want %q", test.endpoint, got, test.want)
|
|
}
|
|
}
|
|
}
|