Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { InputFieldWrapper } from 'features/nodes/components/flow/nodes/Invocati
import { useInputFieldInstanceExists } from 'features/nodes/hooks/useInputFieldInstanceExists';
import { useInputFieldNameSafe } from 'features/nodes/hooks/useInputFieldNameSafe';
import { useInputFieldTemplateExists } from 'features/nodes/hooks/useInputFieldTemplateExists';
import { useNodeType } from 'features/nodes/hooks/useNodeType';
import { nodeAcceptsExtraInputs } from 'features/nodes/types/extraInputs';
import type { PropsWithChildren, ReactNode } from 'react';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
Expand All @@ -17,8 +19,14 @@ type Props = PropsWithChildren<{
export const InputFieldGate = memo(({ nodeId, fieldName, children, fallback, formatLabel }: Props) => {
const hasInstance = useInputFieldInstanceExists(fieldName);
const hasTemplate = useInputFieldTemplateExists(fieldName);
const nodeType = useNodeType();

if (!hasTemplate || !hasInstance) {
// Backend nodes with `extra='allow'` (e.g. core_metadata) accept inputs that aren't declared
// in the OpenAPI schema. Render nothing rather than an "unexpected field" warning.
if (hasInstance && !hasTemplate && nodeAcceptsExtraInputs(nodeType)) {
return null;
}
// fallback may be null, indicating we should render nothing at all - must check for undefined explicitly
if (fallback !== undefined) {
return fallback;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { debounce } from 'es-toolkit';
import { $templates } from 'features/nodes/store/nodesSlice';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import type { NodesState, Templates } from 'features/nodes/store/types';
import { nodeAcceptsExtraInputs } from 'features/nodes/types/extraInputs';
import type {
FieldInputInstance,
FieldInputTemplate,
Expand Down Expand Up @@ -275,6 +276,11 @@ export const getInvocationNodeErrors = (
const fieldTemplate = nodeTemplate.inputs[fieldName];

if (!fieldTemplate) {
// Backend nodes with `extra='allow'` accept inputs that aren't declared in the OpenAPI
// schema; these carry recall metadata and don't need template-based validation.
if (nodeAcceptsExtraInputs(node.data.type)) {
continue;
}
errors.push({ type: 'node-error', nodeId, issue: t('parameters.invoke.missingFieldTemplate') });
continue;
}
Expand Down Expand Up @@ -310,6 +316,9 @@ const syncNodeErrors = (nodesState: NodesState, templates: Templates) => {
const fieldTemplate = nodeTemplate.inputs[fieldName];

if (!fieldTemplate) {
if (nodeAcceptsExtraInputs(node.data.type)) {
continue;
}
errors.push({ type: 'node-error', nodeId: node.id, issue: t('parameters.invoke.missingFieldTemplate') });
$nodeErrors.setKey(node.id, errors);
continue;
Expand Down
16 changes: 16 additions & 0 deletions invokeai/frontend/web/src/features/nodes/types/extraInputs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Some backend nodes are configured with pydantic `extra='allow'` and accept inputs that aren't
* declared in the OpenAPI schema. The most important case is `core_metadata`, which collects
* generation-mode-specific recall data (`z_image_seed_variance_*`, `dype_preset`, `ref_images`, ...).
*
* The frontend must round-trip those extras intact:
* - `graphToWorkflow` synthesizes a `MetadataExtraField` template for them
* - `buildNodesGraph` forwards their values verbatim
* - `fieldValidators` does not treat them as errors
* - `InputFieldGate` does not render an "unexpected field" warning
*
* See: https://github.com/invoke-ai/InvokeAI/issues/9151
*/
const NODES_ACCEPTING_EXTRA_INPUTS: ReadonlySet<string> = new Set(['core_metadata']);

export const nodeAcceptsExtraInputs = (nodeType: string): boolean => NODES_ACCEPTING_EXTRA_INPUTS.has(nodeType);
138 changes: 138 additions & 0 deletions invokeai/frontend/web/src/features/nodes/types/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,32 @@ const zImageGeneratorFieldType = zFieldTypeBase.extend({
name: z.literal('ImageGeneratorField'),
originalType: zStatelessFieldType.optional(),
});
/**
* Synthetic field type used for `core_metadata` extra fields that are not declared in the backend
* schema (e.g. `z_image_seed_variance_*`, `dype_preset`, `ref_images`, ...). The backend's
* `CoreMetadataInvocation` is configured with `extra='allow'`, so these are valid metadata to
* round-trip even though there is no OpenAPI template for them.
*/
const zMetadataExtraFieldType = zFieldTypeBase.extend({
name: z.literal('MetadataExtraField'),
originalType: zStatelessFieldType.optional(),
});
const zLoRAMetadataFieldType = zFieldTypeBase.extend({
name: z.literal('LoRAMetadataField'),
originalType: zStatelessFieldType.optional(),
});
const zControlNetMetadataFieldType = zFieldTypeBase.extend({
name: z.literal('ControlNetMetadataField'),
originalType: zStatelessFieldType.optional(),
});
const zIPAdapterMetadataFieldType = zFieldTypeBase.extend({
name: z.literal('IPAdapterMetadataField'),
originalType: zStatelessFieldType.optional(),
});
const zT2IAdapterMetadataFieldType = zFieldTypeBase.extend({
name: z.literal('T2IAdapterMetadataField'),
originalType: zStatelessFieldType.optional(),
});
const zStatefulFieldType = z.union([
zIntegerFieldType,
zFloatFieldType,
Expand All @@ -220,6 +246,11 @@ const zStatefulFieldType = z.union([
zIntegerGeneratorFieldType,
zStringGeneratorFieldType,
zImageGeneratorFieldType,
zLoRAMetadataFieldType,
zControlNetMetadataFieldType,
zIPAdapterMetadataFieldType,
zT2IAdapterMetadataFieldType,
zMetadataExtraFieldType,
]);
export type StatefulFieldType = z.infer<typeof zStatefulFieldType>;
const statefulFieldTypeNames = zStatefulFieldType.options.map((o) => o.shape.name.value);
Expand Down Expand Up @@ -1228,6 +1259,93 @@ export const getImageGeneratorDefaults = (type: ImageGeneratorFieldValue['type']
};
// #endregion

// #region Metadata pass-through fields
/**
* The `core_metadata` node carries metadata lists that are not edited via the UI - they are set by
* the Generate-mode graph builder (or by an edge) and must survive the workflow roundtrip verbatim
* so that the resulting image retains its recall metadata. Modeled as opaque object lists.
*
* See: https://github.com/invoke-ai/InvokeAI/issues/9151
*/
// `z.any()` (not `z.unknown()`) so the inferred type stays JSON-assignable for logging/serialization.
const zMetadataPassthroughValue = z.array(z.record(z.string(), z.any())).nullish();

const zLoRAMetadataFieldValue = zMetadataPassthroughValue;
const zLoRAMetadataFieldInputInstance = zFieldInputInstanceBase.extend({
value: zLoRAMetadataFieldValue,
});
const zLoRAMetadataFieldInputTemplate = zFieldInputTemplateBase.extend({
type: zLoRAMetadataFieldType,
originalType: zFieldType.optional(),
default: zLoRAMetadataFieldValue,
});
const zLoRAMetadataFieldOutputTemplate = zFieldOutputTemplateBase.extend({
type: zLoRAMetadataFieldType,
});
export type LoRAMetadataFieldInputTemplate = z.infer<typeof zLoRAMetadataFieldInputTemplate>;

const zControlNetMetadataFieldValue = zMetadataPassthroughValue;
const zControlNetMetadataFieldInputInstance = zFieldInputInstanceBase.extend({
value: zControlNetMetadataFieldValue,
});
const zControlNetMetadataFieldInputTemplate = zFieldInputTemplateBase.extend({
type: zControlNetMetadataFieldType,
originalType: zFieldType.optional(),
default: zControlNetMetadataFieldValue,
});
const zControlNetMetadataFieldOutputTemplate = zFieldOutputTemplateBase.extend({
type: zControlNetMetadataFieldType,
});
export type ControlNetMetadataFieldInputTemplate = z.infer<typeof zControlNetMetadataFieldInputTemplate>;

const zIPAdapterMetadataFieldValue = zMetadataPassthroughValue;
const zIPAdapterMetadataFieldInputInstance = zFieldInputInstanceBase.extend({
value: zIPAdapterMetadataFieldValue,
});
const zIPAdapterMetadataFieldInputTemplate = zFieldInputTemplateBase.extend({
type: zIPAdapterMetadataFieldType,
originalType: zFieldType.optional(),
default: zIPAdapterMetadataFieldValue,
});
const zIPAdapterMetadataFieldOutputTemplate = zFieldOutputTemplateBase.extend({
type: zIPAdapterMetadataFieldType,
});
export type IPAdapterMetadataFieldInputTemplate = z.infer<typeof zIPAdapterMetadataFieldInputTemplate>;

const zT2IAdapterMetadataFieldValue = zMetadataPassthroughValue;
const zT2IAdapterMetadataFieldInputInstance = zFieldInputInstanceBase.extend({
value: zT2IAdapterMetadataFieldValue,
});
const zT2IAdapterMetadataFieldInputTemplate = zFieldInputTemplateBase.extend({
type: zT2IAdapterMetadataFieldType,
originalType: zFieldType.optional(),
default: zT2IAdapterMetadataFieldValue,
});
const zT2IAdapterMetadataFieldOutputTemplate = zFieldOutputTemplateBase.extend({
type: zT2IAdapterMetadataFieldType,
});
export type T2IAdapterMetadataFieldInputTemplate = z.infer<typeof zT2IAdapterMetadataFieldInputTemplate>;

/**
* `MetadataExtraField` carries arbitrary JSON values for `core_metadata` extras that are not in the
* OpenAPI schema (e.g. `z_image_seed_variance_*`, `dype_preset`). Synthesized only by
* `graphToWorkflow` for `core_metadata` nodes; never produced by `parseSchema`.
*/
const zMetadataExtraFieldValue = z.any();
const zMetadataExtraFieldInputInstance = zFieldInputInstanceBase.extend({
value: zMetadataExtraFieldValue,
});
const zMetadataExtraFieldInputTemplate = zFieldInputTemplateBase.extend({
type: zMetadataExtraFieldType,
originalType: zFieldType.optional(),
default: zMetadataExtraFieldValue,
});
const zMetadataExtraFieldOutputTemplate = zFieldOutputTemplateBase.extend({
type: zMetadataExtraFieldType,
});
export type MetadataExtraFieldInputTemplate = z.infer<typeof zMetadataExtraFieldInputTemplate>;
// #endregion

// #region StatelessField
/**
* StatelessField is a catchall for stateless fields with no UI input components. They do not
Expand Down Expand Up @@ -1294,6 +1412,11 @@ export const zStatefulFieldValue = z.union([
zIntegerGeneratorFieldValue,
zStringGeneratorFieldValue,
zImageGeneratorFieldValue,
zLoRAMetadataFieldValue,
zControlNetMetadataFieldValue,
zIPAdapterMetadataFieldValue,
zT2IAdapterMetadataFieldValue,
zMetadataExtraFieldValue,
]);
export type StatefulFieldValue = z.infer<typeof zStatefulFieldValue>;

Expand Down Expand Up @@ -1322,6 +1445,11 @@ const zStatefulFieldInputInstance = z.union([
zIntegerGeneratorFieldInputInstance,
zStringGeneratorFieldInputInstance,
zImageGeneratorFieldInputInstance,
zLoRAMetadataFieldInputInstance,
zControlNetMetadataFieldInputInstance,
zIPAdapterMetadataFieldInputInstance,
zT2IAdapterMetadataFieldInputInstance,
zMetadataExtraFieldInputInstance,
]);

export const zFieldInputInstance = z.union([zStatefulFieldInputInstance, zStatelessFieldInputInstance]);
Expand Down Expand Up @@ -1350,6 +1478,11 @@ const zStatefulFieldInputTemplate = z.union([
zIntegerGeneratorFieldInputTemplate,
zStringGeneratorFieldInputTemplate,
zImageGeneratorFieldInputTemplate,
zLoRAMetadataFieldInputTemplate,
zControlNetMetadataFieldInputTemplate,
zIPAdapterMetadataFieldInputTemplate,
zT2IAdapterMetadataFieldInputTemplate,
zMetadataExtraFieldInputTemplate,
]);

export const zFieldInputTemplate = z.union([zStatefulFieldInputTemplate, zStatelessFieldInputTemplate]);
Expand Down Expand Up @@ -1377,6 +1510,11 @@ const zStatefulFieldOutputTemplate = z.union([
zIntegerGeneratorFieldOutputTemplate,
zStringGeneratorFieldOutputTemplate,
zImageGeneratorFieldOutputTemplate,
zLoRAMetadataFieldOutputTemplate,
zControlNetMetadataFieldOutputTemplate,
zIPAdapterMetadataFieldOutputTemplate,
zT2IAdapterMetadataFieldOutputTemplate,
zMetadataExtraFieldOutputTemplate,
]);

export const zFieldOutputTemplate = z.union([zStatefulFieldOutputTemplate, zStatelessFieldOutputTemplate]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { selectNodesSlice } from 'features/nodes/store/selectors';
import type { Templates } from 'features/nodes/store/types';
import { resolveConnectorSource } from 'features/nodes/store/util/connectorTopology';
import type { BoardField } from 'features/nodes/types/common';
import { nodeAcceptsExtraInputs } from 'features/nodes/types/extraInputs';
import type { BoardFieldInputInstance } from 'features/nodes/types/field';
import { isBoardFieldInputInstance, isBoardFieldInputTemplate } from 'features/nodes/types/field';
import { isConnectorNode, isExecutableNode, isInvocationNode } from 'features/nodes/types/invocation';
Expand Down Expand Up @@ -61,7 +62,11 @@ export const buildNodesGraph = (state: RootState, templates: Templates): Require
(inputsAccumulator, input, name) => {
const fieldTemplate = nodeTemplate.inputs[name];
if (!fieldTemplate) {
log.warn({ id, name }, 'Field template not found!');
if (nodeAcceptsExtraInputs(type) && input.value !== undefined) {
inputsAccumulator[name] = input.value;
} else {
log.warn({ id, name }, 'Field template not found!');
}
return inputsAccumulator;
}
if (isBoardFieldInputTemplate(fieldTemplate) && isBoardFieldInputInstance(input)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ const FIELD_VALUE_FALLBACK_MAP: Record<StatefulFieldType['name'], FieldValue> =
IntegerGeneratorField: undefined,
StringGeneratorField: undefined,
ImageGeneratorField: undefined,
LoRAMetadataField: undefined,
ControlNetMetadataField: undefined,
IPAdapterMetadataField: undefined,
T2IAdapterMetadataField: undefined,
MetadataExtraField: undefined,
};

export const buildFieldInputInstance = (id: string, template: FieldInputTemplate): FieldInputInstance => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
BoardFieldInputTemplate,
BooleanFieldInputTemplate,
ColorFieldInputTemplate,
ControlNetMetadataFieldInputTemplate,
EnumFieldInputTemplate,
FieldInputTemplate,
FieldType,
Expand All @@ -16,6 +17,9 @@ import type {
IntegerFieldCollectionInputTemplate,
IntegerFieldInputTemplate,
IntegerGeneratorFieldInputTemplate,
IPAdapterMetadataFieldInputTemplate,
LoRAMetadataFieldInputTemplate,
MetadataExtraFieldInputTemplate,
ModelIdentifierFieldInputTemplate,
SchedulerFieldInputTemplate,
StatefulFieldType,
Expand All @@ -24,6 +28,7 @@ import type {
StringFieldInputTemplate,
StringGeneratorFieldInputTemplate,
StylePresetFieldInputTemplate,
T2IAdapterMetadataFieldInputTemplate,
} from 'features/nodes/types/field';
import {
getFloatGeneratorArithmeticSequenceDefaults,
Expand Down Expand Up @@ -464,6 +469,53 @@ const buildImageGeneratorFieldInputTemplate: FieldInputTemplateBuilder<ImageGene
return template;
};

const buildLoRAMetadataFieldInputTemplate: FieldInputTemplateBuilder<LoRAMetadataFieldInputTemplate> = ({
baseField,
fieldType,
}) => ({
...baseField,
type: fieldType,
default: undefined,
});

const buildControlNetMetadataFieldInputTemplate: FieldInputTemplateBuilder<ControlNetMetadataFieldInputTemplate> = ({
baseField,
fieldType,
}) => ({
...baseField,
type: fieldType,
default: undefined,
});

const buildIPAdapterMetadataFieldInputTemplate: FieldInputTemplateBuilder<IPAdapterMetadataFieldInputTemplate> = ({
baseField,
fieldType,
}) => ({
...baseField,
type: fieldType,
default: undefined,
});

const buildT2IAdapterMetadataFieldInputTemplate: FieldInputTemplateBuilder<T2IAdapterMetadataFieldInputTemplate> = ({
baseField,
fieldType,
}) => ({
...baseField,
type: fieldType,
default: undefined,
});

// MetadataExtraField is synthesized by graphToWorkflow for `core_metadata` extras and never
// produced by parseSchema. This builder exists only to satisfy the StatefulFieldType['name'] record.
const buildMetadataExtraFieldInputTemplate: FieldInputTemplateBuilder<MetadataExtraFieldInputTemplate> = ({
baseField,
fieldType,
}) => ({
...baseField,
type: fieldType,
default: undefined,
});

const TEMPLATE_BUILDER_MAP: Record<StatefulFieldType['name'], FieldInputTemplateBuilder> = {
BoardField: buildBoardFieldInputTemplate,
BooleanField: buildBooleanFieldInputTemplate,
Expand All @@ -480,6 +532,11 @@ const TEMPLATE_BUILDER_MAP: Record<StatefulFieldType['name'], FieldInputTemplate
IntegerGeneratorField: buildIntegerGeneratorFieldInputTemplate,
StringGeneratorField: buildStringGeneratorFieldInputTemplate,
ImageGeneratorField: buildImageGeneratorFieldInputTemplate,
LoRAMetadataField: buildLoRAMetadataFieldInputTemplate,
ControlNetMetadataField: buildControlNetMetadataFieldInputTemplate,
IPAdapterMetadataField: buildIPAdapterMetadataFieldInputTemplate,
T2IAdapterMetadataField: buildT2IAdapterMetadataFieldInputTemplate,
MetadataExtraField: buildMetadataExtraFieldInputTemplate,
};

export const buildFieldInputTemplate = (
Expand Down
Loading
Loading