weige 2 months ago
parent
commit
18c85e70e4
  1. 2
      src/app/api/__tests__/popiserverModels.test.ts
  2. 1
      src/app/api/_popiserverModels.ts
  3. 5
      src/app/api/models/[modelId]/route.ts
  4. 10
      src/components/FloatingActionBar.tsx
  5. 3
      src/components/WorkflowCanvas.tsx
  6. 35
      src/components/__tests__/GenerationComposer.test.tsx
  7. 23
      src/components/__tests__/ModelParameters.test.tsx
  8. 129
      src/components/composer/GenerationComposer.tsx
  9. 7
      src/components/nodes/GenerateAudioNode.tsx
  10. 7
      src/components/nodes/GenerateImageNode.tsx
  11. 7
      src/components/nodes/GenerateVideoNode.tsx
  12. 11
      src/components/nodes/ModelParameters.tsx
  13. 4
      src/hooks/useSelectedModelSchema.ts
  14. 19
      src/utils/modelSelection.ts

2
src/app/api/__tests__/popiserverModels.test.ts

@ -84,6 +84,7 @@ describe("popiserver model helpers", () => {
code: "speech-2.8-hd", code: "speech-2.8-hd",
name: "Speech 2.8 HD", name: "Speech 2.8 HD",
aiModelCodeAlias: "speech-2.8-hd", aiModelCodeAlias: "speech-2.8-hd",
billingMethod: 1,
categories: [{ taskSubType: 301 }], categories: [{ taskSubType: 301 }],
}); });
@ -94,6 +95,7 @@ describe("popiserver model helpers", () => {
aiModelId: 30, aiModelId: 30,
aiModelCode: "speech-2.8-hd", aiModelCode: "speech-2.8-hd",
aiModelCodeAlias: "speech-2.8-hd", aiModelCodeAlias: "speech-2.8-hd",
billingMethod: 1,
}); });
}); });
}); });

1
src/app/api/_popiserverModels.ts

@ -199,6 +199,7 @@ export function popiModelToProviderModel(model: PopiAIModel): ProviderModel {
duration: model.duration, duration: model.duration,
videoRatio: model.videoRatio, videoRatio: model.videoRatio,
resolution: model.resolution, resolution: model.resolution,
billingMethod: model.billingMethod,
isDefault: model.isDefault, isDefault: model.isDefault,
isSupportImages: model.isSupportImages, isSupportImages: model.isSupportImages,
isSupportVideos: model.isSupportVideos, isSupportVideos: model.isSupportVideos,

5
src/app/api/models/[modelId]/route.ts

@ -24,7 +24,7 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { ProviderType } from "@/types"; import { ProviderType } from "@/types";
import { ModelParameter, ModelInput } from "@/lib/providers/types"; import { ModelParameter, ModelInput, ProviderModel } from "@/lib/providers/types";
import { import {
getCachedWaveSpeedSchema, getCachedWaveSpeedSchema,
setCachedWaveSpeedSchema, setCachedWaveSpeedSchema,
@ -37,6 +37,7 @@ import {
buildPopiModelInputs, buildPopiModelInputs,
buildPopiModelParameters, buildPopiModelParameters,
fetchPopiModelDetail, fetchPopiModelDetail,
popiModelToProviderModel,
} from "@/app/api/_popiserverModels"; } from "@/app/api/_popiserverModels";
// Cache for model schemas (10 minute TTL) // Cache for model schemas (10 minute TTL)
@ -125,6 +126,7 @@ interface SchemaSuccessResponse {
success: true; success: true;
parameters: ModelParameter[]; parameters: ModelParameter[];
inputs: ModelInput[]; inputs: ModelInput[];
model?: ProviderModel;
cached: boolean; cached: boolean;
} }
@ -1615,6 +1617,7 @@ export async function GET(
success: true, success: true,
parameters: buildPopiModelParameters(model), parameters: buildPopiModelParameters(model),
inputs: buildPopiModelInputs(model), inputs: buildPopiModelInputs(model),
model: popiModelToProviderModel(model),
cached: false, cached: false,
}); });
} catch (error) { } catch (error) {

10
src/components/FloatingActionBar.tsx

@ -166,6 +166,7 @@ function GenerateComboButton() {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
const addNode = useWorkflowStore((state) => state.addNode); const addNode = useWorkflowStore((state) => state.addNode);
const selectSingleNode = useWorkflowStore((state) => state.selectSingleNode);
const { screenToFlowPosition } = useReactFlow(); const { screenToFlowPosition } = useReactFlow();
useEffect(() => { useEffect(() => {
@ -191,7 +192,8 @@ function GenerateComboButton() {
y: center.y + Math.random() * 100 - 50, y: center.y + Math.random() * 100 - 50,
}); });
addNode(type, position, await buildDefaultGenerationNodeData(type)); const nodeId = addNode(type, position, await buildDefaultGenerationNodeData(type));
selectSingleNode(nodeId);
setIsOpen(false); setIsOpen(false);
}; };
@ -286,6 +288,7 @@ function AllNodesMenu() {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
const addNode = useWorkflowStore((state) => state.addNode); const addNode = useWorkflowStore((state) => state.addNode);
const selectSingleNode = useWorkflowStore((state) => state.selectSingleNode);
const { screenToFlowPosition } = useReactFlow(); const { screenToFlowPosition } = useReactFlow();
useEffect(() => { useEffect(() => {
@ -311,9 +314,10 @@ function AllNodesMenu() {
y: center.y + Math.random() * 100 - 50, y: center.y + Math.random() * 100 - 50,
}); });
addNode(type, position, await buildDefaultGenerationNodeData(type)); const nodeId = addNode(type, position, await buildDefaultGenerationNodeData(type));
selectSingleNode(nodeId);
setIsOpen(false); setIsOpen(false);
}, [addNode, screenToFlowPosition]); }, [addNode, screenToFlowPosition, selectSingleNode]);
const handleDragStart = useCallback((event: React.DragEvent, type: NodeType) => { const handleDragStart = useCallback((event: React.DragEvent, type: NodeType) => {
event.dataTransfer.setData("application/node-type", type); event.dataTransfer.setData("application/node-type", type);

3
src/components/WorkflowCanvas.tsx

@ -2340,7 +2340,8 @@ export function WorkflowCanvas() {
x: event.clientX, x: event.clientX,
y: event.clientY, y: event.clientY,
}); });
addNode(nodeType, position, await buildDefaultGenerationNodeData(nodeType)); const nodeId = addNode(nodeType, position, await buildDefaultGenerationNodeData(nodeType));
selectSingleNode(nodeId);
return; return;
} }

35
src/components/__tests__/GenerationComposer.test.tsx

@ -650,6 +650,7 @@ describe("GenerationComposer", () => {
aiModelCodeAlias: "gpt-image-2-all", aiModelCodeAlias: "gpt-image-2-all",
type: 1, type: 1,
subType: 102, subType: 102,
billingMethod: 1,
}, },
}, },
}), }),
@ -679,6 +680,40 @@ describe("GenerationComposer", () => {
}); });
}); });
it("does not show PopiServer task price estimates for token-billed models", async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
throw new Error(`Unexpected fetch: ${String(input)}`);
});
vi.stubGlobal("fetch", fetchMock);
useWorkflowStore.setState({
nodes: [
imageNode("img-1", true, {
selectedModel: {
provider: "popiserver",
modelId: "32",
displayName: "Token Billed Image",
capabilities: ["text-to-image"],
metadata: {
aiModelId: 32,
aiModelCode: "token-image-model",
aiModelCodeAlias: "token-image-model",
type: 1,
subType: 102,
billingMethod: 2,
},
},
}),
],
});
render(<GenerationComposer />);
await waitFor(() => {
expect(screen.getByRole("button", { name: "Generate" })).toBeInTheDocument();
});
expect(fetchMock.mock.calls.some(([input]) => String(input).startsWith("/api/points/calculate-task-price"))).toBe(false);
});
it("uses the Seedance estimate endpoint for PopiServer Seedance models", async () => { it("uses the Seedance estimate endpoint for PopiServer Seedance models", async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL) => { const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = String(input); const url = String(input);

23
src/components/__tests__/ModelParameters.test.tsx

@ -95,6 +95,29 @@ describe("ModelParameters", () => {
expect(onParametersChange).toHaveBeenCalledWith({ max_images: 2 }); expect(onParametersChange).toHaveBeenCalledWith({ max_images: 2 });
}); });
it("replaces stale enum parameter values with the current schema default", async () => {
const onParametersChange = vi.fn();
render(
<ModelParameters
{...defaultProps}
parameters={{ resolution: "1K" }}
schema={[
createMockParameter({
name: "resolution",
type: "string",
enum: ["4K"],
default: "4K",
}),
]}
onParametersChange={onParametersChange}
/>
);
await waitFor(() => {
expect(onParametersChange).toHaveBeenCalledWith({ resolution: "4K" });
});
});
it("renders number validation and boolean defaults", () => { it("renders number validation and boolean defaults", () => {
render( render(
<ModelParameters <ModelParameters

129
src/components/composer/GenerationComposer.tsx

@ -14,6 +14,7 @@ import {
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { useReactFlow, useViewport, ViewportPortal } from "@xyflow/react"; import { useReactFlow, useViewport, ViewportPortal } from "@xyflow/react";
import { useProviderApiKeys, useWorkflowStore } from "@/store/workflowStore"; import { useProviderApiKeys, useWorkflowStore } from "@/store/workflowStore";
import { useModelStore } from "@/store/modelStore";
import { import {
AspectRatio, AspectRatio,
EaseCurveNodeData, EaseCurveNodeData,
@ -253,6 +254,54 @@ function selectedModelFromProvider(model: ProviderModel): SelectedModel {
}; };
} }
function identifierFromUnknown(value: unknown): string | null {
if (typeof value === "string" && value.trim().length > 0) return value.trim();
if (typeof value === "number" && Number.isFinite(value)) return String(Math.trunc(value));
return null;
}
function getSelectedModelIdentifiers(model: SelectedModel): Set<string> {
return new Set(
[
model.modelId,
model.displayName,
identifierFromUnknown(model.metadata?.aiModelId),
identifierFromUnknown(model.metadata?.aiModelCode),
identifierFromUnknown(model.metadata?.aiModelCodeAlias),
]
.filter((value): value is string => Boolean(value))
.map((value) => value.trim().toLowerCase())
.filter(Boolean)
);
}
function providerModelMatchesSelectedModel(model: ProviderModel, selectedModel: SelectedModel): boolean {
if (model.provider !== selectedModel.provider) return false;
const identifiers = getSelectedModelIdentifiers(selectedModel);
return [
model.id,
model.name,
identifierFromUnknown(model.metadata?.aiModelId),
identifierFromUnknown(model.metadata?.aiModelCode),
identifierFromUnknown(model.metadata?.aiModelCodeAlias),
]
.filter((value): value is string => Boolean(value))
.some((value) => identifiers.has(value.trim().toLowerCase()));
}
function findProviderModelForSelectedModel(models: ProviderModel[], selectedModel: SelectedModel): ProviderModel | null {
return models.find((model) => providerModelMatchesSelectedModel(model, selectedModel)) ?? null;
}
function selectedModelNeedsPopiserverMetadataLookup(model: SelectedModel): boolean {
if (model.provider !== "popiserver") return false;
const billingMethod = finiteIntegerFromUnknown(model.metadata?.billingMethod);
if (billingMethod === null) return true;
if (billingMethod !== 1) return false;
const aiModelId = finiteIntegerFromUnknown(model.metadata?.aiModelId) ?? finiteIntegerFromUnknown(model.modelId);
return !aiModelId;
}
function isSameSelectedModel(a: SelectedModel | undefined, b: SelectedModel | undefined): boolean { function isSameSelectedModel(a: SelectedModel | undefined, b: SelectedModel | undefined): boolean {
return Boolean(a?.provider && b?.provider && a.provider === b.provider && a.modelId === b.modelId); return Boolean(a?.provider && b?.provider && a.provider === b.provider && a.modelId === b.modelId);
} }
@ -393,6 +442,10 @@ function getPopiserverModelName(model: SelectedModel): string {
); );
} }
function isPopiserverPerRunBillingModel(model: SelectedModel): boolean {
return model.provider === "popiserver" && finiteIntegerFromUnknown(model.metadata?.billingMethod) === 1;
}
function buildSeedanceVideoEstimatePayload( function buildSeedanceVideoEstimatePayload(
draft: ComposerDraft, draft: ComposerDraft,
inputVideoDurationSeconds: number inputVideoDurationSeconds: number
@ -456,6 +509,7 @@ function buildPopiserverTaskPricePayload(
): PopiserverTaskPricePayload | null { ): PopiserverTaskPricePayload | null {
const model = draft.selectedModel; const model = draft.selectedModel;
if (model.provider !== "popiserver") return null; if (model.provider !== "popiserver") return null;
if (!isPopiserverPerRunBillingModel(model)) return null;
const aiModelId = finiteIntegerFromUnknown(model.metadata?.aiModelId) ?? finiteIntegerFromUnknown(model.modelId); const aiModelId = finiteIntegerFromUnknown(model.metadata?.aiModelId) ?? finiteIntegerFromUnknown(model.modelId);
if (!aiModelId) return null; if (!aiModelId) return null;
@ -906,6 +960,8 @@ export function GenerationComposer() {
const regenerateNode = useWorkflowStore((state) => state.regenerateNode); const regenerateNode = useWorkflowStore((state) => state.regenerateNode);
const selectSingleNode = useWorkflowStore((state) => state.selectSingleNode); const selectSingleNode = useWorkflowStore((state) => state.selectSingleNode);
const getConnectedInputs = useWorkflowStore((state) => state.getConnectedInputs); const getConnectedInputs = useWorkflowStore((state) => state.getConnectedInputs);
const popiModels = useModelStore((state) => state.all);
const refreshPopiModels = useModelStore((state) => state.refreshPopiModels);
const { newApiWGBaseUrl } = useProviderApiKeys(); const { newApiWGBaseUrl } = useProviderApiKeys();
const { screenToFlowPosition } = useReactFlow(); const { screenToFlowPosition } = useReactFlow();
const { x: viewportX, y: viewportY, zoom } = useViewport(); const { x: viewportX, y: viewportY, zoom } = useViewport();
@ -1053,6 +1109,79 @@ export function GenerationComposer() {
? JSON.stringify(popiserverTaskPricePayload) ? JSON.stringify(popiserverTaskPricePayload)
: null; : null;
useEffect(() => {
const selectedModel = draft.selectedModel;
if (!selectedModelNeedsPopiserverMetadataLookup(selectedModel)) return;
let cancelled = false;
const applyResolvedModel = (models: ProviderModel[]): boolean => {
const matchedModel = findProviderModelForSelectedModel(models, selectedModel);
if (!matchedModel || finiteIntegerFromUnknown(matchedModel.metadata?.billingMethod) === null) {
return false;
}
const resolvedModel = selectedModelFromProvider(matchedModel);
setDraft((current) => {
if (
!isSameSelectedModel(current.selectedModel, selectedModel) ||
!selectedModelNeedsPopiserverMetadataLookup(current.selectedModel)
) {
return current;
}
const next = {
...current,
selectedModel: {
...resolvedModel,
capabilities: current.selectedModel.capabilities.length > 0
? current.selectedModel.capabilities
: resolvedModel.capabilities,
},
};
draftRef.current = next;
return next;
});
const activeContext = contextRef.current;
const nodeModel = activeContext.node?.data && "selectedModel" in activeContext.node.data
? (activeContext.node.data.selectedModel as SelectedModel | undefined)
: undefined;
if (
activeContext.mode === "node-edit" &&
activeContext.node &&
isSameSelectedModel(nodeModel, selectedModel) &&
nodeModel &&
selectedModelNeedsPopiserverMetadataLookup(nodeModel)
) {
updateNodeData(activeContext.node.id, {
selectedModel: {
...resolvedModel,
capabilities: nodeModel.capabilities.length > 0 ? nodeModel.capabilities : resolvedModel.capabilities,
},
});
}
return true;
};
if (applyResolvedModel(popiModels)) return;
void refreshPopiModels().then((models) => {
if (!cancelled) applyResolvedModel(models);
});
return () => {
cancelled = true;
};
}, [
draft.selectedModel.displayName,
draft.selectedModel.metadata,
draft.selectedModel.modelId,
draft.selectedModel.provider,
popiModels,
refreshPopiModels,
updateNodeData,
]);
useEffect(() => { useEffect(() => {
const selectedModel = draft.selectedModel; const selectedModel = draft.selectedModel;
if ( if (

7
src/components/nodes/GenerateAudioNode.tsx

@ -23,7 +23,7 @@ import { HandleLabel } from "./HandleLabel";
import { isPopiProviderMode, POPI_PROVIDER_ID } from "@/lib/providerMode"; import { isPopiProviderMode, POPI_PROVIDER_ID } from "@/lib/providerMode";
import { useI18n } from "@/i18n"; import { useI18n } from "@/i18n";
import { formatUserFacingError } from "@/utils/userFacingErrors"; import { formatUserFacingError } from "@/utils/userFacingErrors";
import { chooseModelFromList, toSelectedModel } from "@/utils/modelSelection"; import { chooseModelFromList, shouldUpdateSelectedModelFromProviderModel, toSelectedModel } from "@/utils/modelSelection";
import { useModelStore } from "@/store/modelStore"; import { useModelStore } from "@/store/modelStore";
type GenerateAudioNodeType = Node<GenerateAudioNodeData, "generateAudio">; type GenerateAudioNodeType = Node<GenerateAudioNodeData, "generateAudio">;
@ -136,10 +136,13 @@ export function GenerateAudioNode({ id, data, selected }: NodeProps<GenerateAudi
!popiProviderMode || popiserverAudioModels.some((model) => model.id === nodeData.selectedModel?.modelId) !popiProviderMode || popiserverAudioModels.some((model) => model.id === nodeData.selectedModel?.modelId)
), ),
refreshKey: nodeData.modelSchemaRequestId, refreshKey: nodeData.modelSchemaRequestId,
onLoaded: ({ inputs, parameters }) => { onLoaded: ({ inputs, parameters, model }) => {
updateNodeData(id, { updateNodeData(id, {
inputSchema: inputs, inputSchema: inputs,
parameterSchema: parameters, parameterSchema: parameters,
...(model && shouldUpdateSelectedModelFromProviderModel(nodeData.selectedModel, model)
? { selectedModel: toSelectedModel(model) }
: {}),
modelSchemaError: null, modelSchemaError: null,
}); });
}, },

7
src/components/nodes/GenerateImageNode.tsx

@ -26,7 +26,7 @@ import { useI18n } from "@/i18n";
import { MediaPreviewModal } from "@/components/MediaPreviewModal"; import { MediaPreviewModal } from "@/components/MediaPreviewModal";
import { createGeminiLegacyImageModel } from "@/store/utils/defaultImageModel"; import { createGeminiLegacyImageModel } from "@/store/utils/defaultImageModel";
import { getProviderDisplayName, isPopiProviderMode, POPI_PROVIDER_ID } from "@/lib/providerMode"; import { getProviderDisplayName, isPopiProviderMode, POPI_PROVIDER_ID } from "@/lib/providerMode";
import { chooseModelFromList, toSelectedModel } from "@/utils/modelSelection"; import { chooseModelFromList, shouldUpdateSelectedModelFromProviderModel, toSelectedModel } from "@/utils/modelSelection";
import { useModelStore } from "@/store/modelStore"; import { useModelStore } from "@/store/modelStore";
import { import {
isBrowserFileSystemPath, isBrowserFileSystemPath,
@ -514,10 +514,13 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
selectedModel: nodeData.selectedModel, selectedModel: nodeData.selectedModel,
enabled: Boolean(nodeData.selectedModel?.modelId) && !usesFirstClassImageControls && canFetchPopiModelSchema, enabled: Boolean(nodeData.selectedModel?.modelId) && !usesFirstClassImageControls && canFetchPopiModelSchema,
refreshKey: nodeData.modelSchemaRequestId, refreshKey: nodeData.modelSchemaRequestId,
onLoaded: ({ inputs, parameters }) => { onLoaded: ({ inputs, parameters, model }) => {
updateNodeData(id, { updateNodeData(id, {
inputSchema: inputs, inputSchema: inputs,
parameterSchema: parameters, parameterSchema: parameters,
...(model && shouldUpdateSelectedModelFromProviderModel(nodeData.selectedModel, model)
? { selectedModel: toSelectedModel(model) }
: {}),
modelSchemaError: null, modelSchemaError: null,
}); });
}, },

7
src/components/nodes/GenerateVideoNode.tsx

@ -26,7 +26,7 @@ import { HandleLabel } from "./HandleLabel";
import { useI18n } from "@/i18n"; import { useI18n } from "@/i18n";
import { MediaPreviewModal } from "@/components/MediaPreviewModal"; import { MediaPreviewModal } from "@/components/MediaPreviewModal";
import { getProviderDisplayName, isPopiProviderMode, POPI_PROVIDER_ID } from "@/lib/providerMode"; import { getProviderDisplayName, isPopiProviderMode, POPI_PROVIDER_ID } from "@/lib/providerMode";
import { chooseModelFromList, toSelectedModel } from "@/utils/modelSelection"; import { chooseModelFromList, shouldUpdateSelectedModelFromProviderModel, toSelectedModel } from "@/utils/modelSelection";
import { useModelStore } from "@/store/modelStore"; import { useModelStore } from "@/store/modelStore";
import { formatUserFacingError } from "@/utils/userFacingErrors"; import { formatUserFacingError } from "@/utils/userFacingErrors";
import { import {
@ -343,10 +343,13 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps<GenerateVide
selectedModel: nodeData.selectedModel, selectedModel: nodeData.selectedModel,
enabled: Boolean(nodeData.selectedModel?.modelId) && canFetchPopiModelSchema, enabled: Boolean(nodeData.selectedModel?.modelId) && canFetchPopiModelSchema,
refreshKey: nodeData.modelSchemaRequestId, refreshKey: nodeData.modelSchemaRequestId,
onLoaded: ({ inputs, parameters }) => { onLoaded: ({ inputs, parameters, model }) => {
updateNodeData(id, { updateNodeData(id, {
inputSchema: inputs, inputSchema: inputs,
parameterSchema: parameters, parameterSchema: parameters,
...(model && shouldUpdateSelectedModelFromProviderModel(nodeData.selectedModel, model)
? { selectedModel: toSelectedModel(model) }
: {}),
modelSchemaError: null, modelSchemaError: null,
}); });
}, },

11
src/components/nodes/ModelParameters.tsx

@ -85,8 +85,15 @@ function ModelParametersInner({
const defaults: Record<string, unknown> = {}; const defaults: Record<string, unknown> = {};
let hasNewDefaults = false; let hasNewDefaults = false;
for (const param of visibleSchema) { for (const param of visibleSchema) {
if (param.default !== undefined && baseParameters[param.name] === undefined) { const currentValue = baseParameters[param.name];
defaults[param.name] = param.default; const enumValues = Array.isArray(param.enum) ? param.enum : [];
const hasInvalidEnumValue =
enumValues.length > 0 &&
currentValue !== undefined &&
!enumValues.some((item) => String(item) === String(currentValue));
const defaultValue = param.default ?? enumValues[0];
if ((currentValue === undefined || hasInvalidEnumValue) && defaultValue !== undefined) {
defaults[param.name] = defaultValue;
hasNewDefaults = true; hasNewDefaults = true;
} }
} }

4
src/hooks/useSelectedModelSchema.ts

@ -2,12 +2,13 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { ModelInputDef, SelectedModel } from "@/types"; import { ModelInputDef, SelectedModel } from "@/types";
import { ModelParameter } from "@/lib/providers/types"; import { ModelParameter, ProviderModel } from "@/lib/providers/types";
import { useProviderApiKeys } from "@/store/workflowStore"; import { useProviderApiKeys } from "@/store/workflowStore";
interface ModelSchemaResult { interface ModelSchemaResult {
parameters: ModelParameter[]; parameters: ModelParameter[];
inputs: ModelInputDef[]; inputs: ModelInputDef[];
model?: ProviderModel;
} }
interface UseSelectedModelSchemaOptions { interface UseSelectedModelSchemaOptions {
@ -89,6 +90,7 @@ export function useSelectedModelSchema({
const nextSchema = { const nextSchema = {
parameters: data.parameters || [], parameters: data.parameters || [],
inputs: data.inputs || [], inputs: data.inputs || [],
model: data.model,
}; };
setParameters(nextSchema.parameters); setParameters(nextSchema.parameters);
setInputs(nextSchema.inputs); setInputs(nextSchema.inputs);

19
src/utils/modelSelection.ts

@ -11,6 +11,25 @@ export function toSelectedModel(model: ProviderModel): SelectedModel {
}; };
} }
function hasValue(value: unknown): boolean {
return value !== undefined && value !== null && value !== "";
}
export function shouldUpdateSelectedModelFromProviderModel(
current: SelectedModel | undefined,
model: ProviderModel
): boolean {
if (!current) return true;
if (current.provider !== model.provider || current.modelId !== model.id) return true;
if (model.provider !== "popiserver") return false;
const currentMetadata = current.metadata ?? {};
const nextMetadata = model.metadata ?? {};
return ["billingMethod", "aiModelId", "type", "subType", "aiModelCode", "aiModelCodeAlias"].some(
(key) => !hasValue(currentMetadata[key]) && hasValue(nextMetadata[key])
);
}
export function chooseModelFromList( export function chooseModelFromList(
models: ProviderModel[], models: ProviderModel[],
preferred?: SelectedModel preferred?: SelectedModel

Loading…
Cancel
Save