mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
Merge pull request #3948 from superman2003/fix/gpt56-billing-usage-integrity
fix(openai): correct GPT-5.6 billing and usage gaps
This commit is contained in:
@@ -53,6 +53,26 @@ func TestUsageConversionsPreserveCacheWriteTokens(t *testing.T) {
|
||||
require.Equal(t, 200, roundTrip.InputTokensDetails.CacheWriteTokens)
|
||||
}
|
||||
|
||||
func TestResponsesUsageNestedCacheWritePresenceOverridesTopLevelAlias(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
nestedJSON string
|
||||
want int
|
||||
}{
|
||||
{name: "explicit zero", nestedJSON: `{"cache_write_tokens":0}`, want: 0},
|
||||
{name: "nonzero", nestedJSON: `{"cache_write_tokens":7}`, want: 7},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var usage ResponsesUsage
|
||||
payload := []byte(`{"input_tokens":20,"output_tokens":2,"cache_creation_input_tokens":19,"input_tokens_details":` + tt.nestedJSON + `}`)
|
||||
require.NoError(t, json.Unmarshal(payload, &usage))
|
||||
require.Equal(t, tt.want, usage.CacheCreationInputTokens)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsToResponses_SystemMessage(t *testing.T) {
|
||||
req := &ChatCompletionsRequest{
|
||||
Model: "gpt-4o",
|
||||
|
||||
@@ -332,6 +332,10 @@ type ResponsesUsage struct {
|
||||
|
||||
func (u *ResponsesUsage) UnmarshalJSON(data []byte) error {
|
||||
type responsesUsageAlias ResponsesUsage
|
||||
type cacheTokenPresence struct {
|
||||
CacheCreationTokens *int `json:"cache_creation_tokens"`
|
||||
CacheWriteTokens *int `json:"cache_write_tokens"`
|
||||
}
|
||||
var aux struct {
|
||||
responsesUsageAlias
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
@@ -345,6 +349,13 @@ func (u *ResponsesUsage) UnmarshalJSON(data []byte) error {
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
var nestedPresence struct {
|
||||
InputTokensDetails *cacheTokenPresence `json:"input_tokens_details"`
|
||||
PromptTokensDetails *cacheTokenPresence `json:"prompt_tokens_details"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &nestedPresence); err != nil {
|
||||
return err
|
||||
}
|
||||
*u = ResponsesUsage(aux.responsesUsageAlias)
|
||||
if u.InputTokens == 0 && aux.PromptTokens != 0 {
|
||||
u.InputTokens = aux.PromptTokens
|
||||
@@ -368,6 +379,20 @@ func (u *ResponsesUsage) UnmarshalJSON(data []byte) error {
|
||||
if u.OutputTokensDetails == nil && aux.CompletionTokensDetails != nil {
|
||||
u.OutputTokensDetails = aux.CompletionTokensDetails
|
||||
}
|
||||
var canonicalCacheCreationTokens *int
|
||||
switch {
|
||||
case nestedPresence.InputTokensDetails != nil && nestedPresence.InputTokensDetails.CacheWriteTokens != nil:
|
||||
canonicalCacheCreationTokens = nestedPresence.InputTokensDetails.CacheWriteTokens
|
||||
case nestedPresence.PromptTokensDetails != nil && nestedPresence.PromptTokensDetails.CacheWriteTokens != nil:
|
||||
canonicalCacheCreationTokens = nestedPresence.PromptTokensDetails.CacheWriteTokens
|
||||
case nestedPresence.InputTokensDetails != nil && nestedPresence.InputTokensDetails.CacheCreationTokens != nil:
|
||||
canonicalCacheCreationTokens = nestedPresence.InputTokensDetails.CacheCreationTokens
|
||||
case nestedPresence.PromptTokensDetails != nil && nestedPresence.PromptTokensDetails.CacheCreationTokens != nil:
|
||||
canonicalCacheCreationTokens = nestedPresence.PromptTokensDetails.CacheCreationTokens
|
||||
}
|
||||
if canonicalCacheCreationTokens != nil {
|
||||
u.CacheCreationInputTokens = max(*canonicalCacheCreationTokens, 0)
|
||||
}
|
||||
if u.TotalTokens == 0 && (u.InputTokens != 0 || u.OutputTokens != 0) {
|
||||
u.TotalTokens = u.InputTokens + u.OutputTokens
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ type Model struct {
|
||||
|
||||
// DefaultModels OpenAI models list
|
||||
var DefaultModels = []Model{
|
||||
{ID: "gpt-5.6", Object: "model", Created: 1780876800, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.6 (Sol)"},
|
||||
{ID: "gpt-5.6-sol", Object: "model", Created: 1780876800, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.6 Sol"},
|
||||
{ID: "gpt-5.6-terra", Object: "model", Created: 1780876800, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.6 Terra"},
|
||||
{ID: "gpt-5.6-luna", Object: "model", Created: 1780876800, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.6 Luna"},
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package openai
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDefaultModelsIncludeBareGPT56Alias(t *testing.T) {
|
||||
require.Contains(t, DefaultModelIDs(), "gpt-5.6")
|
||||
}
|
||||
@@ -199,16 +199,24 @@ func appendRequestTypeOrStreamQueryFilter(query string, args []any, requestType
|
||||
|
||||
// buildRequestTypeFilterCondition 在 request_type 过滤时兼容 legacy 字段,避免历史数据漏查。
|
||||
func buildRequestTypeFilterCondition(startArgIndex int, requestType int16) (string, []any) {
|
||||
return buildRequestTypeFilterConditionWithAlias(startArgIndex, requestType, "")
|
||||
}
|
||||
|
||||
func buildRequestTypeFilterConditionWithAlias(startArgIndex int, requestType int16, alias string) (string, []any) {
|
||||
normalized := service.RequestTypeFromInt16(requestType)
|
||||
requestTypeArg := int16(normalized)
|
||||
prefix := ""
|
||||
if alias != "" {
|
||||
prefix = alias + "."
|
||||
}
|
||||
switch normalized {
|
||||
case service.RequestTypeSync:
|
||||
return fmt.Sprintf("(request_type = $%d OR (request_type = %d AND stream = FALSE AND openai_ws_mode = FALSE))", startArgIndex, int16(service.RequestTypeUnknown)), []any{requestTypeArg}
|
||||
return fmt.Sprintf("(%srequest_type = $%d OR (%srequest_type = %d AND %sstream = FALSE AND %sopenai_ws_mode = FALSE))", prefix, startArgIndex, prefix, int16(service.RequestTypeUnknown), prefix, prefix), []any{requestTypeArg}
|
||||
case service.RequestTypeStream:
|
||||
return fmt.Sprintf("(request_type = $%d OR (request_type = %d AND stream = TRUE AND openai_ws_mode = FALSE))", startArgIndex, int16(service.RequestTypeUnknown)), []any{requestTypeArg}
|
||||
return fmt.Sprintf("(%srequest_type = $%d OR (%srequest_type = %d AND %sstream = TRUE AND %sopenai_ws_mode = FALSE))", prefix, startArgIndex, prefix, int16(service.RequestTypeUnknown), prefix, prefix), []any{requestTypeArg}
|
||||
case service.RequestTypeWSV2:
|
||||
return fmt.Sprintf("(request_type = $%d OR (request_type = %d AND openai_ws_mode = TRUE))", startArgIndex, int16(service.RequestTypeUnknown)), []any{requestTypeArg}
|
||||
return fmt.Sprintf("(%srequest_type = $%d OR (%srequest_type = %d AND %sopenai_ws_mode = TRUE))", prefix, startArgIndex, prefix, int16(service.RequestTypeUnknown), prefix), []any{requestTypeArg}
|
||||
default:
|
||||
return fmt.Sprintf("request_type = $%d", startArgIndex), []any{requestTypeArg}
|
||||
return fmt.Sprintf("%srequest_type = $%d", prefix, startArgIndex), []any{requestTypeArg}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,14 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -48,3 +53,27 @@ func TestResolveModelDimensionExpression(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserBreakdownStatsRequestTypeIncludesLegacyFallback(t *testing.T) {
|
||||
db, mock := newSQLMock(t)
|
||||
repo := &usageLogRepository{sql: db}
|
||||
start := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)
|
||||
end := start.Add(24 * time.Hour)
|
||||
requestType := int16(service.RequestTypeStream)
|
||||
|
||||
legacyFilter := `(ul.request_type = $3 OR (ul.request_type = 0 AND ul.stream = TRUE AND ul.openai_ws_mode = FALSE))`
|
||||
mock.ExpectQuery(regexp.QuoteMeta(legacyFilter)).
|
||||
WithArgs(start, end, requestType).
|
||||
WillReturnRows(sqlmock.NewRows([]string{
|
||||
"user_id", "email", "requests", "input_tokens", "output_tokens",
|
||||
"cache_tokens", "total_tokens", "cost", "actual_cost", "account_cost",
|
||||
}))
|
||||
|
||||
rows, err := repo.GetUserBreakdownStats(context.Background(), start, end, usagestats.UserBreakdownDimension{
|
||||
RequestType: &requestType,
|
||||
}, 0)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, rows)
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
@@ -642,8 +642,9 @@ func (r *usageLogRepository) GetUserBreakdownStats(ctx context.Context, startTim
|
||||
args = append(args, dim.AccountID)
|
||||
}
|
||||
if dim.RequestType != nil {
|
||||
query += fmt.Sprintf(" AND ul.request_type = $%d", len(args)+1)
|
||||
args = append(args, *dim.RequestType)
|
||||
condition, conditionArgs := buildRequestTypeFilterConditionWithAlias(len(args)+1, *dim.RequestType, "ul")
|
||||
query += " AND " + condition
|
||||
args = append(args, conditionArgs...)
|
||||
}
|
||||
if dim.Stream != nil {
|
||||
query += fmt.Sprintf(" AND ul.stream = $%d", len(args)+1)
|
||||
|
||||
@@ -293,6 +293,9 @@ func (s *BillingService) initFallbackPricing() {
|
||||
CacheCreationPricePerTokenPriority: 12.5e-6,
|
||||
CacheReadPricePerToken: 0.5e-6,
|
||||
CacheReadPricePerTokenPriority: 1e-6,
|
||||
LongContextInputThreshold: openAIGPT54LongContextInputThreshold,
|
||||
LongContextInputMultiplier: openAIGPT54LongContextInputMultiplier,
|
||||
LongContextOutputMultiplier: openAIGPT54LongContextOutputMultiplier,
|
||||
}
|
||||
s.fallbackPrices["gpt-5.6-terra"] = &ModelPricing{
|
||||
InputPricePerToken: 2.5e-6,
|
||||
@@ -303,6 +306,9 @@ func (s *BillingService) initFallbackPricing() {
|
||||
CacheCreationPricePerTokenPriority: 6.25e-6,
|
||||
CacheReadPricePerToken: 0.25e-6,
|
||||
CacheReadPricePerTokenPriority: 0.5e-6,
|
||||
LongContextInputThreshold: openAIGPT54LongContextInputThreshold,
|
||||
LongContextInputMultiplier: openAIGPT54LongContextInputMultiplier,
|
||||
LongContextOutputMultiplier: openAIGPT54LongContextOutputMultiplier,
|
||||
}
|
||||
s.fallbackPrices["gpt-5.6-luna"] = &ModelPricing{
|
||||
InputPricePerToken: 1e-6,
|
||||
@@ -313,6 +319,9 @@ func (s *BillingService) initFallbackPricing() {
|
||||
CacheCreationPricePerTokenPriority: 2.5e-6,
|
||||
CacheReadPricePerToken: 0.1e-6,
|
||||
CacheReadPricePerTokenPriority: 0.2e-6,
|
||||
LongContextInputThreshold: openAIGPT54LongContextInputThreshold,
|
||||
LongContextInputMultiplier: openAIGPT54LongContextInputMultiplier,
|
||||
LongContextOutputMultiplier: openAIGPT54LongContextOutputMultiplier,
|
||||
}
|
||||
|
||||
s.fallbackPrices["gpt-5.4-mini"] = &ModelPricing{
|
||||
@@ -1100,7 +1109,7 @@ func (s *BillingService) applyModelSpecificPricingPolicy(model string, pricing *
|
||||
if !isGPT56 && !usesLegacyLongContextPricing {
|
||||
return pricing
|
||||
}
|
||||
needsLongContextPolicy := usesLegacyLongContextPricing &&
|
||||
needsLongContextPolicy := (isGPT56 || usesLegacyLongContextPricing) &&
|
||||
(pricing.LongContextInputThreshold <= 0 || pricing.LongContextInputMultiplier <= 0 || pricing.LongContextOutputMultiplier <= 0)
|
||||
needsCacheCreationPolicy := isGPT56 && !pricing.CacheCreationPriceExplicit && (pricing.CacheCreationPricePerToken <= 0 ||
|
||||
(pricing.InputPricePerTokenPriority > 0 && pricing.CacheCreationPricePerTokenPriority <= 0))
|
||||
@@ -1108,7 +1117,7 @@ func (s *BillingService) applyModelSpecificPricingPolicy(model string, pricing *
|
||||
return pricing
|
||||
}
|
||||
cloned := *pricing
|
||||
if isGPT56 {
|
||||
if isGPT56 && !cloned.CacheCreationPriceExplicit {
|
||||
if cloned.CacheCreationPricePerToken <= 0 {
|
||||
cloned.CacheCreationPricePerToken = cloned.InputPricePerToken * 1.25
|
||||
}
|
||||
@@ -1116,7 +1125,7 @@ func (s *BillingService) applyModelSpecificPricingPolicy(model string, pricing *
|
||||
cloned.CacheCreationPricePerTokenPriority = cloned.InputPricePerTokenPriority * 1.25
|
||||
}
|
||||
}
|
||||
if usesLegacyLongContextPricing {
|
||||
if isGPT56 || usesLegacyLongContextPricing {
|
||||
if cloned.LongContextInputThreshold <= 0 {
|
||||
cloned.LongContextInputThreshold = openAIGPT54LongContextInputThreshold
|
||||
}
|
||||
|
||||
@@ -2,14 +2,12 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/xai"
|
||||
"github.com/Wei-Shaw/sub2api/internal/util/responseheaders"
|
||||
@@ -411,16 +409,9 @@ func (s *OpenAIGatewayService) bufferRawChatCompletions(
|
||||
return nil, fmt.Errorf("read upstream body: %w", err)
|
||||
}
|
||||
|
||||
var ccResp apicompat.ChatCompletionsResponse
|
||||
var usage OpenAIUsage
|
||||
if err := json.Unmarshal(respBody, &ccResp); err == nil && ccResp.Usage != nil {
|
||||
usage = OpenAIUsage{
|
||||
InputTokens: ccResp.Usage.PromptTokens,
|
||||
OutputTokens: ccResp.Usage.CompletionTokens,
|
||||
}
|
||||
if ccResp.Usage.PromptTokensDetails != nil {
|
||||
usage.CacheReadInputTokens = ccResp.Usage.PromptTokensDetails.CachedTokens
|
||||
}
|
||||
if parsedUsage, ok := extractOpenAIUsageFromJSONBytes(respBody); ok {
|
||||
usage = parsedUsage
|
||||
}
|
||||
|
||||
if s.responseHeaderFilter != nil {
|
||||
|
||||
@@ -155,6 +155,57 @@ func TestForwardAsRawChatCompletions_PreservesMappedGPT56MaxEffort(t *testing.T)
|
||||
require.Equal(t, "max", *result.ReasoningEffort)
|
||||
}
|
||||
|
||||
func TestForwardAsRawChatCompletions_NonStreamingCapturesCacheWriteUsage(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
usageJSON string
|
||||
wantWrite int
|
||||
}{
|
||||
{
|
||||
name: "positive cache write",
|
||||
usageJSON: `{"prompt_tokens":12,"completion_tokens":3,"total_tokens":15,"prompt_tokens_details":{"cached_tokens":4,"cache_write_tokens":6}}`,
|
||||
wantWrite: 6,
|
||||
},
|
||||
{
|
||||
name: "nested zero overrides legacy alias",
|
||||
usageJSON: `{"prompt_tokens":12,"completion_tokens":3,"total_tokens":15,"cache_creation_input_tokens":19,"prompt_tokens_details":{"cached_tokens":4,"cache_write_tokens":0}}`,
|
||||
wantWrite: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
body := []byte(`{"model":"gpt-5.6","messages":[{"role":"user","content":"hello"}],"stream":false}`)
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", bytes.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
upstream := &httpUpstreamRecorder{resp: &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
Body: io.NopCloser(strings.NewReader(
|
||||
`{"id":"chatcmpl_cache","object":"chat.completion","model":"gpt-5.6","choices":[{"index":0,"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}],"usage":` + tt.usageJSON + `}`,
|
||||
)),
|
||||
}}
|
||||
svc := &OpenAIGatewayService{
|
||||
cfg: rawChatCompletionsTestConfig(),
|
||||
httpUpstream: upstream,
|
||||
}
|
||||
|
||||
result, err := svc.forwardAsRawChatCompletions(context.Background(), c, rawChatCompletionsTestAccount(), body, "")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.Equal(t, 12, result.Usage.InputTokens)
|
||||
require.Equal(t, 4, result.Usage.CacheReadInputTokens)
|
||||
require.Equal(t, tt.wantWrite, result.Usage.CacheCreationInputTokens)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestForwardAsRawChatCompletions_PreservesDeepSeekReasoningContentNonStreaming(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
|
||||
@@ -1120,13 +1120,6 @@ func copyOpenAIUsageFromResponsesUsage(usage *apicompat.ResponsesUsage) OpenAIUs
|
||||
}
|
||||
if usage.InputTokensDetails != nil {
|
||||
result.CacheReadInputTokens = usage.InputTokensDetails.CachedTokens
|
||||
if result.CacheCreationInputTokens == 0 {
|
||||
if usage.InputTokensDetails.CacheWriteTokens > 0 {
|
||||
result.CacheCreationInputTokens = usage.InputTokensDetails.CacheWriteTokens
|
||||
} else {
|
||||
result.CacheCreationInputTokens = usage.InputTokensDetails.CacheCreationTokens
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
//go:build unit
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCopyOpenAIUsageFromResponsesUsageTrustsCanonicalCacheCreationValue(t *testing.T) {
|
||||
usage := &apicompat.ResponsesUsage{
|
||||
InputTokens: 20,
|
||||
OutputTokens: 2,
|
||||
CacheCreationInputTokens: 0,
|
||||
InputTokensDetails: &apicompat.ResponsesInputTokensDetails{
|
||||
CachedTokens: 3,
|
||||
CacheWriteTokens: 19,
|
||||
},
|
||||
}
|
||||
|
||||
got := copyOpenAIUsageFromResponsesUsage(usage)
|
||||
|
||||
require.Equal(t, 20, got.InputTokens)
|
||||
require.Equal(t, 3, got.CacheReadInputTokens)
|
||||
require.Zero(t, got.CacheCreationInputTokens)
|
||||
}
|
||||
@@ -772,9 +772,16 @@ func openAIUsageFromGJSON(value gjson.Result) (OpenAIUsage, bool) {
|
||||
}
|
||||
|
||||
func openAICacheReadTokensFromUsage(value gjson.Result) int {
|
||||
return firstPositiveGJSONInt(
|
||||
for _, nested := range []gjson.Result{
|
||||
value.Get("input_tokens_details.cached_tokens"),
|
||||
value.Get("prompt_tokens_details.cached_tokens"),
|
||||
} {
|
||||
if nested.Exists() {
|
||||
return max(int(nested.Int()), 0)
|
||||
}
|
||||
}
|
||||
|
||||
return firstPositiveGJSONInt(
|
||||
value.Get("cache_read_input_tokens"),
|
||||
value.Get("cache_read_tokens"),
|
||||
value.Get("cached_tokens"),
|
||||
@@ -782,11 +789,18 @@ func openAICacheReadTokensFromUsage(value gjson.Result) int {
|
||||
}
|
||||
|
||||
func openAICacheCreationTokensFromUsage(value gjson.Result) int {
|
||||
return firstPositiveGJSONInt(
|
||||
for _, nested := range []gjson.Result{
|
||||
value.Get("input_tokens_details.cache_write_tokens"),
|
||||
value.Get("prompt_tokens_details.cache_write_tokens"),
|
||||
value.Get("input_tokens_details.cache_creation_tokens"),
|
||||
value.Get("prompt_tokens_details.cache_creation_tokens"),
|
||||
} {
|
||||
if nested.Exists() {
|
||||
return max(int(nested.Int()), 0)
|
||||
}
|
||||
}
|
||||
|
||||
return firstPositiveGJSONInt(
|
||||
value.Get("cache_write_tokens"),
|
||||
value.Get("cache_creation_input_tokens"),
|
||||
value.Get("cache_write_input_tokens"),
|
||||
|
||||
@@ -2821,6 +2821,14 @@ func TestExtractOpenAIUsageFromJSONBytes_AcceptsResponseAndChatUsageShapes(t *te
|
||||
usage, ok = extractOpenAIUsageFromJSONBytes([]byte(`{"usage":{"input_tokens":20,"output_tokens":2,"cache_creation_input_tokens":19,"input_tokens_details":{"cache_write_tokens":7}}}`))
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 7, usage.CacheCreationInputTokens, "官方嵌套字段应优先于兼容顶层别名")
|
||||
|
||||
usage, ok = extractOpenAIUsageFromJSONBytes([]byte(`{"usage":{"input_tokens":20,"output_tokens":2,"cache_creation_input_tokens":19,"input_tokens_details":{"cache_write_tokens":0}}}`))
|
||||
require.True(t, ok)
|
||||
require.Zero(t, usage.CacheCreationInputTokens, "官方嵌套字段显式为零时仍应优先于兼容顶层别名")
|
||||
|
||||
usage, ok = extractOpenAIUsageFromJSONBytes([]byte(`{"usage":{"input_tokens":20,"output_tokens":2,"cache_read_input_tokens":19,"input_tokens_details":{"cached_tokens":0}}}`))
|
||||
require.True(t, ok)
|
||||
require.Zero(t, usage.CacheReadInputTokens, "官方嵌套缓存读取字段显式为零时仍应优先于兼容顶层别名")
|
||||
}
|
||||
|
||||
func TestExtractCodexFinalResponse_SampleReplay(t *testing.T) {
|
||||
|
||||
@@ -71,6 +71,14 @@ func normalizeKnownOpenAICodexModel(model string) string {
|
||||
return "gpt-5.6-terra"
|
||||
case strings.Contains(normalized, "gpt-5.6-luna"):
|
||||
return "gpt-5.6-luna"
|
||||
case normalized == "gpt-5.6":
|
||||
return "gpt-5.6-sol"
|
||||
case strings.HasPrefix(normalized, "gpt-5.6-"):
|
||||
suffix := strings.TrimPrefix(normalized, "gpt-5.6-")
|
||||
if suffix == "max" || isKnownCodexModelSuffix(suffix) {
|
||||
return "gpt-5.6-sol"
|
||||
}
|
||||
return ""
|
||||
case strings.Contains(normalized, "gpt-5.5-pro"):
|
||||
return "gpt-5.5-pro"
|
||||
case strings.Contains(normalized, "gpt-5.5"):
|
||||
@@ -102,6 +110,12 @@ func normalizeKnownOpenAICodexModel(model string) string {
|
||||
// (含大小写/路径/后缀变体)或已归一化的基名,两者均能正确识别。
|
||||
func isOpenAIGPT56Model(model string) bool {
|
||||
normalized := canonicalizeOpenAIModelAliasSpelling(model)
|
||||
if normalized == "gpt-5.6" {
|
||||
return true
|
||||
}
|
||||
if suffix, ok := strings.CutPrefix(normalized, "gpt-5.6-"); ok && (suffix == "max" || isKnownCodexModelSuffix(suffix)) {
|
||||
return true
|
||||
}
|
||||
for _, prefix := range []string{"gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"} {
|
||||
if normalized == prefix || strings.HasPrefix(normalized, prefix+"-") {
|
||||
return true
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNormalizeKnownOpenAICodexModel_BareGPT56RoutesToSol(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
"gpt-5.6": "gpt-5.6-sol",
|
||||
"openai/gpt-5.6": "gpt-5.6-sol",
|
||||
"gpt5.6": "gpt-5.6-sol",
|
||||
"gpt-5.6-high": "gpt-5.6-sol",
|
||||
"gpt-5.6-max": "gpt-5.6-sol",
|
||||
"gpt-5.6-2026-07-09": "gpt-5.6-sol",
|
||||
"openai/gpt-5.6-max": "gpt-5.6-sol",
|
||||
}
|
||||
|
||||
for input, expected := range tests {
|
||||
t.Run(input, func(t *testing.T) {
|
||||
require.Equal(t, expected, normalizeKnownOpenAICodexModel(input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUsageBillingModelCandidates_BareGPT56IncludesSol(t *testing.T) {
|
||||
require.Equal(t,
|
||||
[]string{"gpt-5.6", "gpt-5.6-sol"},
|
||||
usageBillingModelCandidates("gpt-5.6"),
|
||||
)
|
||||
require.Equal(t,
|
||||
[]string{"openai/gpt-5.6", "gpt-5.6", "gpt-5.6-sol"},
|
||||
usageBillingModelCandidates("openai/gpt-5.6"),
|
||||
)
|
||||
}
|
||||
@@ -252,6 +252,18 @@ func TestNormalizeOpenAIModelForUpstream(t *testing.T) {
|
||||
model string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "oauth routes bare GPT-5.6 alias to Sol",
|
||||
account: &Account{Type: AccountTypeOAuth},
|
||||
model: "gpt-5.6",
|
||||
want: "gpt-5.6-sol",
|
||||
},
|
||||
{
|
||||
name: "oauth routes provider-prefixed GPT-5.6 alias to Sol",
|
||||
account: &Account{Type: AccountTypeOAuth},
|
||||
model: "openai/gpt-5.6",
|
||||
want: "gpt-5.6-sol",
|
||||
},
|
||||
{
|
||||
name: "oauth preserves unknown non codex model",
|
||||
account: &Account{Type: AccountTypeOAuth},
|
||||
@@ -282,6 +294,12 @@ func TestNormalizeOpenAIModelForUpstream(t *testing.T) {
|
||||
model: "codex-auto-review",
|
||||
want: "codex-auto-review",
|
||||
},
|
||||
{
|
||||
name: "apikey preserves official bare GPT-5.6 alias",
|
||||
account: &Account{Type: AccountTypeAPIKey},
|
||||
model: "gpt-5.6",
|
||||
want: "gpt-5.6",
|
||||
},
|
||||
{
|
||||
name: "apikey preserves custom compatible model",
|
||||
account: &Account{Type: AccountTypeAPIKey},
|
||||
|
||||
@@ -816,6 +816,13 @@ func openAICacheCreationTokensFromUsage(value gjson.Result) int {
|
||||
"prompt_tokens_details.cache_write_tokens",
|
||||
"input_tokens_details.cache_creation_tokens",
|
||||
"prompt_tokens_details.cache_creation_tokens",
|
||||
} {
|
||||
result := value.Get(field)
|
||||
if result.Exists() {
|
||||
return max(int(result.Int()), 0)
|
||||
}
|
||||
}
|
||||
for _, field := range []string{
|
||||
"cache_write_tokens",
|
||||
"cache_creation_input_tokens",
|
||||
"cache_write_input_tokens",
|
||||
|
||||
@@ -335,6 +335,13 @@ func TestParseUsageAndAccumulateAcceptsChatUsageAliases(t *testing.T) {
|
||||
require.Equal(t, got, state.usage)
|
||||
}
|
||||
|
||||
func TestOpenAICacheCreationTokensFromUsageNestedZeroWins(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
usage := gjson.Parse(`{"input_tokens_details":{"cache_write_tokens":0},"cache_creation_input_tokens":19}`)
|
||||
require.Zero(t, openAICacheCreationTokensFromUsage(usage))
|
||||
}
|
||||
|
||||
func TestEmitTurnCompleteCoverage(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -44,6 +44,9 @@ var (
|
||||
CacheCreationInputTokenCostPriority: 1.25e-05,
|
||||
CacheReadInputTokenCost: 5e-07,
|
||||
CacheReadInputTokenCostPriority: 1e-06,
|
||||
LongContextInputTokenThreshold: openAIGPT54LongContextInputThreshold,
|
||||
LongContextInputCostMultiplier: openAIGPT54LongContextInputMultiplier,
|
||||
LongContextOutputCostMultiplier: openAIGPT54LongContextOutputMultiplier,
|
||||
SupportsServiceTier: true,
|
||||
LiteLLMProvider: "openai",
|
||||
Mode: "chat",
|
||||
@@ -58,6 +61,9 @@ var (
|
||||
CacheCreationInputTokenCostPriority: 6.25e-06,
|
||||
CacheReadInputTokenCost: 2.5e-07,
|
||||
CacheReadInputTokenCostPriority: 5e-07,
|
||||
LongContextInputTokenThreshold: openAIGPT54LongContextInputThreshold,
|
||||
LongContextInputCostMultiplier: openAIGPT54LongContextInputMultiplier,
|
||||
LongContextOutputCostMultiplier: openAIGPT54LongContextOutputMultiplier,
|
||||
SupportsServiceTier: true,
|
||||
LiteLLMProvider: "openai",
|
||||
Mode: "chat",
|
||||
@@ -72,6 +78,9 @@ var (
|
||||
CacheCreationInputTokenCostPriority: 2.5e-06,
|
||||
CacheReadInputTokenCost: 1e-07,
|
||||
CacheReadInputTokenCostPriority: 2e-07,
|
||||
LongContextInputTokenThreshold: openAIGPT54LongContextInputThreshold,
|
||||
LongContextInputCostMultiplier: openAIGPT54LongContextInputMultiplier,
|
||||
LongContextOutputCostMultiplier: openAIGPT54LongContextOutputMultiplier,
|
||||
SupportsServiceTier: true,
|
||||
LiteLLMProvider: "openai",
|
||||
Mode: "chat",
|
||||
@@ -140,6 +149,9 @@ type LiteLLMRawEntry struct {
|
||||
CacheCreationInputTokenCostAbove1hr *float64 `json:"cache_creation_input_token_cost_above_1hr"`
|
||||
CacheReadInputTokenCost *float64 `json:"cache_read_input_token_cost"`
|
||||
CacheReadInputTokenCostPriority *float64 `json:"cache_read_input_token_cost_priority"`
|
||||
LongContextInputTokenThreshold *int `json:"long_context_input_token_threshold"`
|
||||
LongContextInputCostMultiplier *float64 `json:"long_context_input_cost_multiplier"`
|
||||
LongContextOutputCostMultiplier *float64 `json:"long_context_output_cost_multiplier"`
|
||||
SupportsServiceTier bool `json:"supports_service_tier"`
|
||||
LiteLLMProvider string `json:"litellm_provider"`
|
||||
Mode string `json:"mode"`
|
||||
@@ -462,6 +474,15 @@ func (s *PricingService) parsePricingData(body []byte) (map[string]*LiteLLMModel
|
||||
if entry.CacheReadInputTokenCostPriority != nil {
|
||||
pricing.CacheReadInputTokenCostPriority = *entry.CacheReadInputTokenCostPriority
|
||||
}
|
||||
if entry.LongContextInputTokenThreshold != nil {
|
||||
pricing.LongContextInputTokenThreshold = *entry.LongContextInputTokenThreshold
|
||||
}
|
||||
if entry.LongContextInputCostMultiplier != nil {
|
||||
pricing.LongContextInputCostMultiplier = *entry.LongContextInputCostMultiplier
|
||||
}
|
||||
if entry.LongContextOutputCostMultiplier != nil {
|
||||
pricing.LongContextOutputCostMultiplier = *entry.LongContextOutputCostMultiplier
|
||||
}
|
||||
if entry.OutputCostPerImage != nil {
|
||||
pricing.OutputCostPerImage = *entry.OutputCostPerImage
|
||||
}
|
||||
@@ -713,6 +734,12 @@ func normalizeModelNameForPricing(model string) string {
|
||||
|
||||
model = strings.TrimLeft(model, "/")
|
||||
if canonical := canonicalizeOpenAIModelAliasSpelling(model); canonical != "" {
|
||||
if canonical == "gpt-5.6" {
|
||||
return "gpt-5.6-sol"
|
||||
}
|
||||
if suffix, ok := strings.CutPrefix(canonical, "gpt-5.6-"); ok && (suffix == "max" || isKnownCodexModelSuffix(suffix)) {
|
||||
return "gpt-5.6-sol"
|
||||
}
|
||||
return canonical
|
||||
}
|
||||
return model
|
||||
|
||||
@@ -22,6 +22,9 @@ func TestParsePricingData_ParsesPriorityAndServiceTierFields(t *testing.T) {
|
||||
"cache_creation_input_token_cost_priority": 0.000005,
|
||||
"cache_read_input_token_cost": 0.00000025,
|
||||
"cache_read_input_token_cost_priority": 0.0000005,
|
||||
"long_context_input_token_threshold": 272000,
|
||||
"long_context_input_cost_multiplier": 2,
|
||||
"long_context_output_cost_multiplier": 1.5,
|
||||
"supports_service_tier": true,
|
||||
"supports_prompt_caching": true,
|
||||
"litellm_provider": "openai",
|
||||
@@ -37,6 +40,9 @@ func TestParsePricingData_ParsesPriorityAndServiceTierFields(t *testing.T) {
|
||||
require.InDelta(t, 3e-5, pricing.OutputCostPerTokenPriority, 1e-12)
|
||||
require.InDelta(t, 5e-6, pricing.CacheCreationInputTokenCostPriority, 1e-12)
|
||||
require.InDelta(t, 5e-7, pricing.CacheReadInputTokenCostPriority, 1e-12)
|
||||
require.Equal(t, 272000, pricing.LongContextInputTokenThreshold)
|
||||
require.InDelta(t, 2.0, pricing.LongContextInputCostMultiplier, 1e-12)
|
||||
require.InDelta(t, 1.5, pricing.LongContextOutputCostMultiplier, 1e-12)
|
||||
require.True(t, pricing.SupportsServiceTier)
|
||||
}
|
||||
|
||||
@@ -72,7 +78,9 @@ func TestBillingService_GPT56CacheWritePricingUsesOfficialMultiplier(t *testing.
|
||||
require.NoError(t, err)
|
||||
require.InDelta(t, tt.input*1.25, pricing.CacheCreationPricePerToken, 1e-12)
|
||||
require.InDelta(t, tt.inputPriority*1.25, pricing.CacheCreationPricePerTokenPriority, 1e-12)
|
||||
require.Zero(t, pricing.LongContextInputThreshold)
|
||||
require.Equal(t, 272000, pricing.LongContextInputThreshold)
|
||||
require.InDelta(t, 2.0, pricing.LongContextInputMultiplier, 1e-12)
|
||||
require.InDelta(t, 1.5, pricing.LongContextOutputMultiplier, 1e-12)
|
||||
|
||||
tokens := UsageTokens{InputTokens: 700, OutputTokens: 50, CacheCreationTokens: 200, CacheReadTokens: 100}
|
||||
standard, err := svc.CalculateCostWithServiceTier(tt.model, tokens, 1, "")
|
||||
@@ -90,25 +98,87 @@ func TestBillingService_GPT56CacheWritePricingUsesOfficialMultiplier(t *testing.
|
||||
}
|
||||
}
|
||||
|
||||
func TestBillingService_GPT56DoesNotUseLegacyLongContextMultiplier(t *testing.T) {
|
||||
model := "gpt-5.6-sol"
|
||||
pricingSvc := &PricingService{pricingData: map[string]*LiteLLMModelPricing{
|
||||
model: {
|
||||
InputCostPerToken: 5e-6,
|
||||
OutputCostPerToken: 30e-6,
|
||||
CacheReadInputTokenCost: 0.5e-6,
|
||||
},
|
||||
}}
|
||||
svc := NewBillingService(&config.Config{}, pricingSvc)
|
||||
tokens := UsageTokens{InputTokens: 100000, CacheCreationTokens: 173000, OutputTokens: 10}
|
||||
func TestBillingService_GPT56UsesLongContextPricingAcrossModelsAndTiers(t *testing.T) {
|
||||
models := []struct {
|
||||
name string
|
||||
input, cached float64
|
||||
cacheWrite, output float64
|
||||
}{
|
||||
{name: "gpt-5.6-sol", input: 5e-6, cached: 0.5e-6, cacheWrite: 6.25e-6, output: 30e-6},
|
||||
{name: "gpt-5.6-terra", input: 2.5e-6, cached: 0.25e-6, cacheWrite: 3.125e-6, output: 15e-6},
|
||||
{name: "gpt-5.6-luna", input: 1e-6, cached: 0.1e-6, cacheWrite: 1.25e-6, output: 6e-6},
|
||||
}
|
||||
tiers := []struct {
|
||||
name string
|
||||
priceScale float64
|
||||
}{
|
||||
{name: "standard", priceScale: 1},
|
||||
{name: "priority", priceScale: 2},
|
||||
{name: "flex", priceScale: 0.5},
|
||||
}
|
||||
tokens := UsageTokens{
|
||||
InputTokens: 100000,
|
||||
CacheCreationTokens: 100000,
|
||||
CacheReadTokens: 73000,
|
||||
OutputTokens: 10,
|
||||
}
|
||||
|
||||
cost, err := svc.CalculateCost(model, tokens, 1)
|
||||
for _, model := range models {
|
||||
for _, tier := range tiers {
|
||||
t.Run(model.name+"/"+tier.name, func(t *testing.T) {
|
||||
svc := NewBillingService(&config.Config{}, nil)
|
||||
serviceTier := ""
|
||||
if tier.name != "standard" {
|
||||
serviceTier = tier.name
|
||||
}
|
||||
cost, err := svc.CalculateCostWithServiceTier(model.name, tokens, 1, serviceTier)
|
||||
require.NoError(t, err)
|
||||
require.InDelta(t, float64(tokens.InputTokens)*model.input*tier.priceScale*2, cost.InputCost, 1e-12)
|
||||
require.InDelta(t, float64(tokens.CacheCreationTokens)*model.cacheWrite*tier.priceScale*2, cost.CacheCreationCost, 1e-12)
|
||||
require.InDelta(t, float64(tokens.CacheReadTokens)*model.cached*tier.priceScale*2, cost.CacheReadCost, 1e-12)
|
||||
require.InDelta(t, float64(tokens.OutputTokens)*model.output*tier.priceScale*1.5, cost.OutputCost, 1e-12)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBillingService_GPT56LongContextBoundaryIsExclusive(t *testing.T) {
|
||||
svc := NewBillingService(&config.Config{}, nil)
|
||||
tokens := UsageTokens{InputTokens: 100000, CacheCreationTokens: 100000, CacheReadTokens: 72000, OutputTokens: 10}
|
||||
|
||||
cost, err := svc.CalculateCost("gpt-5.6-sol", tokens, 1)
|
||||
require.NoError(t, err)
|
||||
require.InDelta(t, 100000*5e-6, cost.InputCost, 1e-12)
|
||||
require.InDelta(t, 173000*6.25e-6, cost.CacheCreationCost, 1e-12)
|
||||
require.InDelta(t, 100000*6.25e-6, cost.CacheCreationCost, 1e-12)
|
||||
require.InDelta(t, 72000*0.5e-6, cost.CacheReadCost, 1e-12)
|
||||
require.InDelta(t, 10*30e-6, cost.OutputCost, 1e-12)
|
||||
}
|
||||
|
||||
func TestPricingService_BareGPT56AliasDeterministicallyUsesSol(t *testing.T) {
|
||||
pricingSvc := &PricingService{pricingData: map[string]*LiteLLMModelPricing{
|
||||
"gpt-5.6-sol": {InputCostPerToken: 5e-6},
|
||||
"gpt-5.6-terra": {InputCostPerToken: 2.5e-6},
|
||||
"gpt-5.6-luna": {InputCostPerToken: 1e-6},
|
||||
"gpt-5.4": {InputCostPerToken: 2.5e-6},
|
||||
}}
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
for _, alias := range []string{"gpt-5.6", "openai/gpt-5.6"} {
|
||||
pricing := pricingSvc.GetModelPricing(alias)
|
||||
require.NotNil(t, pricing)
|
||||
require.InDelta(t, 5e-6, pricing.InputCostPerToken, 1e-12, "iteration=%d alias=%s", i, alias)
|
||||
}
|
||||
}
|
||||
|
||||
billingSvc := NewBillingService(&config.Config{}, pricingSvc)
|
||||
for _, alias := range []string{"gpt-5.6", "openai/gpt-5.6"} {
|
||||
pricing, err := billingSvc.GetModelPricing(alias)
|
||||
require.NoError(t, err)
|
||||
require.InDelta(t, 5e-6, pricing.InputPricePerToken, 1e-12)
|
||||
require.InDelta(t, 6.25e-6, pricing.CacheCreationPricePerToken, 1e-12)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultPricingIncludesOfficialGPT56Rates(t *testing.T) {
|
||||
data, err := os.ReadFile(filepath.Join("..", "..", "resources", "model-pricing", "model_prices_and_context_window.json"))
|
||||
require.NoError(t, err)
|
||||
@@ -140,7 +210,9 @@ func TestDefaultPricingIncludesOfficialGPT56Rates(t *testing.T) {
|
||||
require.InDelta(t, tt.cachedPriority, pricing.CacheReadPricePerTokenPriority, 1e-12)
|
||||
require.InDelta(t, tt.cacheWritePriority, pricing.CacheCreationPricePerTokenPriority, 1e-12)
|
||||
require.InDelta(t, tt.outputPriority, pricing.OutputPricePerTokenPriority, 1e-12)
|
||||
require.Zero(t, pricing.LongContextInputThreshold)
|
||||
require.Equal(t, 272000, pricing.LongContextInputThreshold)
|
||||
require.InDelta(t, 2.0, pricing.LongContextInputMultiplier, 1e-12)
|
||||
require.InDelta(t, 1.5, pricing.LongContextOutputMultiplier, 1e-12)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -181,7 +253,9 @@ func assertGPT56FallbackPricing(t *testing.T, pricing *ModelPricing, input, cach
|
||||
require.InDelta(t, cached, pricing.CacheReadPricePerToken, 1e-12)
|
||||
require.InDelta(t, cacheWrite, pricing.CacheCreationPricePerToken, 1e-12)
|
||||
require.InDelta(t, output, pricing.OutputPricePerToken, 1e-12)
|
||||
require.Zero(t, pricing.LongContextInputThreshold)
|
||||
require.Equal(t, 272000, pricing.LongContextInputThreshold)
|
||||
require.InDelta(t, 2.0, pricing.LongContextInputMultiplier, 1e-12)
|
||||
require.InDelta(t, 1.5, pricing.LongContextOutputMultiplier, 1e-12)
|
||||
}
|
||||
|
||||
func TestParsePricingData_KeepsImageOnlyPricing(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Cyber-policy blocks are recorded as request_type=4 so they remain visible in
|
||||
-- usage audits without being confused with legacy request_type=0 rows.
|
||||
ALTER TABLE usage_logs
|
||||
DROP CONSTRAINT IF EXISTS usage_logs_request_type_check;
|
||||
|
||||
ALTER TABLE usage_logs
|
||||
ADD CONSTRAINT usage_logs_request_type_check
|
||||
CHECK (request_type IN (0, 1, 2, 3, 4)) NOT VALID;
|
||||
@@ -213,3 +213,30 @@ func TestMigration154aAddsSparkShadowIndexesConcurrently(t *testing.T) {
|
||||
require.Contains(t, sql, "quota_dimension = 'spark'")
|
||||
require.Contains(t, sql, "deleted_at IS NULL")
|
||||
}
|
||||
|
||||
func TestMigration173AllowsCyberBlockedUsageRequestType(t *testing.T) {
|
||||
entries, err := FS.ReadDir(".")
|
||||
require.NoError(t, err)
|
||||
|
||||
previousIndex := -1
|
||||
currentIndex := -1
|
||||
for i, entry := range entries {
|
||||
switch entry.Name() {
|
||||
case "172_video_per_second_billing_metadata.sql":
|
||||
previousIndex = i
|
||||
case "173_allow_cyber_blocked_usage_request_type.sql":
|
||||
currentIndex = i
|
||||
}
|
||||
}
|
||||
require.NotEqual(t, -1, previousIndex)
|
||||
require.NotEqual(t, -1, currentIndex)
|
||||
require.Less(t, previousIndex, currentIndex)
|
||||
|
||||
content, err := FS.ReadFile("173_allow_cyber_blocked_usage_request_type.sql")
|
||||
require.NoError(t, err)
|
||||
|
||||
sql := string(content)
|
||||
require.Contains(t, sql, "DROP CONSTRAINT IF EXISTS usage_logs_request_type_check")
|
||||
require.Contains(t, sql, "ADD CONSTRAINT usage_logs_request_type_check")
|
||||
require.Contains(t, sql, "CHECK (request_type IN (0, 1, 2, 3, 4)) NOT VALID")
|
||||
}
|
||||
|
||||
@@ -4972,6 +4972,9 @@
|
||||
"input_cost_per_token_batches": 2.5e-06,
|
||||
"input_cost_per_token_flex": 2.5e-06,
|
||||
"input_cost_per_token_priority": 1e-05,
|
||||
"long_context_input_token_threshold": 272000,
|
||||
"long_context_input_cost_multiplier": 2.0,
|
||||
"long_context_output_cost_multiplier": 1.5,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 1050000,
|
||||
"max_output_tokens": 128000,
|
||||
@@ -5021,6 +5024,9 @@
|
||||
"input_cost_per_token_batches": 1.25e-06,
|
||||
"input_cost_per_token_flex": 1.25e-06,
|
||||
"input_cost_per_token_priority": 5e-06,
|
||||
"long_context_input_token_threshold": 272000,
|
||||
"long_context_input_cost_multiplier": 2.0,
|
||||
"long_context_output_cost_multiplier": 1.5,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 1050000,
|
||||
"max_output_tokens": 128000,
|
||||
@@ -5070,6 +5076,9 @@
|
||||
"input_cost_per_token_batches": 5e-07,
|
||||
"input_cost_per_token_flex": 5e-07,
|
||||
"input_cost_per_token_priority": 2e-06,
|
||||
"long_context_input_token_threshold": 272000,
|
||||
"long_context_input_cost_multiplier": 2.0,
|
||||
"long_context_output_cost_multiplier": 1.5,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 1050000,
|
||||
"max_output_tokens": 128000,
|
||||
|
||||
@@ -636,6 +636,23 @@ function generateOpenCodeConfig(platform: string, baseUrl: string, apiKey: strin
|
||||
xhigh: {}
|
||||
}
|
||||
},
|
||||
'gpt-5.6': {
|
||||
name: 'GPT-5.6 (Sol)',
|
||||
limit: {
|
||||
context: 1050000,
|
||||
output: 128000
|
||||
},
|
||||
options: {
|
||||
store: false
|
||||
},
|
||||
variants: {
|
||||
low: {},
|
||||
medium: {},
|
||||
high: {},
|
||||
xhigh: {},
|
||||
max: {}
|
||||
}
|
||||
},
|
||||
'gpt-5.6-sol': {
|
||||
name: 'GPT-5.6 Sol',
|
||||
limit: {
|
||||
@@ -649,7 +666,8 @@ function generateOpenCodeConfig(platform: string, baseUrl: string, apiKey: strin
|
||||
low: {},
|
||||
medium: {},
|
||||
high: {},
|
||||
xhigh: {}
|
||||
xhigh: {},
|
||||
max: {}
|
||||
}
|
||||
},
|
||||
'gpt-5.6-terra': {
|
||||
@@ -665,7 +683,8 @@ function generateOpenCodeConfig(platform: string, baseUrl: string, apiKey: strin
|
||||
low: {},
|
||||
medium: {},
|
||||
high: {},
|
||||
xhigh: {}
|
||||
xhigh: {},
|
||||
max: {}
|
||||
}
|
||||
},
|
||||
'gpt-5.6-luna': {
|
||||
@@ -681,7 +700,8 @@ function generateOpenCodeConfig(platform: string, baseUrl: string, apiKey: strin
|
||||
low: {},
|
||||
medium: {},
|
||||
high: {},
|
||||
xhigh: {}
|
||||
xhigh: {},
|
||||
max: {}
|
||||
}
|
||||
},
|
||||
'gpt-5.5': {
|
||||
|
||||
@@ -123,6 +123,43 @@ describe('UseKeyModal', () => {
|
||||
expect(codeBlock.text()).not.toContain('"name": "GPT-5.4 Nano"')
|
||||
})
|
||||
|
||||
it('renders GPT-5.6 alias and max variants in OpenCode config', async () => {
|
||||
const wrapper = mount(UseKeyModal, {
|
||||
props: {
|
||||
show: true,
|
||||
apiKey: 'sk-test',
|
||||
baseUrl: 'https://example.com/v1',
|
||||
platform: 'openai'
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
BaseDialog: {
|
||||
template: '<div><slot /><slot name="footer" /></div>'
|
||||
},
|
||||
Icon: {
|
||||
template: '<span />'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const opencodeTab = wrapper.findAll('button').find((button) =>
|
||||
button.text().includes('keys.useKeyModal.cliTabs.opencode')
|
||||
)
|
||||
expect(opencodeTab).toBeDefined()
|
||||
await opencodeTab!.trigger('click')
|
||||
await nextTick()
|
||||
|
||||
const parsed = JSON.parse(wrapper.find('pre code').text())
|
||||
const models = parsed.provider.openai.models
|
||||
for (const model of ['gpt-5.6', 'gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna']) {
|
||||
expect(models[model]).toBeDefined()
|
||||
expect(models[model].variants).toHaveProperty('max')
|
||||
expect(models[model].variants).toHaveProperty('xhigh')
|
||||
}
|
||||
expect(models['gpt-5.6'].name).toBe('GPT-5.6 (Sol)')
|
||||
})
|
||||
|
||||
it('renders Claude Fable 5 OpenCode config with adaptive thinking', async () => {
|
||||
const wrapper = mount(UseKeyModal, {
|
||||
props: {
|
||||
|
||||
@@ -14,6 +14,7 @@ describe('useModelWhitelist', () => {
|
||||
expect(models).toContain('gpt-5.4-mini')
|
||||
expect(models).toContain('gpt-5.4-2026-03-05')
|
||||
expect(models).toContain('codex-auto-review')
|
||||
expect(models).toContain('gpt-5.6')
|
||||
})
|
||||
|
||||
it('openai 模型列表不再暴露已下线的 ChatGPT 登录 Codex 模型', () => {
|
||||
|
||||
@@ -8,7 +8,7 @@ const openaiModels = [
|
||||
'gpt-5.2', 'gpt-5.2-2025-12-11', 'gpt-5.2-chat-latest',
|
||||
'gpt-5.2-pro', 'gpt-5.2-pro-2025-12-11',
|
||||
// GPT-5.6 系列
|
||||
'gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna',
|
||||
'gpt-5.6', 'gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna',
|
||||
// GPT-5.5 系列
|
||||
'gpt-5.5',
|
||||
// GPT-5.4 系列
|
||||
@@ -280,6 +280,7 @@ const openaiPresetMappings = [
|
||||
{ label: 'o3', from: 'o3', to: 'o3', color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400' },
|
||||
{ label: 'GPT-5.3 Codex Spark', from: 'gpt-5.3-codex-spark', to: 'gpt-5.3-codex-spark', color: 'bg-teal-100 text-teal-700 hover:bg-teal-200 dark:bg-teal-900/30 dark:text-teal-400' },
|
||||
{ label: 'GPT-5.2', from: 'gpt-5.2', to: 'gpt-5.2', color: 'bg-red-100 text-red-700 hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400' },
|
||||
{ label: 'GPT-5.6', from: 'gpt-5.6', to: 'gpt-5.6', color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400' },
|
||||
{ label: 'GPT-5.6 Sol', from: 'gpt-5.6-sol', to: 'gpt-5.6-sol', color: 'bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-400' },
|
||||
{ label: 'GPT-5.6 Terra', from: 'gpt-5.6-terra', to: 'gpt-5.6-terra', color: 'bg-lime-100 text-lime-700 hover:bg-lime-200 dark:bg-lime-900/30 dark:text-lime-400' },
|
||||
{ label: 'GPT-5.6 Luna', from: 'gpt-5.6-luna', to: 'gpt-5.6-luna', color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400' },
|
||||
|
||||
Reference in New Issue
Block a user