fix(ui): handle SecretInput union in config form analyzer

The config form marks models.providers as unsupported because
SecretInputSchema creates a oneOf union that the form analyzer
cannot handle. Add detection for secret-ref union variants and
normalize them to plain string inputs for form display.

Closes #31490
This commit is contained in:
ningding97
2026-03-03 00:03:19 +08:00
committed by Peter Steinberger
parent 1727279598
commit 9c1312b5e4
2 changed files with 136 additions and 0 deletions

View File

@@ -304,6 +304,83 @@ describe("config form renderer", () => {
expect(noMatchContainer.textContent).toContain('No settings match "mode tag:security"');
});
it("supports SecretInput unions in additionalProperties maps", () => {
const onPatch = vi.fn();
const container = document.createElement("div");
const schema = {
type: "object",
properties: {
models: {
type: "object",
properties: {
providers: {
type: "object",
additionalProperties: {
type: "object",
properties: {
apiKey: {
anyOf: [
{ type: "string" },
{
oneOf: [
{
type: "object",
properties: {
source: { type: "string", const: "env" },
provider: { type: "string" },
id: { type: "string" },
},
required: ["source", "provider", "id"],
additionalProperties: false,
},
{
type: "object",
properties: {
source: { type: "string", const: "file" },
provider: { type: "string" },
id: { type: "string" },
},
required: ["source", "provider", "id"],
additionalProperties: false,
},
],
},
],
},
},
},
},
},
},
},
};
const analysis = analyzeConfigSchema(schema);
expect(analysis.unsupportedPaths).not.toContain("models.providers");
expect(analysis.unsupportedPaths).not.toContain("models.providers.*.apiKey");
render(
renderConfigForm({
schema: analysis.schema,
uiHints: {
"models.providers.*.apiKey": { sensitive: true },
},
unsupportedPaths: analysis.unsupportedPaths,
value: { models: { providers: { openai: { apiKey: "old" } } } },
onPatch,
}),
container,
);
const apiKeyInput: HTMLInputElement | null = container.querySelector("input[type='password']");
expect(apiKeyInput).not.toBeNull();
if (!apiKeyInput) {
return;
}
apiKeyInput.value = "new-key";
apiKeyInput.dispatchEvent(new Event("input", { bubbles: true }));
expect(onPatch).toHaveBeenCalledWith(["models", "providers", "openai", "apiKey"], "new-key");
});
it("flags unsupported unions", () => {
const schema = {
type: "object",

View File

@@ -118,6 +118,58 @@ function normalizeSchemaNode(
};
}
function isSecretRefVariant(entry: JsonSchema): boolean {
if (schemaType(entry) !== "object") {
return false;
}
const source = entry.properties?.source;
const provider = entry.properties?.provider;
const id = entry.properties?.id;
if (!source || !provider || !id) {
return false;
}
return (
typeof source.const === "string" &&
schemaType(provider) === "string" &&
schemaType(id) === "string"
);
}
function isSecretRefUnion(entry: JsonSchema): boolean {
const variants = entry.oneOf ?? entry.anyOf;
if (!variants || variants.length === 0) {
return false;
}
return variants.every((variant) => isSecretRefVariant(variant));
}
function normalizeSecretInputUnion(
schema: JsonSchema,
path: Array<string | number>,
remaining: JsonSchema[],
nullable: boolean,
): ConfigSchemaAnalysis | null {
const stringIndex = remaining.findIndex((entry) => schemaType(entry) === "string");
if (stringIndex < 0) {
return null;
}
const nonString = remaining.filter((_, index) => index !== stringIndex);
if (nonString.length !== 1 || !isSecretRefUnion(nonString[0])) {
return null;
}
return normalizeSchemaNode(
{
...schema,
...remaining[stringIndex],
nullable,
anyOf: undefined,
oneOf: undefined,
allOf: undefined,
},
path,
);
}
function normalizeUnion(
schema: JsonSchema,
path: Array<string | number>,
@@ -161,6 +213,13 @@ function normalizeUnion(
remaining.push(entry);
}
// Config secrets accept either a raw key string or a structured secret ref object.
// The form only supports editing the string path for now.
const secretInput = normalizeSecretInputUnion(schema, path, remaining, nullable);
if (secretInput) {
return secretInput;
}
if (literals.length > 0 && remaining.length === 0) {
const unique: unknown[] = [];
for (const value of literals) {