Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/desktop/src/bun/remote/remote-runtime-client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {
ArkImageGenerationConfig,
ImageGenerationConfig,
AgentEvent,
BuiltinTool,
CustomModel,
Expand Down Expand Up @@ -135,7 +135,7 @@ export class RemoteRuntimeClient implements RuntimeClient {
}

createSubagentThread(
input: Parameters<RuntimeClient["createSubagentThread"]>[0],
input: Parameters<RuntimeClient["createSubagentThread"]>[0]
) {
return this._rpc<
Awaited<ReturnType<RuntimeClient["createSubagentThread"]>>
Expand Down Expand Up @@ -315,7 +315,7 @@ export class RemoteRuntimeClient implements RuntimeClient {
api?:
"anthropic-messages" | "openai-completions" | "openai-responses" | null;
icon?: string | null;
imageGeneration?: ArkImageGenerationConfig;
imageGeneration?: ImageGenerationConfig;
}) {
return this._rpc<ModelProviderGroup[]>("models.updateProvider", input);
}
Expand Down
125 changes: 95 additions & 30 deletions apps/desktop/src/components/settings/image-model-editor-dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
"use client";

import {
getOpenAIImageSizes,
normalizeImageSize,
SEEDREAM_IMAGE_SIZES,
type SeedreamImageModelDefinition,
type SeedreamImageSize,
type ImageGenerationApi,
type ImageModelDefinition,
type ImageSize,
} from "@llm-space/core";
import { ModelAvatar } from "@llm-space/ui/components/thread-playground/model-avatar";
import { Button } from "@llm-space/ui/ui/button";
Expand Down Expand Up @@ -33,68 +36,90 @@ interface ImageModelFormState {
id: string;
name: string;
icon: string;
supportedSizes: SeedreamImageSize[];
defaultSize: SeedreamImageSize;
supportedSizes: ImageSize[];
defaultSize: ImageSize;
responseFormat: "auto" | "b64_json";
}

/** Create the editable form state for a new or existing image model. */
function _initialState(
model: SeedreamImageModelDefinition | null | undefined
model: ImageModelDefinition | null | undefined,
api: ImageGenerationApi
): ImageModelFormState {
return model
? {
id: model.id,
name: model.name,
icon: model.icon ?? "",
supportedSizes: [...model.supportedSizes],
defaultSize: model.defaultSize,
}
: {
id: "",
name: "",
icon: "",
supportedSizes: [...SEEDREAM_IMAGE_SIZES],
defaultSize: "2K",
};
if (model) {
return {
id: model.id,
name: model.name,
icon: model.icon ?? "",
supportedSizes: [
...new Set(
model.supportedSizes.map((size) => normalizeImageSize(size, api))
),
],
defaultSize: normalizeImageSize(model.defaultSize, api),
responseFormat: model.responseFormat ?? "auto",
};
}
return {
id: "",
name: "",
icon: "",
supportedSizes:
api === "openai-images" ? ["1024x1024"] : [...SEEDREAM_IMAGE_SIZES],
defaultSize: api === "openai-images" ? "1024x1024" : "2K",
responseFormat: "auto",
};
}

/** Add or edit one provider-owned custom image model definition. */
export function ImageModelEditorDialog({
open,
onOpenChange,
api,
model,
existingIds,
onSave,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
model?: SeedreamImageModelDefinition | null;
api: ImageGenerationApi;
model?: ImageModelDefinition | null;
existingIds: readonly string[];
onSave: (model: SeedreamImageModelDefinition, originalId?: string) => void;
onSave: (model: ImageModelDefinition, originalId?: string) => void;
}) {
const { t } = useI18n();
const [form, setForm] = useState<ImageModelFormState>(() =>
_initialState(model)
_initialState(model, api)
);

useEffect(() => {
if (open) {
setForm(_initialState(model));
setForm(_initialState(model, api));
}
}, [model, open]);
}, [model, open, api]);

const id = form.id.trim();
const sizeOptions: readonly ImageSize[] =
api === "openai-images" ? getOpenAIImageSizes(id) : SEEDREAM_IMAGE_SIZES;
const unsupportedSizes = form.supportedSizes.filter(
(size) => !sizeOptions.includes(size)
);
const displayedSizes = [...sizeOptions, ...unsupportedSizes];
const duplicateId = existingIds.some(
(candidate) => candidate === id && candidate !== model?.id
);
const canSave =
id.length > 0 && form.supportedSizes.length > 0 && !duplicateId;
id.length > 0 &&
form.supportedSizes.length > 0 &&
form.supportedSizes.includes(form.defaultSize) &&
unsupportedSizes.length === 0 &&
!duplicateId;

/** Keep the default size valid while the supported-size set changes. */
const handleSizeToggle = (size: SeedreamImageSize, enabled: boolean) => {
const handleSizeToggle = (size: ImageSize, enabled: boolean) => {
setForm((current) => {
const supportedSizes = enabled
? SEEDREAM_IMAGE_SIZES.filter(
? displayedSizes.filter(
(candidate) =>
current.supportedSizes.includes(candidate) || candidate === size
)
Expand All @@ -121,6 +146,9 @@ export function ImageModelEditorDialog({
name: form.name.trim() || id,
supportedSizes: form.supportedSizes,
defaultSize: form.defaultSize,
...(form.responseFormat === "b64_json"
? { responseFormat: "b64_json" as const }
: {}),
...(icon ? { icon } : {}),
},
model?.id
Expand Down Expand Up @@ -197,7 +225,7 @@ export function ImageModelEditorDialog({

<_Field label={t.models.supportedSizes}>
<div className="grid grid-cols-2 gap-2">
{SEEDREAM_IMAGE_SIZES.map((size) => (
{displayedSizes.map((size) => (
<div
key={size}
className="bg-muted/40 flex items-center justify-between rounded-md px-3 py-2 text-sm"
Expand All @@ -214,6 +242,13 @@ export function ImageModelEditorDialog({
</div>
))}
</div>
{unsupportedSizes.length > 0 && (
<p role="alert" className="text-destructive text-xs">
{formatMessage(t.models.unsupportedImageSizes, {
sizes: unsupportedSizes.join(", "),
})}
</p>
)}
</_Field>

<_Field label={t.models.defaultSize}>
Expand All @@ -223,7 +258,7 @@ export function ImageModelEditorDialog({
onValueChange={(value) =>
setForm((current) => ({
...current,
defaultSize: value as SeedreamImageSize,
defaultSize: value as ImageSize,
}))
}
>
Expand All @@ -242,6 +277,36 @@ export function ImageModelEditorDialog({
</SelectContent>
</Select>
</_Field>

{api === "openai-images" && (
<_Field label={t.models.responseFormat}>
<Select
value={form.responseFormat}
onValueChange={(value) =>
setForm((current) => ({
...current,
responseFormat:
value as ImageModelFormState["responseFormat"],
}))
}
>
<SelectTrigger
className="w-full"
aria-label={t.models.imageResponseFormat}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto">
{t.models.automaticResponseFormat}
</SelectItem>
<SelectItem value="b64_json">
{t.models.base64ResponseFormat}
</SelectItem>
</SelectContent>
</Select>
</_Field>
)}
</div>

<DialogFooter>
Expand Down
64 changes: 52 additions & 12 deletions apps/desktop/src/components/settings/models-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
import {
formatProviderProfileLabel,
getArkImageModelDefinitions,
type ArkImageGenerationConfig,
getImageModelDefinitions,
type CustomModel,
type ImageGenerationApi,
type ImageGenerationConfig,
type ImageModelDefinition,
type ModelProviderGroup,
type ProviderProfile,
type SeedreamImageModelDefinition,
} from "@llm-space/core";
import { ConfirmDialog } from "@llm-space/ui/components/confirm-dialog";
import { Link } from "@llm-space/ui/components/link";
Expand Down Expand Up @@ -1094,8 +1096,8 @@ function ProviderEditor({
</div>
) : null}

{provider.id === "ark" && canManageModels ? (
<_ArkImageGenerationEditor provider={provider} />
{(provider.id === "ark" || !isBuiltin) && canManageModels ? (
<_ImageGenerationEditor provider={provider} />
) : null}
</div>
</ScrollArea>
Expand Down Expand Up @@ -1236,8 +1238,8 @@ function _ProviderProfileEditor({
);
}

/** Chat-model-parity inventory management for Ark image models. */
function _ArkImageGenerationEditor({
/** Chat-model-parity inventory management for provider-owned image models. */
function _ImageGenerationEditor({
provider,
}: {
provider: ModelProviderGroup;
Expand All @@ -1246,7 +1248,12 @@ function _ArkImageGenerationEditor({
const displayName = providerDisplayName(provider, lang);
const updateProvider = useUpdateProvider();
const config = provider.imageGeneration ?? {};
const models = getArkImageModelDefinitions(config);
const imageApi =
config.api ?? (provider.id === "ark" ? "ark-images" : "openai-images");
const models =
provider.id === "ark"
? getArkImageModelDefinitions(config)
: getImageModelDefinitions(config);
const disabledModels = new Set(config.disabledModels ?? []);
const enabledModels = models.filter((model) => !disabledModels.has(model.id));
const customModels = new Set((config.models ?? []).map((model) => model.id));
Expand All @@ -1255,16 +1262,17 @@ function _ArkImageGenerationEditor({
);
const [modelListRef] = useAutoAnimation<HTMLDivElement>();
const [editorOpen, setEditorOpen] = useState(false);
const [editingModel, setEditingModel] =
useState<SeedreamImageModelDefinition | null>(null);
const [editingModel, setEditingModel] = useState<ImageModelDefinition | null>(
null
);

const visibleModels = models.filter((model) => {
if (modelView === "enabled") return !disabledModels.has(model.id);
if (modelView === "disabled") return disabledModels.has(model.id);
return true;
});

const update = (imageGeneration: ArkImageGenerationConfig) => {
const update = (imageGeneration: ImageGenerationConfig) => {
void updateProvider(provider.id, { imageGeneration }).catch((error) => {
toast.error("Failed to update image generation", {
description:
Expand Down Expand Up @@ -1296,7 +1304,7 @@ function _ArkImageGenerationEditor({

/** Add or replace a custom image model and preserve its disabled state. */
const handleSaveCustomModel = (
model: SeedreamImageModelDefinition,
model: ImageModelDefinition,
originalId?: string
) => {
const custom = (config.models ?? []).filter(
Expand Down Expand Up @@ -1331,6 +1339,37 @@ function _ArkImageGenerationEditor({

return (
<>
{provider.id !== "ark" ? (
<div className="flex flex-col gap-2">
<span className="text-sm font-medium">{t.models.imageApiType}</span>
<Select
value={imageApi}
onValueChange={(api) =>
update({ ...config, api: api as ImageGenerationApi })
}
>
<SelectTrigger
className="w-full"
aria-label={formatMessage(t.models.imageApiAriaLabel, {
name: displayName,
})}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="ark-images">
{t.models.arkImagesApi}
</SelectItem>
<SelectItem value="openai-images">
{t.models.openAIImagesApi}
</SelectItem>
<SelectItem value="openai-images-extra-body">
{t.models.openAIImagesExtraBodyApi}
</SelectItem>
</SelectContent>
</Select>
</div>
) : null}
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">{t.models.imageModels}</span>
Expand Down Expand Up @@ -1427,6 +1466,7 @@ function _ArkImageGenerationEditor({
<ImageModelEditorDialog
open={editorOpen}
onOpenChange={setEditorOpen}
api={imageApi}
model={editingModel}
existingIds={models.map((model) => model.id)}
onSave={handleSaveCustomModel}
Expand All @@ -1446,7 +1486,7 @@ function _ImageModelListItem({
onDelete,
}: {
providerName: string;
model: SeedreamImageModelDefinition;
model: ImageModelDefinition;
enabled: boolean;
isCustom: boolean;
onToggle: (enabled: boolean) => void;
Expand Down
Loading