feat(billing): Codex alpha/search 网页搜索按次计费

- alpha/search 成功请求(上游 2xx)按次计费落 usage_logs(billing_mode=per_request),
  上游错误透传/failover 不计费;使用 mandatory 池提交,池满同步兜底不丢扣费
- 单价默认 0.01 USD/次(官方 $10/1000 次),分组新增 web_search_price_per_call
  覆盖价(0=免费,负数/留空=默认价),实际扣费叠加分组费率倍数(与 token 口径一致)
- 分组字段全链路:ent schema + 迁移 174 + 端口快照 v15 + admin 创建/更新 + DTO
- 前端分组表单(openai 平台)新增单次价格配置,实时预览应用当前倍率后的单次价格
- 该端点鉴权维持仅 OpenAI 分组(非 OpenAI 分组 404)
This commit is contained in:
shaw committed 2026-07-13 09:54:51 +08:00
1 parent 42f3c22830
commit 7cbb36f278
34 files changed
+789 -33

No files matched your search

+104
View File
@@ -1315,6 +1315,41 @@
</div>
</div>
<!-- Codex 网页搜索按次计费(仅 openai 平台) -->
<div
v-if="createForm.platform === 'openai'"
class="border-t border-gray-200 dark:border-dark-400 pt-4 mt-4"
>
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
{{ t("admin.groups.webSearchPricing.title") }}
</h4>
<div>
<label class="input-label">{{
t("admin.groups.webSearchPricing.pricePerCall")
}}</label>
<input
v-model.number="createForm.web_search_price_per_call"
type="number"
step="0.001"
min="0"
placeholder="0.01"
class="input"
/>
<p class="input-hint">
{{ t("admin.groups.webSearchPricing.pricePerCallHint") }}
</p>
<div
class="mt-2 rounded-lg bg-gray-50 p-3 text-xs text-gray-600 dark:bg-dark-700 dark:text-gray-300"
>
{{
t("admin.groups.webSearchPricing.finalPricePreview", {
price: createWebSearchFinalPricePreview,
})
}}
</div>
</div>
</div>
<!-- OpenAI Messages 调度配置(仅 openai 平台) -->
<div
v-if="createForm.platform === 'openai'"
@@ -2790,6 +2825,41 @@
</div>
</div>
<!-- Codex 网页搜索按次计费(仅 openai 平台) -->
<div
v-if="editForm.platform === 'openai'"
class="border-t border-gray-200 dark:border-dark-400 pt-4 mt-4"
>
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
{{ t("admin.groups.webSearchPricing.title") }}
</h4>
<div>
<label class="input-label">{{
t("admin.groups.webSearchPricing.pricePerCall")
}}</label>
<input
v-model.number="editForm.web_search_price_per_call"
type="number"
step="0.001"
min="0"
placeholder="0.01"
class="input"
/>
<p class="input-hint">
{{ t("admin.groups.webSearchPricing.pricePerCallHint") }}
</p>
<div
class="mt-2 rounded-lg bg-gray-50 p-3 text-xs text-gray-600 dark:bg-dark-700 dark:text-gray-300"
>
{{
t("admin.groups.webSearchPricing.finalPricePreview", {
price: editWebSearchFinalPricePreview,
})
}}
</div>
</div>
</div>
<!-- OpenAI Messages 调度配置(仅 openai 平台) -->
<div
v-if="editForm.platform === 'openai'"
@@ -3888,6 +3958,8 @@ const createForm = reactive({
video_price_480p: null as number | null,
video_price_720p: null as number | null,
video_price_1080p: null as number | null,
// Codex 网页搜索按次计费(仅 openai 平台使用);null = 使用默认价 0.01
web_search_price_per_call: null as number | null,
// 高峰时段倍率配置
peak_rate_enabled: false,
peak_start: "",
@@ -4233,6 +4305,8 @@ const editForm = reactive({
video_price_480p: null as number | null,
video_price_720p: null as number | null,
video_price_1080p: null as number | null,
// Codex 网页搜索按次计费(仅 openai 平台使用);null = 使用默认价 0.01
web_search_price_per_call: null as number | null,
// 高峰时段倍率配置
peak_rate_enabled: false,
peak_start: "",
@@ -4390,6 +4464,27 @@ const editVideoFinalPricePreview = computed(() =>
buildVideoFinalPricePreview(editForm),
);
// Codex 网页搜索单次默认价(与后端 defaultWebSearchPricePerCall 一致,官方 $10/1000 次)
const DEFAULT_WEB_SEARCH_PRICE_PER_CALL = 0.01;
const buildWebSearchFinalPricePreview = (form: {
web_search_price_per_call: number | string | null;
rate_multiplier: number | string | null;
}) => {
const basePrice =
parsePreviewPrice(form.web_search_price_per_call) ??
DEFAULT_WEB_SEARCH_PRICE_PER_CALL;
const multiplier = normalizePreviewNumber(form.rate_multiplier, 1);
return formatImagePricePreview(basePrice * multiplier);
};
const createWebSearchFinalPricePreview = computed(() =>
buildWebSearchFinalPricePreview(createForm),
);
const editWebSearchFinalPricePreview = computed(() =>
buildWebSearchFinalPricePreview(editForm),
);
const resetDisabledBatchImagePricing = (
form: Pick<
ImagePricingFormState,
@@ -4615,6 +4710,7 @@ const closeCreateModal = () => {
createForm.video_price_480p = null;
createForm.video_price_720p = null;
createForm.video_price_1080p = null;
createForm.web_search_price_per_call = null;
createForm.peak_rate_enabled = false;
createForm.peak_start = "";
createForm.peak_end = "";
@@ -4726,6 +4822,9 @@ const handleCreateGroup = async () => {
requestData.video_price_480p = emptyToNull(requestData.video_price_480p);
requestData.video_price_720p = emptyToNull(requestData.video_price_720p);
requestData.video_price_1080p = emptyToNull(requestData.video_price_1080p);
requestData.web_search_price_per_call = emptyToNull(
requestData.web_search_price_per_call,
);
requestData.peak_rate_enabled = createForm.peak_rate_enabled;
requestData.peak_start = createForm.peak_start;
requestData.peak_end = createForm.peak_end;
@@ -4779,6 +4878,7 @@ const handleEdit = async (group: AdminGroup) => {
editForm.video_price_480p = group.video_price_480p;
editForm.video_price_720p = group.video_price_720p;
editForm.video_price_1080p = group.video_price_1080p;
editForm.web_search_price_per_call = group.web_search_price_per_call ?? null;
editForm.peak_rate_enabled = group.peak_rate_enabled ?? false;
editForm.peak_start = group.peak_start ?? "";
editForm.peak_end = group.peak_end ?? "";
@@ -4836,6 +4936,7 @@ const closeEditModal = () => {
editForm.video_price_480p = null;
editForm.video_price_720p = null;
editForm.video_price_1080p = null;
editForm.web_search_price_per_call = null;
resetMessagesDispatchFormState(editForm);
resetModelsListState(editModelsListState);
};
@@ -4914,6 +5015,9 @@ const handleUpdateGroup = async () => {
payload.video_price_480p = emptyPriceToClear(payload.video_price_480p);
payload.video_price_720p = emptyPriceToClear(payload.video_price_720p);
payload.video_price_1080p = emptyPriceToClear(payload.video_price_1080p);
payload.web_search_price_per_call = emptyPriceToClear(
payload.web_search_price_per_call,
);
payload.peak_rate_enabled = editForm.peak_rate_enabled;
payload.peak_start = editForm.peak_start;
payload.peak_end = editForm.peak_end;