修复 SpacetimeDB 定价 record 缺少历史模型回填的问题
- 将 backfill_legacy_gpt_image_2_5_pricing 提升为 pub(crate) 并补充文档说明其覆盖磁盘 override 与 SpacetimeDB record 两条加载路径 - editor_generation_pricing_from_record 在 validate 前先对旧 record 做受控 backfill,避免升级前只写单个 gpt-image-2 key 时被必填校验拒绝 - 新增用例覆盖旧 record 自动补齐两个 GPT Image 2.5 key,以及显式配置不被回填覆盖 - 更新兼容回填的 TODO 注释,说明移除条件还需包含 SpacetimeDB record
This commit is contained in:
@@ -393,7 +393,11 @@ fn load_editor_generation_pricing_from_candidates(
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn backfill_legacy_gpt_image_2_5_pricing(
|
||||
/// 兼容升级前只含单个历史 `gpt-image-2` key 的持久化定价配置。
|
||||
///
|
||||
/// 磁盘 override 与 SpacetimeDB record 两条加载路径都要调用,补齐缺失的
|
||||
/// `gpt-image-2.5-flare-c` / `gpt-image-2.5-sunburst-c` 后才允许 `validate()`。
|
||||
pub(crate) fn backfill_legacy_gpt_image_2_5_pricing(
|
||||
config: &mut EditorGenerationPricingConfig,
|
||||
fallback: &EditorGenerationPricingConfig,
|
||||
source: &str,
|
||||
@@ -408,7 +412,8 @@ fn backfill_legacy_gpt_image_2_5_pricing(
|
||||
return Ok(());
|
||||
}
|
||||
// TODO: compatibility backfill for legacy single-key pricing; remove once
|
||||
// all persisted overrides contain the two explicit GPT Image 2.5 keys.
|
||||
// every persisted override and SpacetimeDB record contains the two explicit
|
||||
// GPT Image 2.5 keys.
|
||||
let pricing = config
|
||||
.models
|
||||
.get(EDITOR_IMAGE_MODEL_GPT_IMAGE_2)
|
||||
|
||||
@@ -495,7 +495,14 @@ fn editor_generation_pricing_from_record(
|
||||
})?;
|
||||
models.insert(EDITOR_SOUND_EFFECT_MODEL_ELEVENLABS.to_string(), pricing);
|
||||
}
|
||||
let config = EditorGenerationPricingConfig { models };
|
||||
let mut config = EditorGenerationPricingConfig { models };
|
||||
// 中文注释:升级前写入的 record 可能只有单个历史 gpt-image-2 key,
|
||||
// 与磁盘 override 一样先做受控 backfill,再进入必填校验。
|
||||
crate::editor_generation_config::backfill_legacy_gpt_image_2_5_pricing(
|
||||
&mut config,
|
||||
legacy_fallback,
|
||||
"SpacetimeDB 模型定价配置",
|
||||
)?;
|
||||
config.validate()?;
|
||||
Ok(config)
|
||||
}
|
||||
@@ -2875,6 +2882,138 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_generation_pricing_typed_record_backfills_legacy_gpt_image_keys() {
|
||||
use crate::editor_generation_config::{
|
||||
EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT, EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION,
|
||||
};
|
||||
|
||||
let fallback = crate::editor_generation_config::parse_editor_generation_pricing_json(
|
||||
crate::editor_generation_config::EDITOR_GENERATION_PRICING_DEFAULT_JSON,
|
||||
"test default pricing",
|
||||
)
|
||||
.expect("default pricing should parse");
|
||||
let mut models =
|
||||
editor_generation_pricing_to_records(&fallback).expect("pricing should map to records");
|
||||
// 中文注释:模拟升级前持久化的旧 record:只有单个历史 gpt-image-2 key。
|
||||
models.retain(|pricing| {
|
||||
pricing.model != EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION
|
||||
&& pricing.model != EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT
|
||||
});
|
||||
let legacy_pricing = models
|
||||
.iter()
|
||||
.find(|pricing| pricing.model == "gpt-image-2")
|
||||
.cloned()
|
||||
.expect("legacy record kept the single gpt-image-2 key");
|
||||
let record = EditorGenerationPricingConfigRecord {
|
||||
config_id: "global".to_string(),
|
||||
models,
|
||||
updated_by_admin_user_id: Some("admin:test".to_string()),
|
||||
updated_at: "2026-09-18T00:00:00Z".to_string(),
|
||||
updated_at_micros: 2,
|
||||
};
|
||||
|
||||
let actual = editor_generation_pricing_from_record(record, &fallback)
|
||||
.expect("legacy single-key record should be backfilled instead of rejected");
|
||||
|
||||
assert_eq!(legacy_pricing.unit.as_str(), "perGeneration");
|
||||
let expected = EditorGenerationModelPricing {
|
||||
unit: EditorGenerationPricingUnit::PerGeneration,
|
||||
price: legacy_pricing.price,
|
||||
prices: legacy_pricing
|
||||
.prices
|
||||
.iter()
|
||||
.map(|tier| (tier.key.clone(), tier.price))
|
||||
.collect(),
|
||||
};
|
||||
assert_eq!(
|
||||
actual
|
||||
.models
|
||||
.get(EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION),
|
||||
Some(&expected)
|
||||
);
|
||||
assert_eq!(
|
||||
actual.models.get(EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT),
|
||||
Some(&expected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_generation_pricing_typed_record_keeps_explicit_gpt_image_keys() {
|
||||
use crate::editor_generation_config::{
|
||||
EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT, EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION,
|
||||
};
|
||||
|
||||
let fallback = crate::editor_generation_config::parse_editor_generation_pricing_json(
|
||||
crate::editor_generation_config::EDITOR_GENERATION_PRICING_DEFAULT_JSON,
|
||||
"test default pricing",
|
||||
)
|
||||
.expect("default pricing should parse");
|
||||
let mut models =
|
||||
editor_generation_pricing_to_records(&fallback).expect("pricing should map to records");
|
||||
for pricing in models.iter_mut() {
|
||||
if pricing.model == EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION {
|
||||
pricing.prices = vec![
|
||||
EditorGenerationPricingTierRecord {
|
||||
key: "1K".to_string(),
|
||||
price: 71,
|
||||
},
|
||||
EditorGenerationPricingTierRecord {
|
||||
key: "2K".to_string(),
|
||||
price: 72,
|
||||
},
|
||||
];
|
||||
}
|
||||
if pricing.model == EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT {
|
||||
pricing.prices = vec![
|
||||
EditorGenerationPricingTierRecord {
|
||||
key: "1K".to_string(),
|
||||
price: 81,
|
||||
},
|
||||
EditorGenerationPricingTierRecord {
|
||||
key: "2K".to_string(),
|
||||
price: 82,
|
||||
},
|
||||
];
|
||||
}
|
||||
if pricing.model == "gpt-image-2" {
|
||||
pricing.prices = vec![
|
||||
EditorGenerationPricingTierRecord {
|
||||
key: "1K".to_string(),
|
||||
price: 1,
|
||||
},
|
||||
EditorGenerationPricingTierRecord {
|
||||
key: "2K".to_string(),
|
||||
price: 2,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
let record = EditorGenerationPricingConfigRecord {
|
||||
config_id: "global".to_string(),
|
||||
models,
|
||||
updated_by_admin_user_id: Some("admin:test".to_string()),
|
||||
updated_at: "2026-09-18T00:00:00Z".to_string(),
|
||||
updated_at_micros: 3,
|
||||
};
|
||||
|
||||
let actual = editor_generation_pricing_from_record(record, &fallback)
|
||||
.expect("explicit GPT Image 2.5 keys should be preserved");
|
||||
|
||||
let generation = actual
|
||||
.models
|
||||
.get(EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION)
|
||||
.expect("generation key should exist");
|
||||
assert_eq!(generation.prices.get("1K"), Some(&71));
|
||||
assert_eq!(generation.prices.get("2K"), Some(&72));
|
||||
let edit = actual
|
||||
.models
|
||||
.get(EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT)
|
||||
.expect("edit key should exist");
|
||||
assert_eq!(edit.prices.get("1K"), Some(&81));
|
||||
assert_eq!(edit.prices.get("2K"), Some(&82));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_generation_pricing_upsert_input_uses_runtime_service_bootstrap_secret() {
|
||||
let mut config = AppConfig::default();
|
||||
|
||||
Reference in New Issue
Block a user