Files
CherryHQ-cherry-studio/docs/references/ai/image-generation-parameters.md
SuYao 5706307451 refactor(ai-service): consolidate AI runtime to main process (#14911)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: fullex <106392080+0xfullex@users.noreply.github.com>
Signed-off-by: suyao <sy20010504@gmail.com>
2026-06-05 00:06:51 +08:00

221 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Image-Generation Parameterized Architecture
How the paintings page renders a per-model parameter form, collects the user's
values, and turns them into a vendor-correct image-generation request — **all
driven by registry data, with zero per-vendor UI code**.
The goal: adding a new image model (or a whole new vendor) should be a **data**
change in the registry, not a code change. A new param that several models
share is a one-row addition. Vendor wire-format quirks live in exactly one
place each (the AI SDK adapter), never in the form or the page.
---
## The pipeline at a glance
```
registry data ──► DataApi ──► useImageGenerationSupport ──► imageGenerationToFields ──► form widgets
(models.json / (SWR cache) (SupportSpec → BaseConfigItem) │
provider-models.json) │ user edits
painting.params (canonical bag)
canonicalGenerate (partition) │
┌─────────────────────────────────┘
aiSdkParams (native fields) providerBag (everything else)
│ │
└───────────────┬───────────────────┘
AiProvider.modernGeneratePaintingImage
│ buildImageProviderOptions (emitter table)
native AI SDK image model OR custom ImageGenerationTransport
(OpenAI / Google / compat) (DashScope / PPIO / DMXAPI custom families)
```
Two halves, one canonical vocabulary in the middle:
- **Read half** — registry `supports` → form fields → `painting.params`.
- **Write half** — `painting.params` → partitioned params → vendor wire body.
The contract between them is the **canonical key set**: the form, the registry,
and `canonicalGenerate` all agree on names like `size`, `numImages`,
`negativePrompt`, `seed`, `aspectRatio`, `imageResolution`. Vendor renames
(`negative_prompt`, `batchSize`, `imageConfig.imageSize`, …) happen only in the
adapter at the very end.
---
## Registry schema
Source of truth: [`packages/provider-registry/src/schemas/model.ts`](../../../packages/provider-registry/src/schemas/model.ts) (`ImageGenerationSupportSchema`).
```ts
interface ImageGenerationSupport {
modes: Partial<Record<ImageGenerationMode, ModeDef>> // generate | edit | remix | upscale | merge
}
interface ModeDef {
supports: Record<string, SupportSpec> // canonical-key → control spec
vendorTransport?: { endpoint: string; isSync?: boolean } // per-model routing hint
requirePrompt?: boolean // default true; false for no-prompt models (qwen-mt-image, upscalers)
}
type SupportSpec =
| { type: 'switch'; default?: boolean }
| { type: 'enum'; options: string[]; default?: string; render?: 'select' | 'chips'; columns?: number }
| { type: 'range'; min: number; max: number; default?: number; step?: number }
| { type: 'size'; minSide: number; maxSide: number; pairedEnumKey?: string }
| { type: 'text'; multiline?: boolean }
```
`supports` is a **uniform map keyed by canonical name**. There is no
field-special-casing in the schema — `size`, `numImages`, `customSize` are all
just entries with a `SupportSpec`. The schema does **not** enumerate the legal
canonical keys; the only place a canonical key is "registered" for the UI is its
label row in `KEY_LABELS` (below).
### Where a model's block lives, and resolution order
A model's `imageGeneration` block can sit in two files:
- [`packages/provider-registry/data/models.json`](../../../packages/provider-registry/data/models.json) — the **base** entry. Use this for a model whose params are provider-agnostic (the official OpenAI/Google/etc. contract — `gpt-image-1`, `gemini-3-pro-image`).
- [`packages/provider-registry/data/provider-models.json`](../../../packages/provider-registry/data/provider-models.json) — a **provider override** keyed by `{ providerId, modelId }`. Use this for vendor-flavored params or vendor-exclusive models (a gateway's own SKUs, DashScope's wan/qwen families).
Resolution is **override-wins**, in `ProviderRegistryService.getImageGenerationSupport` ([`src/main/data/services/ProviderRegistryService.ts`](../../../src/main/data/services/ProviderRegistryService.ts)):
```
registryOverride.imageGeneration ?? presetModel.imageGeneration ?? null
```
Lookup tolerates id normalization (`findOverride` / `findModel` fall back to the
normalized id), so a wire id with dots (`wan2.5-i2i-preview`) and a sanitized id
(`wan2-5-i2i-preview`) resolve to the same entry.
> If `getImageGenerationSupport` returns `null`, the form is empty. That is the
> usual cause of "this model has no form": the base entry lacks an
> `imageGeneration` block **and** no provider override supplies one.
---
## Read half — registry → form
The renderer fetches the block over DataApi and turns it into widgets:
1. **Fetch**`useImageGenerationSupport(providerId, modelId)` ([`src/renderer/pages/paintings/hooks/useImageGenerationSupport.ts`](../../../src/renderer/pages/paintings/hooks/useImageGenerationSupport.ts)) queries `GET /providers/:providerId/models/:modelId*/image-generation-support` (SWR-cached; revalidates on registry mutations).
2. **Map**`imageGenerationToFields(support, { mode })` ([`src/renderer/pages/paintings/form/imageGenerationToFields.ts`](../../../src/renderer/pages/paintings/form/imageGenerationToFields.ts)) iterates `modes[mode].supports` and dispatches each entry through `specToField` by `spec.type`:
| `SupportSpec.type` | widget (`BaseConfigItem`) |
| --- | --- |
| `switch` | toggle |
| `enum` (`render:'chips'`) | chip row (size / aspectRatio / imageResolution) |
| `enum` (default) | select dropdown |
| `range` | slider |
| `size` | custom width×height inputs (gated on `pairedEnumKey === 'custom'`) |
| `text` | input / textarea (`multiline`) |
No per-vendor branches, no hardcoded key list. If the requested mode is
absent, it falls back to the model's first declared mode (each painting
provider shows one tab).
3. **Label**`KEY_LABELS` in the same file maps each canonical key to its i18n
title/tooltip. This is the **only registry of canonical keys for the UI**.
A key with no `KEY_LABELS` entry renders with the raw key as its title.
Form edits write into **`painting.params`** — a flat `Record<string, unknown>`
keyed by canonical name. Defaults are committed (not just displayed) when the
model is selected, by `computeModelFieldReset` ([`src/renderer/pages/paintings/utils/computeModelFieldReset.ts`](../../../src/renderer/pages/paintings/utils/computeModelFieldReset.ts)) — it writes each new model's `spec.default`, clears params the new model doesn't accept, and resets enum carry-overs that are invalid for the new model.
---
## Write half — params → vendor request
### 1. Partition (`canonicalGenerate`)
[`src/renderer/pages/paintings/model/canonicalGenerate.ts`](../../../src/renderer/pages/paintings/model/canonicalGenerate.ts) splits every `painting.params` entry into two buckets:
- **`AI_SDK_NATIVE_KEYS`** (after `POSITIONAL_RENAME`: `size→imageSize`, `numImages→batchSize`) → `aiSdkParams`, the positional AI SDK call options.
- **everything else** → `providerBag` = `providerOptions[providerId]`, forwarded by reference (so non-JSON callbacks like `onProgress` survive the plugin chain).
Empty / `undefined` / `null` values are dropped here — the server applies its
own default; no client-side defaults are invented. The `'auto'` sentinel is
**not** dropped at this stage: it's carried through and resolved to "omit the
field" one stage later by the emitters (e.g. `toDashScopeSize` /
`resolveSizeParameter` in `dashscopeTransport.ts`).
### 2. Transport routing hint (`paintingPipeline`)
[`src/renderer/pages/paintings/model/paintingPipeline.ts`](../../../src/renderer/pages/paintings/model/paintingPipeline.ts) reads the resolved mode's `vendorTransport` and `requirePrompt`, then:
- injects `painting.params.modelDescriptor = { id, endpoint, isSync, mode }` so a custom transport can route by it (PPIO-style async endpoints), and
- threads `requirePrompt` into `canonicalGenerate` so no-prompt models (qwen-mt-image, upscalers) skip the empty-prompt guard.
### 3. providerOptions emitters (`buildImageProviderOptions`)
[`src/main/ai/utils/imageOptions.ts`](../../../src/main/ai/utils/imageOptions.ts) is a **table of per-provider emitters** that map canonical params to each vendor's real wire field names and bag key:
```
EMITTERS: Record<providerId, Emitter> // unlisted ids → diffusion fallback
```
- `openaiFamily` (openai / azure / newapi / cherryin …) → `{ quality, background, moderation, style }` dual-keyed under `openai` + the raw id.
- `diffusion` (silicon / zhipu / openrouter / …, the default) → snake_case `{ negative_prompt, seed, num_inference_steps, guidance_scale, prompt_enhancement, quality }`.
- `google``imageConfig.{aspectRatio, imageSize}` + Imagen `personGeneration` (lowercased for the AI SDK schema).
- `dashscope`, `dmxapi`, `aihubmix` → their own field sets / dual-keying.
This is where wire-format quirks live — snake_case renames, `imageConfig`
nesting, enum casing. Nowhere else.
### 4. The model itself
[`AiService.generateImage`](../../../src/main/ai/AiService.ts) (reached via the `Ai_GenerateImage` IPC) hands `aiSdkParams` + `providerOptions` to the resolved image model. The model is one of two kinds, decided by the provider factory:
- **Native AI SDK image model** — `OpenAIImageModel`, `@ai-sdk/google` `.image()`, `OpenAICompatibleImageModel`. Spreads `providerOptions[key]` into the request body.
- **Custom `ImageGenerationTransport`** — for async submit→poll vendors or non-OpenAI wire shapes (DashScope, PPIO, DMXAPI's Doubao/Wan/async-Qwen families). See [`src/main/ai/provider/custom/imageGenerationModel.ts`](../../../src/main/ai/provider/custom/imageGenerationModel.ts); each vendor's transport lives beside its provider in a per-vendor folder (e.g. [`dmxapi/dmxapiTransport.ts`](../../../src/main/ai/provider/custom/dmxapi/dmxapiTransport.ts)), with shared helpers in [`transportUtils.ts`](../../../src/main/ai/provider/custom/transportUtils.ts). Multi-backend gateways (DMXAPI) dispatch by a `{match, family}` table on the model id — see [`dmxapi/dmxapiProvider.ts`](../../../src/main/ai/provider/custom/dmxapi/dmxapiProvider.ts).
---
## Recipes
### Add a parameter to a model
1. Pick the **canonical key** (reuse an existing one if the param already exists elsewhere — `seed`, `negativePrompt`, …).
2. If it's new to the UI, add a `KEY_LABELS` row in `imageGenerationToFields.ts` and the i18n strings (`pnpm i18n:sync`).
3. Declare it in the model's `supports` with the right `SupportSpec`.
4. If it must reach the wire under a different name / bag, handle it in the vendor's emitter (`imageOptions.ts`) or transport — **only** if it isn't already an `AI_SDK_NATIVE_KEYS` field that the adapter handles.
### Add a model
- Provider-agnostic official model (OpenAI/Google contract) → add the `imageGeneration` block to the **base** entry in `models.json`.
- Vendor-flavored or vendor-exclusive → add a `{ providerId, modelId, imageGeneration }` override in `provider-models.json` (set `apiModelId` to the wire id; standalone models also set `name`/`capabilities`/`inputModalities`).
- Async / non-OpenAI wire shape → add `vendorTransport.endpoint` (+ `isSync`) and ensure the vendor's transport recognizes the model's family.
### Add a vendor
- OpenAI-compatible? Nothing custom needed — the `diffusion` fallback emitter + `OpenAICompatibleImageModel` cover it.
- Native SDK (OpenAI/Google/Anthropic backed)? Route by model family in the provider factory (see the DMXAPI `{match, family}` tables).
- Bespoke wire shape / async? Implement an `ImageGenerationTransport` and register it on the provider's `imageModel(...)`.
---
## Critical files
| Concern | File |
| --- | --- |
| Registry schema | `packages/provider-registry/src/schemas/model.ts` |
| Base model data | `packages/provider-registry/data/models.json` |
| Provider overrides | `packages/provider-registry/data/provider-models.json` |
| Resolver (override ?? base) | `src/main/data/services/ProviderRegistryService.ts` |
| Support fetch hook | `src/renderer/pages/paintings/hooks/useImageGenerationSupport.ts` |
| Registry → form fields | `src/renderer/pages/paintings/form/imageGenerationToFields.ts` |
| Default population on switch | `src/renderer/pages/paintings/utils/computeModelFieldReset.ts` |
| Param partition | `src/renderer/pages/paintings/model/canonicalGenerate.ts` |
| Transport hint + requirePrompt | `src/renderer/pages/paintings/model/paintingPipeline.ts` |
| providerOptions emitters | `src/main/ai/utils/imageOptions.ts` |
| Custom transport wrapper | `src/main/ai/provider/custom/imageGenerationModel.ts` |
| Vendor provider + transport | `src/main/ai/provider/custom/<vendor>/{<vendor>Provider,<vendor>Transport}.ts` |
| Shared transport helpers | `src/main/ai/provider/custom/transportUtils.ts` |
```