Merge remote-tracking branch 'origin/main' into v2/localmain

This commit is contained in:
t0ng7u
2026-04-28 13:35:19 +08:00
4 changed files with 132 additions and 12 deletions
+10 -2
View File
@@ -252,8 +252,16 @@ func updateConfigFromMap(config interface{}, configMap map[string]string) error
continue
}
}
case reflect.Map, reflect.Slice, reflect.Struct:
// 复杂类型使用JSON反序列化
case reflect.Map:
// json.Unmarshal merges into existing maps (keeps old keys that are
// absent from the new JSON). Allocate a fresh map so removed keys
// are properly cleared.
fresh := reflect.New(field.Type())
if err := json.Unmarshal([]byte(strValue), fresh.Interface()); err != nil {
continue
}
field.Set(fresh.Elem())
case reflect.Slice, reflect.Struct:
err := json.Unmarshal([]byte(strValue), field.Addr().Interface())
if err != nil {
continue
+96
View File
@@ -0,0 +1,96 @@
package config
import (
"testing"
)
type testConfigWithMap struct {
Modes map[string]string `json:"modes"`
Exprs map[string]string `json:"exprs"`
Name string `json:"name"`
}
func TestUpdateConfigFromMap_MapReplacement(t *testing.T) {
cfg := &testConfigWithMap{
Modes: map[string]string{
"model-a": "tiered_expr",
"model-b": "tiered_expr",
},
Exprs: map[string]string{
"model-a": "p * 5 + c * 25",
"model-b": "p * 10 + c * 50",
},
Name: "billing",
}
// Simulate removing model-a: new value only has model-b
err := UpdateConfigFromMap(cfg, map[string]string{
"modes": `{"model-b": "tiered_expr"}`,
"exprs": `{"model-b": "p * 10 + c * 50"}`,
})
if err != nil {
t.Fatalf("UpdateConfigFromMap failed: %v", err)
}
if _, ok := cfg.Modes["model-a"]; ok {
t.Errorf("Modes still contains model-a after it was removed from the update; got %v", cfg.Modes)
}
if _, ok := cfg.Exprs["model-a"]; ok {
t.Errorf("Exprs still contains model-a after it was removed from the update; got %v", cfg.Exprs)
}
if cfg.Modes["model-b"] != "tiered_expr" {
t.Errorf("Modes[model-b] = %q, want %q", cfg.Modes["model-b"], "tiered_expr")
}
if cfg.Exprs["model-b"] != "p * 10 + c * 50" {
t.Errorf("Exprs[model-b] = %q, want %q", cfg.Exprs["model-b"], "p * 10 + c * 50")
}
}
func TestUpdateConfigFromMap_EmptyMapClearsAll(t *testing.T) {
cfg := &testConfigWithMap{
Modes: map[string]string{
"model-a": "tiered_expr",
},
Exprs: map[string]string{
"model-a": "p * 5 + c * 25",
},
}
err := UpdateConfigFromMap(cfg, map[string]string{
"modes": `{}`,
"exprs": `{}`,
})
if err != nil {
t.Fatalf("UpdateConfigFromMap failed: %v", err)
}
if len(cfg.Modes) != 0 {
t.Errorf("Modes should be empty after updating with {}, got %v", cfg.Modes)
}
if len(cfg.Exprs) != 0 {
t.Errorf("Exprs should be empty after updating with {}, got %v", cfg.Exprs)
}
}
func TestUpdateConfigFromMap_ScalarFieldsUnchanged(t *testing.T) {
cfg := &testConfigWithMap{
Modes: map[string]string{"m": "v"},
Name: "old",
}
err := UpdateConfigFromMap(cfg, map[string]string{
"name": "new",
})
if err != nil {
t.Fatalf("UpdateConfigFromMap failed: %v", err)
}
if cfg.Name != "new" {
t.Errorf("Name = %q, want %q", cfg.Name, "new")
}
// modes was not in configMap, should remain unchanged
if cfg.Modes["m"] != "v" {
t.Errorf("Modes should be unchanged, got %v", cfg.Modes)
}
}
+11 -2
View File
@@ -2293,7 +2293,12 @@ export function renderTieredModelPrice(opts) {
const { symbol, rate } = getCurrencyConfig();
const gr = groupRatio || 1;
const priceLines = BILLING_PRICING_VARS.map((v) => [v.field, v.label]);
const hasAnyCacheTokens = cacheTokens > 0 || cacheCreationTokens > 0
|| cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0;
const priceLines = BILLING_PRICING_VARS
.filter((v) => v.group !== 'cache' || hasAnyCacheTokens)
.map((v) => [v.field, v.label]);
const lines = [
buildBillingText('命中档位:{{tier}}', { tier: matchedTier || tier.label }),
@@ -2334,7 +2339,11 @@ export function renderTieredModelPriceSimple(opts) {
];
if (tier && isPriceDisplayMode(displayMode)) {
const priceSegments = BILLING_PRICING_VARS.map((v) => [v.field, v.shortLabel]);
const hasAnyCacheTokens = cacheTokens > 0 || cacheCreationTokens > 0
|| cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0;
const priceSegments = BILLING_PRICING_VARS
.filter((v) => v.group !== 'cache' || hasAnyCacheTokens)
.map((v) => [v.field, v.shortLabel]);
for (const [field, label] of priceSegments) {
if (tier[field] > 0) {
segments.push({
@@ -1050,16 +1050,23 @@ export function useModelPricingEditorState({
tieredOutput['billing_setting.billing_expr'][model.name] = finalBillingExpr;
}
}
if (model.billingMode === 'tiered_expr') {
continue;
}
const serialized = serializeModel(model, t);
Object.entries(serialized).forEach(([key, value]) => {
if (value !== null) {
output[key][model.name] = value;
// Always serialize ratio/price values for all models (including
// tiered_expr) so they serve as fallback during multi-instance sync
// delay. ModelPriceHelper checks billing_mode first, so these values
// are only used when billing_setting hasn't propagated yet.
try {
const serialized = serializeModel(model, t);
Object.entries(serialized).forEach(([key, value]) => {
if (value !== null) {
output[key][model.name] = value;
}
});
} catch (e) {
if (model.billingMode !== 'tiered_expr') {
throw e;
}
});
}
}
const requestQueue = [