Skip to content

Commit a25bedb

Browse files
authored
Merge pull request #115 from DEEIX-AI/imagine_and_tools
feat: enhance image generation and tools use
2 parents 6901859 + fff74ba commit a25bedb

59 files changed

Lines changed: 2878 additions & 1132 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/docs/docs.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9586,6 +9586,12 @@ const docTemplate = `{
95869586
"nativeToolBillingEnabled": {
95879587
"type": "boolean"
95889588
},
9589+
"nativeToolPricing": {
9590+
"type": "array",
9591+
"items": {
9592+
"$ref": "#/definitions/internal_transport_http_billing.NativeToolPricingRequest"
9593+
}
9594+
},
95899595
"prepaidAmountUSD": {
95909596
"type": "number",
95919597
"minimum": 0
@@ -10071,6 +10077,26 @@ const docTemplate = `{
1007110077
}
1007210078
}
1007310079
},
10080+
"internal_transport_http_billing.NativeToolPricingRequest": {
10081+
"type": "object",
10082+
"properties": {
10083+
"billable": {
10084+
"type": "boolean"
10085+
},
10086+
"priceLabel": {
10087+
"type": "string"
10088+
},
10089+
"priceNanousd": {
10090+
"type": "integer"
10091+
},
10092+
"toolKey": {
10093+
"type": "string"
10094+
},
10095+
"unit": {
10096+
"type": "string"
10097+
}
10098+
}
10099+
},
1007410100
"internal_transport_http_billing.NativeToolPricingResponse": {
1007510101
"type": "object",
1007610102
"properties": {

backend/docs/swagger.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9579,6 +9579,12 @@
95799579
"nativeToolBillingEnabled": {
95809580
"type": "boolean"
95819581
},
9582+
"nativeToolPricing": {
9583+
"type": "array",
9584+
"items": {
9585+
"$ref": "#/definitions/internal_transport_http_billing.NativeToolPricingRequest"
9586+
}
9587+
},
95829588
"prepaidAmountUSD": {
95839589
"type": "number",
95849590
"minimum": 0
@@ -10064,6 +10070,26 @@
1006410070
}
1006510071
}
1006610072
},
10073+
"internal_transport_http_billing.NativeToolPricingRequest": {
10074+
"type": "object",
10075+
"properties": {
10076+
"billable": {
10077+
"type": "boolean"
10078+
},
10079+
"priceLabel": {
10080+
"type": "string"
10081+
},
10082+
"priceNanousd": {
10083+
"type": "integer"
10084+
},
10085+
"toolKey": {
10086+
"type": "string"
10087+
},
10088+
"unit": {
10089+
"type": "string"
10090+
}
10091+
}
10092+
},
1006710093
"internal_transport_http_billing.NativeToolPricingResponse": {
1006810094
"type": "object",
1006910095
"properties": {

backend/docs/swagger.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,6 +1216,10 @@ definitions:
12161216
type: string
12171217
nativeToolBillingEnabled:
12181218
type: boolean
1219+
nativeToolPricing:
1220+
items:
1221+
$ref: '#/definitions/internal_transport_http_billing.NativeToolPricingRequest'
1222+
type: array
12191223
prepaidAmountUSD:
12201224
minimum: 0
12211225
type: number
@@ -1543,6 +1547,19 @@ definitions:
15431547
updatedAt:
15441548
type: string
15451549
type: object
1550+
internal_transport_http_billing.NativeToolPricingRequest:
1551+
properties:
1552+
billable:
1553+
type: boolean
1554+
priceLabel:
1555+
type: string
1556+
priceNanousd:
1557+
type: integer
1558+
toolKey:
1559+
type: string
1560+
unit:
1561+
type: string
1562+
type: object
15461563
internal_transport_http_billing.NativeToolPricingResponse:
15471564
properties:
15481565
billable:

backend/internal/application/billing/service.go

Lines changed: 53 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,16 @@ import (
1616

1717
domainbilling "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/domain/billing"
1818
"github.com/DEEIX-AI/DEEIX-Chat/backend/internal/repository"
19+
"github.com/DEEIX-AI/DEEIX-Chat/backend/internal/shared/nativetool"
1920
)
2021

2122
const (
2223
defaultPageSize = 20
2324
maxPageSize = 200
2425
publicModelPricingCacheTTL = 30 * time.Second
2526
nativeToolPricingSource = "provider_official_defaults"
26-
nativeToolUSD001Nanousd = 10_000_000
27-
nativeToolUSD0025Nanousd = 25_000_000
28-
nativeToolUSD0005Nanousd = 5_000_000
29-
nativeToolUSD00025Nanousd = 2_500_000
3027
)
3128

32-
// nativeToolCallPrice 描述可直接折算为按次计费的模型原生工具默认价格。
33-
type nativeToolCallPrice struct {
34-
provider string
35-
serviceName string
36-
nanousdPerCall int64
37-
}
38-
3929
// UserSubscriptionSnapshot 描述用户当前订阅的派生结果。
4030
type UserSubscriptionSnapshot struct {
4131
UserID uint
@@ -298,23 +288,27 @@ func (s *Service) GetBillingMode(ctx context.Context) (string, error) {
298288

299289
// ListNativeToolDefaultPricing 返回当前内置的原生工具默认价格目录。
300290
func ListNativeToolDefaultPricing() []NativeToolPricingView {
301-
return []NativeToolPricingView{
302-
{Provider: "OpenAI", ToolKey: "openaiWebSearchReasoning", PriceNanousd: nativeToolUSD001Nanousd, Unit: "call", Billable: true},
303-
{Provider: "OpenAI", ToolKey: "openaiWebSearchStandard", PriceNanousd: nativeToolUSD0025Nanousd, Unit: "call", Billable: true},
304-
{Provider: "OpenAI", ToolKey: "openaiShell", PriceLabel: "notMetered", Billable: false},
305-
{Provider: "OpenAI", ToolKey: "openaiImageGeneration", PriceLabel: "notMetered", Billable: false},
306-
{Provider: "OpenAI", ToolKey: "openaiCodeInterpreter", PriceLabel: "notMetered", Billable: false},
307-
{Provider: "Anthropic", ToolKey: "anthropicWebSearch", PriceNanousd: nativeToolUSD001Nanousd, Unit: "search", Billable: true},
308-
{Provider: "Anthropic", ToolKey: "anthropicWebFetch", PriceLabel: "included", Billable: false},
309-
{Provider: "Anthropic", ToolKey: "anthropicCodeExecution", PriceLabel: "notMetered", Billable: false},
310-
{Provider: "Anthropic", ToolKey: "anthropicAdvisor", PriceLabel: "notMetered", Billable: false},
311-
{Provider: "Anthropic", ToolKey: "anthropicToolSearch", PriceLabel: "included", Billable: false},
312-
{Provider: "xAI", ToolKey: "xaiWebSearch", PriceNanousd: nativeToolUSD0005Nanousd, Unit: "call", Billable: true},
313-
{Provider: "xAI", ToolKey: "xaiXSearch", PriceNanousd: nativeToolUSD0005Nanousd, Unit: "call", Billable: true},
314-
{Provider: "xAI", ToolKey: "xaiCodeExecution", PriceNanousd: nativeToolUSD0005Nanousd, Unit: "call", Billable: true},
315-
{Provider: "xAI", ToolKey: "xaiAttachmentSearch", PriceNanousd: nativeToolUSD001Nanousd, Unit: "call", Billable: true},
316-
{Provider: "xAI", ToolKey: "xaiCollectionsSearch", PriceNanousd: nativeToolUSD00025Nanousd, Unit: "call", Billable: true},
291+
return nativeToolPricingViews(nativetool.PricingDefinitions())
292+
}
293+
294+
// ListNativeToolPricing 返回应用管理员覆盖后的原生工具计费价格目录。
295+
func ListNativeToolPricing(rawPricingJSON string) []NativeToolPricingView {
296+
return nativeToolPricingViews(nativetool.PricingDefinitionsWithOverrides(rawPricingJSON))
297+
}
298+
299+
func nativeToolPricingViews(items []nativetool.PricingDefinition) []NativeToolPricingView {
300+
results := make([]NativeToolPricingView, 0, len(items))
301+
for _, item := range items {
302+
results = append(results, NativeToolPricingView{
303+
Provider: item.Provider,
304+
ToolKey: item.ToolKey,
305+
PriceNanousd: item.PriceNanousd,
306+
Unit: item.Unit,
307+
PriceLabel: item.PriceLabel,
308+
Billable: item.Billable,
309+
})
317310
}
311+
return results
318312
}
319313

320314
// ListBillingAccountSnapshots 批量查询用户按量余额。
@@ -1400,7 +1394,15 @@ func (s *Service) BuildUsageLedger(ctx context.Context, input UsagePricingInput)
14001394
if err != nil {
14011395
return nil, err
14021396
}
1403-
nativeToolItems, nativeToolBilledNanousd := buildNativeToolServiceItems(input, mode, isFreeModel, nativeToolBillingEnabled)
1397+
nativeToolPricingJSON, err := s.repo.GetNativeToolPricingJSON(ctx)
1398+
if err != nil {
1399+
return nil, err
1400+
}
1401+
nativeToolPricingOverrides, err := nativetool.ParsePricingOverridesJSON(nativeToolPricingJSON)
1402+
if err != nil {
1403+
nativeToolPricingOverrides = map[string]nativetool.PricingOverride{}
1404+
}
1405+
nativeToolItems, nativeToolBilledNanousd := buildNativeToolServiceItems(input, mode, isFreeModel, nativeToolBillingEnabled, nativeToolPricingOverrides)
14041406
if len(nativeToolItems) > 0 {
14051407
serviceItems = append(serviceItems, nativeToolItems...)
14061408
serviceBilledNanousd += nativeToolBilledNanousd
@@ -1460,7 +1462,7 @@ func (s *Service) BuildUsageLedger(ctx context.Context, input UsagePricingInput)
14601462
"duration_billed_nanousd": durationBilledNanousd,
14611463
"server_side_tool_usage": normalizeUsageCountMap(input.ServerSideToolUsage),
14621464
"native_tool_billing_enabled": nativeToolBillingEnabled,
1463-
"native_tool_pricing_source": nativeToolPricingSource,
1465+
"native_tool_pricing_source": nativeToolPricingSourceForSnapshot(nativeToolPricingJSON),
14641466
"native_tool_billed_nanousd": nativeToolBilledNanousd,
14651467
"base_service_billed_nanousd": serviceBilledNanousd,
14661468
"service_items": usageServiceItemSnapshots(serviceItems),
@@ -2684,7 +2686,7 @@ func paginateModelPricing(items []domainbilling.ModelPricing, offset int, limit
26842686
}
26852687

26862688
// buildNativeToolServiceItems 将原生 server-side tool 调用转换为账单服务项。
2687-
func buildNativeToolServiceItems(input UsagePricingInput, billingMode string, isFreeModel bool, enabled bool) ([]domainbilling.UsageServiceItem, int64) {
2689+
func buildNativeToolServiceItems(input UsagePricingInput, billingMode string, isFreeModel bool, enabled bool, pricingOverrides map[string]nativetool.PricingOverride) ([]domainbilling.UsageServiceItem, int64) {
26882690
if billingMode == "self" || isFreeModel || !enabled || len(input.ServerSideToolUsage) == 0 {
26892691
return []domainbilling.UsageServiceItem{}, 0
26902692
}
@@ -2695,20 +2697,20 @@ func buildNativeToolServiceItems(input UsagePricingInput, billingMode string, is
26952697
results := make([]domainbilling.UsageServiceItem, 0, len(counts))
26962698
var total int64
26972699
for toolName, count := range counts {
2698-
price, ok := nativeToolDefaultCallPrice(input, toolName)
2699-
if !ok || price.nanousdPerCall <= 0 || count <= 0 {
2700+
price, ok := nativeToolDefaultCallPrice(input, toolName, pricingOverrides)
2701+
if !ok || price.NanousdPerCall <= 0 || count <= 0 {
27002702
continue
27012703
}
2702-
billed := count * price.nanousdPerCall
2704+
billed := count * price.NanousdPerCall
27032705
results = append(results, domainbilling.UsageServiceItem{
2704-
ServiceCode: nativeToolServiceCode(price.provider, toolName),
2705-
ServiceName: price.serviceName,
2706+
ServiceCode: nativeToolServiceCode(price.Provider, toolName),
2707+
ServiceName: price.ServiceName,
27062708
PlatformModelName: strings.TrimSpace(input.PlatformModelName),
27072709
ProviderProtocol: strings.TrimSpace(input.ProviderProtocol),
27082710
RateMultiplier: 1,
27092711
PricingMode: domainbilling.PricingModeCall,
27102712
CallCount: count,
2711-
CallNanousdPerCall: price.nanousdPerCall,
2713+
CallNanousdPerCall: price.NanousdPerCall,
27122714
CallBilledNanousd: billed,
27132715
BilledNanousd: billed,
27142716
})
@@ -2718,44 +2720,22 @@ func buildNativeToolServiceItems(input UsagePricingInput, billingMode string, is
27182720
}
27192721

27202722
// nativeToolDefaultCallPrice 返回当前已适配厂商原生工具的官方默认按次价格。
2721-
func nativeToolDefaultCallPrice(input UsagePricingInput, toolName string) (nativeToolCallPrice, bool) {
2722-
tool := strings.TrimSpace(toolName)
2723-
switch strings.TrimSpace(input.ProviderProtocol) {
2724-
case "anthropic_messages":
2725-
switch tool {
2726-
case "web_search":
2727-
return nativeToolCallPrice{provider: "anthropic", serviceName: "Anthropic Web search", nanousdPerCall: nativeToolUSD001Nanousd}, true
2728-
default:
2729-
return nativeToolCallPrice{}, false
2730-
}
2731-
case "openai_responses", "openai_chat_completions":
2732-
switch tool {
2733-
case "web_search", "web_search_preview":
2734-
if isOpenAIWebSearchReasoningModel(input) {
2735-
return nativeToolCallPrice{provider: "openai", serviceName: "OpenAI Web search", nanousdPerCall: nativeToolUSD001Nanousd}, true
2736-
}
2737-
return nativeToolCallPrice{provider: "openai", serviceName: "OpenAI Web search", nanousdPerCall: nativeToolUSD0025Nanousd}, true
2738-
default:
2739-
return nativeToolCallPrice{}, false
2740-
}
2741-
case "xai_responses":
2742-
switch tool {
2743-
case "web_search":
2744-
return nativeToolCallPrice{provider: "xai", serviceName: "xAI Web Search", nanousdPerCall: nativeToolUSD0005Nanousd}, true
2745-
case "x_search":
2746-
return nativeToolCallPrice{provider: "xai", serviceName: "xAI X Search", nanousdPerCall: nativeToolUSD0005Nanousd}, true
2747-
case "code_interpreter", "code_execution":
2748-
return nativeToolCallPrice{provider: "xai", serviceName: "xAI Code Execution", nanousdPerCall: nativeToolUSD0005Nanousd}, true
2749-
case "attachment_search", "file_attachment_search":
2750-
return nativeToolCallPrice{provider: "xai", serviceName: "xAI File Attachments Search", nanousdPerCall: nativeToolUSD001Nanousd}, true
2751-
case "file_search", "collection_search", "collections_search":
2752-
return nativeToolCallPrice{provider: "xai", serviceName: "xAI Collections Search / RAG", nanousdPerCall: nativeToolUSD00025Nanousd}, true
2753-
default:
2754-
return nativeToolCallPrice{}, false
2755-
}
2756-
default:
2757-
return nativeToolCallPrice{}, false
2723+
func nativeToolDefaultCallPrice(input UsagePricingInput, toolName string, pricingOverrides map[string]nativetool.PricingOverride) (nativetool.UsagePrice, bool) {
2724+
key, ok := nativetool.UsagePricingKey(input.ProviderProtocol, toolName)
2725+
if !ok {
2726+
return nativetool.UsagePrice{}, false
2727+
}
2728+
if key == "openaiWebSearchStandard" && isOpenAIWebSearchReasoningModel(input) {
2729+
key = "openaiWebSearchReasoning"
2730+
}
2731+
return nativetool.UsagePriceByKeyWithOverrides(key, pricingOverrides)
2732+
}
2733+
2734+
func nativeToolPricingSourceForSnapshot(raw string) string {
2735+
if nativetool.PricingOverridesUseDefaults(raw) {
2736+
return nativeToolPricingSource
27582737
}
2738+
return "admin_configured"
27592739
}
27602740

27612741
// isOpenAIWebSearchReasoningModel 区分 OpenAI Web Search 的推理与非推理模型价格。

backend/internal/application/billing/service_model_identity_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type billingRepositoryStub struct {
2626
prices []domainbilling.Price
2727
subscriptions []domainbilling.Subscription
2828
nativeToolBillingEnabled bool
29+
nativeToolPricingJSON string
2930
requestedPlatformModelName string
3031
replacedSubscription *domainbilling.Subscription
3132
}
@@ -42,6 +43,10 @@ func (r *billingRepositoryStub) GetNativeToolBillingEnabled(context.Context) (bo
4243
return r.nativeToolBillingEnabled, nil
4344
}
4445

46+
func (r *billingRepositoryStub) GetNativeToolPricingJSON(context.Context) (string, error) {
47+
return r.nativeToolPricingJSON, nil
48+
}
49+
4550
func (r *billingRepositoryStub) GetModelPricing(_ context.Context, platformModelName string) (*domainbilling.ModelPricing, error) {
4651
r.requestedPlatformModelName = platformModelName
4752
if r.pricing == nil {
@@ -343,6 +348,43 @@ func TestBuildUsageLedgerBillsNativeToolDefaultsWhenEnabled(t *testing.T) {
343348
}
344349
}
345350

351+
func TestBuildUsageLedgerUsesNativeToolPricingOverrides(t *testing.T) {
352+
repo := &billingRepositoryStub{
353+
mode: "usage",
354+
nativeToolBillingEnabled: true,
355+
nativeToolPricingJSON: `{"xaiWebSearch":{"priceNanousd":123000000,"unit":"call","priceLabel":"","billable":true}}`,
356+
pricing: &domainbilling.ModelPricing{
357+
PlatformModelName: "grok-4.3",
358+
Currency: "USD",
359+
PricingMode: domainbilling.PricingModeToken,
360+
},
361+
}
362+
service := NewService(repo)
363+
364+
ledger, err := service.BuildUsageLedger(context.Background(), UsagePricingInput{
365+
UserID: 1,
366+
PlatformModelName: "grok-4.3",
367+
ProviderProtocol: "xai_responses",
368+
ServerSideToolUsage: map[string]int64{
369+
"web_search": 2,
370+
},
371+
})
372+
if err != nil {
373+
t.Fatalf("build usage ledger: %v", err)
374+
}
375+
if ledger.BilledNanousd != 246_000_000 {
376+
t.Fatalf("expected native tool override billing total, got %d", ledger.BilledNanousd)
377+
}
378+
379+
var snapshot map[string]interface{}
380+
if err := json.Unmarshal([]byte(ledger.PricingSnapshotJSON), &snapshot); err != nil {
381+
t.Fatalf("unmarshal pricing snapshot: %v", err)
382+
}
383+
if snapshot["native_tool_pricing_source"] != "admin_configured" {
384+
t.Fatalf("expected admin native tool pricing source, got %#v", snapshot["native_tool_pricing_source"])
385+
}
386+
}
387+
346388
func TestBuildUsageLedgerBillsOpenAIWebSearchPreviewByModelFamily(t *testing.T) {
347389
cases := []struct {
348390
name string

backend/internal/application/channel/model_catalog.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,8 @@ func isGeminiImageGenerationModel(code string) bool {
460460
switch strings.TrimSpace(strings.ToLower(code)) {
461461
case "nano-banana", "nano-banana-2", "nano-banana-pro",
462462
"gemini-2.5-flash-image",
463+
"gemini-3.1-flash-image",
464+
"gemini-3-pro-image",
463465
"gemini-3.1-flash-image-preview",
464466
"gemini-3-pro-image-preview":
465467
return true

0 commit comments

Comments
 (0)