mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 03:52:26 +08:00
fix: restrict batch image groups to gemini
This commit is contained in:
@@ -1912,7 +1912,7 @@ func (s *adminServiceImpl) CreateGroup(ctx context.Context, input *CreateGroupIn
|
||||
}
|
||||
|
||||
allowImageGeneration := input.AllowImageGeneration || defaultAllowImageGenerationForPlatform(platform)
|
||||
allowBatchImageGeneration := input.AllowBatchImageGeneration && allowImageGeneration
|
||||
allowBatchImageGeneration := input.AllowBatchImageGeneration && allowImageGeneration && platform == PlatformGemini
|
||||
|
||||
// 如果指定了复制账号的源分组,先获取账号 ID 列表
|
||||
var accountIDsToCopy []int64
|
||||
@@ -2150,7 +2150,7 @@ func (s *adminServiceImpl) UpdateGroup(ctx context.Context, id int64, input *Upd
|
||||
if input.AllowBatchImageGeneration != nil {
|
||||
group.AllowBatchImageGeneration = *input.AllowBatchImageGeneration
|
||||
}
|
||||
if !group.AllowImageGeneration {
|
||||
if !group.AllowImageGeneration || group.Platform != PlatformGemini {
|
||||
group.AllowBatchImageGeneration = false
|
||||
}
|
||||
if input.ImageRateIndependent != nil {
|
||||
|
||||
@@ -252,6 +252,26 @@ func TestAdminService_CreateGroup_DisablesBatchImageWhenImageGenerationDisabled(
|
||||
require.False(t, group.AllowBatchImageGeneration)
|
||||
}
|
||||
|
||||
func TestAdminService_CreateGroup_DisablesBatchImageForNonGeminiPlatform(t *testing.T) {
|
||||
repo := &groupRepoStubForAdmin{}
|
||||
svc := &adminServiceImpl{groupRepo: repo}
|
||||
|
||||
group, err := svc.CreateGroup(context.Background(), &CreateGroupInput{
|
||||
Name: "openai-image",
|
||||
Description: "OpenAI image group",
|
||||
Platform: PlatformOpenAI,
|
||||
RateMultiplier: 1.0,
|
||||
AllowImageGeneration: true,
|
||||
AllowBatchImageGeneration: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, group)
|
||||
require.NotNil(t, repo.created)
|
||||
require.True(t, repo.created.AllowImageGeneration)
|
||||
require.False(t, repo.created.AllowBatchImageGeneration)
|
||||
require.False(t, group.AllowBatchImageGeneration)
|
||||
}
|
||||
|
||||
// TestAdminService_UpdateGroup_WithImagePricing 测试更新分组时 ImagePrice 字段正确更新
|
||||
func TestAdminService_UpdateGroup_WithImagePricing(t *testing.T) {
|
||||
existingGroup := &Group{
|
||||
@@ -370,6 +390,29 @@ func TestAdminService_UpdateGroup_DisablesBatchImageWhenImageGenerationDisabled(
|
||||
require.False(t, group.AllowBatchImageGeneration)
|
||||
}
|
||||
|
||||
func TestAdminService_UpdateGroup_DisablesBatchImageWhenPlatformChangesFromGemini(t *testing.T) {
|
||||
existingGroup := &Group{
|
||||
ID: 1,
|
||||
Name: "existing-gemini",
|
||||
Platform: PlatformGemini,
|
||||
Status: StatusActive,
|
||||
AllowImageGeneration: true,
|
||||
AllowBatchImageGeneration: true,
|
||||
}
|
||||
repo := &groupRepoStubForAdmin{getByID: existingGroup}
|
||||
svc := &adminServiceImpl{groupRepo: repo}
|
||||
|
||||
group, err := svc.UpdateGroup(context.Background(), 1, &UpdateGroupInput{
|
||||
Platform: PlatformOpenAI,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, group)
|
||||
require.NotNil(t, repo.updated)
|
||||
require.Equal(t, PlatformOpenAI, repo.updated.Platform)
|
||||
require.False(t, repo.updated.AllowBatchImageGeneration)
|
||||
require.False(t, group.AllowBatchImageGeneration)
|
||||
}
|
||||
|
||||
func TestAdminService_UpdateGroup_ClearsDescriptionWhenEmptyString(t *testing.T) {
|
||||
existingGroup := &Group{
|
||||
ID: 1,
|
||||
|
||||
@@ -872,6 +872,9 @@ func (s *BatchImagePublicService) ensureGroupAllowsBatchImage(ctx context.Contex
|
||||
if !group.AllowBatchImageGeneration {
|
||||
return ErrBatchImageGroupDisabled
|
||||
}
|
||||
if group.Platform != PlatformGemini {
|
||||
return ErrBatchImageGroupDisabled
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,9 @@ func TestBatchImagePublicService_Submit(t *testing.T) {
|
||||
svc.GroupRepo = &publicBatchImageGroupRepo{groups: map[int64]*Group{
|
||||
groupID: {
|
||||
ID: groupID,
|
||||
Platform: PlatformGemini,
|
||||
RateMultiplier: 2.0,
|
||||
AllowImageGeneration: true,
|
||||
AllowBatchImageGeneration: true,
|
||||
ImageRateIndependent: false,
|
||||
BatchImageDiscountMultiplier: 0.8,
|
||||
@@ -104,7 +106,9 @@ func TestBatchImagePublicService_Submit(t *testing.T) {
|
||||
svc.GroupRepo = &publicBatchImageGroupRepo{groups: map[int64]*Group{
|
||||
groupID: {
|
||||
ID: groupID,
|
||||
Platform: PlatformGemini,
|
||||
RateMultiplier: 1.0,
|
||||
AllowImageGeneration: true,
|
||||
AllowBatchImageGeneration: true,
|
||||
ImagePrice1K: &imagePrice,
|
||||
BatchImageDiscountMultiplier: 0.5,
|
||||
@@ -140,6 +144,7 @@ func TestBatchImagePublicService_Submit(t *testing.T) {
|
||||
svc.GroupRepo = &publicBatchImageGroupRepo{groups: map[int64]*Group{
|
||||
groupID: {
|
||||
ID: groupID,
|
||||
Platform: PlatformGemini,
|
||||
RateMultiplier: 1,
|
||||
AllowBatchImageGeneration: false,
|
||||
BatchImageDiscountMultiplier: 0.5,
|
||||
@@ -472,7 +477,9 @@ func TestBatchImagePublicService_ListModels(t *testing.T) {
|
||||
svc.GroupRepo = &publicBatchImageGroupRepo{groups: map[int64]*Group{
|
||||
groupID: {
|
||||
ID: groupID,
|
||||
Platform: PlatformGemini,
|
||||
RateMultiplier: 1,
|
||||
AllowImageGeneration: true,
|
||||
AllowBatchImageGeneration: true,
|
||||
BatchImageDiscountMultiplier: 0.5,
|
||||
BatchImageHoldMultiplier: 0.6,
|
||||
|
||||
+10
-2
@@ -7,7 +7,7 @@ Supported providers:
|
||||
- `gemini_api`
|
||||
- `vertex`
|
||||
|
||||
API users do not see Gemini file names, Vertex job names, GCS paths, signed URLs, API keys, or service account material. Downloads are proxied through Sub2API.
|
||||
API users do not see Gemini file names, Vertex job names, GCS paths, signed URLs, API keys, or service account material. Downloads are proxied through Sub2API in this MVP.
|
||||
|
||||
## API Routes
|
||||
|
||||
@@ -198,6 +198,7 @@ For the managed Vertex/GCS batch bucket, disable Cloud Storage soft delete or co
|
||||
`gemini_api`:
|
||||
|
||||
- Uses Gemini Batch API with JSONL file mode.
|
||||
- Supports Gemini `apikey` upstream accounts with a configured API key.
|
||||
- Result file refs are internal.
|
||||
- API keys are never returned.
|
||||
- The provider can be selected and submitted through Sub2API when an administrator configures a Gemini API-key upstream account. In the 2026-07-07 PR validation, this path was verified as selectable/callable, but successful image generation was not continued because the test API key had no prepayment.
|
||||
@@ -205,11 +206,14 @@ For the managed Vertex/GCS batch bucket, disable Cloud Storage soft delete or co
|
||||
`vertex`:
|
||||
|
||||
- Uses Vertex `BatchPredictionJob` with managed GCS JSONL.
|
||||
- Supports Gemini `service_account` upstream accounts with valid service account JSON.
|
||||
- GCS bucket and prefix are server-managed.
|
||||
- Vertex job name and GCS paths are internal.
|
||||
- Batch image output should be treated as `1K`/default only in MVP.
|
||||
- Do not promise `2K` or `4K`.
|
||||
|
||||
Other Gemini account/login types are not selected by the current batch image providers unless they expose equivalent API-key or service-account credentials through the same provider flow. They were not covered by the 2026-07-07 PR validation.
|
||||
|
||||
## Official Google Enablement
|
||||
|
||||
Operators must enable Gemini/Vertex capability in Google's official console before turning on Sub2API batch image for any group. Sub2API feature flags and group switches do not create Google-side access by themselves.
|
||||
@@ -221,7 +225,7 @@ Recommended production path:
|
||||
- Use a service account or Application Default Credentials for the Sub2API runtime.
|
||||
- Create one fixed Cloud Storage bucket for batch image input and output, then grant the runtime and Vertex service agent the minimum required bucket permissions.
|
||||
- Configure Sub2API with the project id, location, managed bucket, provider account, model whitelist, and pricing.
|
||||
- Enable `BATCH_IMAGE_ENABLED` globally and `allow_batch_image_generation` only on the intended Gemini group.
|
||||
- Enable `BATCH_IMAGE_ENABLED` globally, enable image generation on the intended Gemini group, then enable `allow_batch_image_generation` for that group. Non-Gemini groups are not eligible for batch image generation, and the admin UI only shows the batch image group switch after image generation is enabled on a Gemini group.
|
||||
|
||||
API-key path:
|
||||
|
||||
@@ -312,6 +316,10 @@ Feature flags default to disabled.
|
||||
- Confirm billing pricing.
|
||||
- Run smoke tests before enabling.
|
||||
|
||||
## Future Optimization
|
||||
|
||||
- Optional object-storage download offload: persist completed image outputs to an operator-configured object store such as GCS, S3, or R2, then issue short-lived signed download links to users. This would avoid routing large image/ZIP downloads through the Sub2API server, which is useful for small-bandwidth deployments. Keep it opt-in because it needs extra storage credentials, lifecycle cleanup, signed-URL expiry policy, access auditing, and compatibility with output deletion.
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- No provider refs in public responses.
|
||||
|
||||
@@ -2314,6 +2314,7 @@ export default {
|
||||
batchHoldMultiplier: 'Batch hold price ratio',
|
||||
batchSectionHint: 'Batch image settings only apply to batch jobs: settlement applies the batch discount, and the upfront hold is normal image price × batch hold price ratio. Reference images also create upstream input-token usage, so a batch image discount above 0.5 is recommended.',
|
||||
batchDisabledHint: 'Enable image generation for this group before enabling batch image generation.',
|
||||
batchGeminiOnlyHint: 'Batch image generation is currently available only for Gemini groups.',
|
||||
modeHint: 'By default, image billing uses image price × current effective group multiplier. Independent mode uses image price × image multiplier.',
|
||||
finalPricePreview: 'Final per-image price preview',
|
||||
notConfigured: 'Not configured'
|
||||
|
||||
@@ -2396,6 +2396,7 @@ export default {
|
||||
batchHoldMultiplier: '批量冻结价格比例',
|
||||
batchSectionHint: '批量生图仅影响批量任务:结算价格会叠加批量折扣倍率,提交时冻结金额按普通生图原价 × 批量冻结价格比例计算。参考图也会产生上游输入 token 消耗,建议批量生图折扣倍率设置大于 0.5。',
|
||||
batchDisabledHint: '请先开启当前分组生图,才能开启批量生图。',
|
||||
batchGeminiOnlyHint: '批量生图当前仅支持 Gemini 分组。',
|
||||
modeHint: '默认关闭独立倍率时,图片费用 = 图片价格 × 当前分组有效倍率;开启独立倍率后,图片费用 = 图片价格 × 生图独立倍率。',
|
||||
finalPricePreview: '最终单张价格预览',
|
||||
notConfigured: '未配置'
|
||||
|
||||
@@ -889,25 +889,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 border-t border-dashed border-gray-200 pt-4 dark:border-dark-700">
|
||||
<div v-if="createForm.platform === 'gemini' && createForm.allow_image_generation" class="mt-4 border-t border-dashed border-gray-200 pt-4 dark:border-dark-700">
|
||||
<label
|
||||
class="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
:class="!createForm.allow_image_generation ? 'cursor-not-allowed opacity-50' : ''"
|
||||
>
|
||||
<input
|
||||
v-model="createForm.allow_batch_image_generation"
|
||||
type="checkbox"
|
||||
:disabled="!createForm.allow_image_generation"
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
{{ t("admin.groups.imagePricing.allowBatchImageGeneration") }}
|
||||
</label>
|
||||
<p
|
||||
v-if="!createForm.allow_image_generation"
|
||||
class="mt-2 text-xs text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
{{ t("admin.groups.imagePricing.batchDisabledHint") }}
|
||||
</p>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.groups.imagePricing.batchSectionHint") }}
|
||||
</p>
|
||||
@@ -943,6 +935,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
v-else-if="createForm.platform !== 'gemini'"
|
||||
class="mt-4 border-t border-dashed border-gray-200 pt-4 text-xs text-gray-500 dark:border-dark-700 dark:text-gray-400"
|
||||
>
|
||||
{{ t("admin.groups.imagePricing.batchGeminiOnlyHint") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 高峰时段倍率配置(仅订阅类型分组) -->
|
||||
@@ -2282,25 +2280,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 border-t border-dashed border-gray-200 pt-4 dark:border-dark-700">
|
||||
<div v-if="editForm.platform === 'gemini' && editForm.allow_image_generation" class="mt-4 border-t border-dashed border-gray-200 pt-4 dark:border-dark-700">
|
||||
<label
|
||||
class="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
:class="!editForm.allow_image_generation ? 'cursor-not-allowed opacity-50' : ''"
|
||||
>
|
||||
<input
|
||||
v-model="editForm.allow_batch_image_generation"
|
||||
type="checkbox"
|
||||
:disabled="!editForm.allow_image_generation"
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
{{ t("admin.groups.imagePricing.allowBatchImageGeneration") }}
|
||||
</label>
|
||||
<p
|
||||
v-if="!editForm.allow_image_generation"
|
||||
class="mt-2 text-xs text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
{{ t("admin.groups.imagePricing.batchDisabledHint") }}
|
||||
</p>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.groups.imagePricing.batchSectionHint") }}
|
||||
</p>
|
||||
@@ -2336,6 +2326,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
v-else-if="editForm.platform !== 'gemini'"
|
||||
class="mt-4 border-t border-dashed border-gray-200 pt-4 text-xs text-gray-500 dark:border-dark-700 dark:text-gray-400"
|
||||
>
|
||||
{{ t("admin.groups.imagePricing.batchGeminiOnlyHint") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 高峰时段倍率配置(仅订阅类型分组) -->
|
||||
@@ -4036,6 +4032,7 @@ const editForm = reactive({
|
||||
});
|
||||
|
||||
type ImagePricingFormState = {
|
||||
platform: GroupPlatform;
|
||||
allow_image_generation: boolean;
|
||||
allow_batch_image_generation: boolean;
|
||||
rate_multiplier: number;
|
||||
@@ -4103,10 +4100,10 @@ const editImageFinalPricePreview = computed(() =>
|
||||
const resetDisabledBatchImagePricing = (
|
||||
form: Pick<
|
||||
ImagePricingFormState,
|
||||
"allow_image_generation" | "allow_batch_image_generation" | "batch_image_discount_multiplier" | "batch_image_hold_multiplier"
|
||||
"platform" | "allow_image_generation" | "allow_batch_image_generation" | "batch_image_discount_multiplier" | "batch_image_hold_multiplier"
|
||||
>,
|
||||
) => {
|
||||
if (!form.allow_image_generation) {
|
||||
if (form.platform !== "gemini" || !form.allow_image_generation) {
|
||||
form.allow_batch_image_generation = false;
|
||||
}
|
||||
if (!form.allow_batch_image_generation) {
|
||||
@@ -4683,6 +4680,7 @@ watch(
|
||||
createForm.require_oauth_only = false;
|
||||
createForm.require_privacy_set = false;
|
||||
}
|
||||
resetDisabledBatchImagePricing(createForm);
|
||||
resetModelsListState(createModelsListState);
|
||||
loadModelsListCandidates("create", 0, newVal);
|
||||
},
|
||||
@@ -4715,6 +4713,7 @@ watch(
|
||||
editForm.require_oauth_only = false;
|
||||
editForm.require_privacy_set = false;
|
||||
}
|
||||
resetDisabledBatchImagePricing(editForm);
|
||||
if (editingGroup.value) {
|
||||
resetModelsListState(editModelsListState, editForm.platform === editingGroup.value.platform ? editingGroup.value.models_list_config : undefined);
|
||||
loadModelsListCandidates("edit", editingGroup.value.id, newVal);
|
||||
|
||||
@@ -60,7 +60,7 @@ Covered by automated tests and smoke:
|
||||
The batch image feature has two independent gates:
|
||||
|
||||
- Global runtime gate: `BATCH_IMAGE_ENABLED` controls whether `/v1/images/batches*` is available at all. If disabled, the backend returns `404 BATCH_IMAGE_DISABLED` regardless of group settings. This value is loaded at application startup, so changing the server environment requires restarting/redeploying the app container.
|
||||
- Group/API-key gate: `groups.allow_batch_image_generation` controls whether a user's API key may use the feature. If the global gate is enabled but the API key's group is not allowed, the backend returns `403 BATCH_IMAGE_GROUP_DISABLED`.
|
||||
- Group/API-key gate: only Gemini groups with image generation enabled can enable `groups.allow_batch_image_generation`, which controls whether a user's API key may use the feature. If the global gate is enabled but the API key's group is not allowed, the backend returns `403 BATCH_IMAGE_GROUP_DISABLED`.
|
||||
|
||||
Frontend visibility follows the same group/API-key gate for user-facing entry points:
|
||||
|
||||
|
||||
@@ -8,11 +8,12 @@ Main capabilities:
|
||||
|
||||
- Public async batch image API under `/v1/images/batches*`.
|
||||
- Provider support for Vertex-managed Gemini batch jobs and Gemini API batch jobs.
|
||||
- Upstream account support is limited to Gemini `service_account` accounts for the Vertex provider and Gemini `apikey` accounts for the Gemini API provider.
|
||||
- Redis-backed worker queue, delayed requeue, stale active recovery, and per-job locks.
|
||||
- PostgreSQL job/item state, provider refs kept internal, and proxied item/ZIP downloads.
|
||||
- Balance hold, capture, release, partial-failure settlement, and idempotent billing request ids.
|
||||
- Frontend user batch image guide and gated navigation entry.
|
||||
- Feature gates through global `BATCH_IMAGE_ENABLED` and group-level `allow_batch_image_generation`.
|
||||
- Feature gates through global `BATCH_IMAGE_ENABLED`, Gemini-only group eligibility, image-generation enablement, and group-level `allow_batch_image_generation`.
|
||||
|
||||
The feature is intentionally not GA by default. It should be enabled first through feature flag and group opt-in only.
|
||||
|
||||
@@ -44,12 +45,14 @@ Online validation recorded on 2026-07-07:
|
||||
|
||||
- No high-concurrency online stress test was run because it would create unnecessary provider cost and production pressure.
|
||||
- Gemini API-key upstream success still needs one paid/prepaid low-cost image test when such a key is available.
|
||||
- Other Gemini login/account types were not tested and are not selected by the current providers unless they can expose equivalent service-account or API-key credentials through the same provider flow.
|
||||
- A future integration test can exercise simultaneous cancel vs settlement under load, although Redis per-job locks, PostgreSQL row locks, and billing idempotency are already present.
|
||||
- Optional object-storage download offload could be added later: store completed outputs in GCS/S3/R2 and issue short-lived signed links so large image/ZIP downloads do not consume Sub2API server bandwidth. This should remain opt-in because it adds storage credentials, lifecycle cleanup, signed-link expiry, and access-audit requirements.
|
||||
|
||||
## Rollout Recommendation
|
||||
|
||||
Merge/review behind flags only:
|
||||
|
||||
- Keep `BATCH_IMAGE_ENABLED=false` by default.
|
||||
- Enable only for selected Gemini groups through `allow_batch_image_generation=true`.
|
||||
- Enable only for selected Gemini groups after `allow_image_generation=true`, then set `allow_batch_image_generation=true`; non-Gemini groups are intentionally not eligible for this switch.
|
||||
- Start with one controlled group and monitor job state, provider errors, hold/capture/release events, and download volume before broader enablement.
|
||||
|
||||
Reference in New Issue
Block a user