smoltalk 0.13.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/dist/clients/google.js +4 -3
- package/dist/clients/llamaCppLoader.d.ts +4 -0
- package/dist/embed/mlx.d.ts +15 -0
- package/dist/embed/mlx.js +24 -0
- package/dist/embed/openai.d.ts +11 -1
- package/dist/embed/openai.js +29 -7
- package/dist/embed.d.ts +6 -0
- package/dist/embed.js +46 -0
- package/dist/util/jsonSchema.d.ts +14 -0
- package/dist/util/jsonSchema.js +83 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -276,7 +276,7 @@ const r = await textSync({
|
|
|
276
276
|
| `deepinfra` | ✅ | ✅ | ❌ (uses per-model endpoints, not OpenAI shape) | ❌ |
|
|
277
277
|
| `litellm` | ✅ | ✅ | ✅ (if the upstream model supports it) | ✅ (if upstream supports it) |
|
|
278
278
|
| `openai-compat` | ✅ | ✅ | ✅ (backend-dependent) | depends on backend |
|
|
279
|
-
| `mlx` | ✅ |
|
|
279
|
+
| `mlx` | ✅ | ✅ | ❌ | ❌ |
|
|
280
280
|
|
|
281
281
|
Smoltalk surfaces a clear `failure(...)` from `embed()`/`image()` for the
|
|
282
282
|
unsupported combinations rather than silently dropping the call.
|
|
@@ -543,6 +543,11 @@ const { resolveModel } = await loadLlamaCpp({
|
|
|
543
543
|
const modelPath = await resolveModel("hf:org/repo/model.gguf", "/models/cache");
|
|
544
544
|
```
|
|
545
545
|
|
|
546
|
+
`embed()` with `provider: "llama-cpp"` and a local `.gguf` path works the
|
|
547
|
+
same way and needs `smoltalk-llama-cpp` >= 0.5.0; the vector is computed in
|
|
548
|
+
process. See that package's README for the embedding caveats (dimension
|
|
549
|
+
truncation, one model file per role).
|
|
550
|
+
|
|
546
551
|
## Audio (STT/TTS)
|
|
547
552
|
|
|
548
553
|
Three audio primitives. `transcribe()` (speech-to-text) and `speak()`
|
package/dist/clients/google.js
CHANGED
|
@@ -4,7 +4,7 @@ import { getLogger } from "../util/logger.js";
|
|
|
4
4
|
import { redactAttachments } from "../util/redact.js";
|
|
5
5
|
import { addCosts, addTokenUsage, success, } from "../types.js";
|
|
6
6
|
import { zodToGoogleTool } from "../util/tool.js";
|
|
7
|
-
import { responseFormatToJsonSchema } from "../util/jsonSchema.js";
|
|
7
|
+
import { responseFormatToJsonSchema, constToEnum } from "../util/jsonSchema.js";
|
|
8
8
|
import { normalizeGoogleStopReason } from "../util/stopReason.js";
|
|
9
9
|
import { SmolError, SmolContentPolicyError, SmolContextWindowExceededError, smolErrorForStatus, } from "../smolError.js";
|
|
10
10
|
import { extractHttpErrorFields } from "../util/httpError.js";
|
|
@@ -297,7 +297,8 @@ export class SmolGoogle extends BaseClient {
|
|
|
297
297
|
}
|
|
298
298
|
if (config.responseFormat) {
|
|
299
299
|
genConfig.responseMimeType = "application/json";
|
|
300
|
-
|
|
300
|
+
// Gemini ignores `const` but honours `enum`; see constToEnum.
|
|
301
|
+
genConfig.responseJsonSchema = constToEnum(responseFormatToJsonSchema(config.responseFormat));
|
|
301
302
|
}
|
|
302
303
|
if (config.thinking?.enabled) {
|
|
303
304
|
// Gemini only returns thought-summary parts (parts with `thought: true`,
|
|
@@ -520,7 +521,7 @@ export class SmolGoogle extends BaseClient {
|
|
|
520
521
|
const hasTools = config.tools && config.tools.length > 0;
|
|
521
522
|
const hasStructuredResponse = !!config.responseFormat;
|
|
522
523
|
if (hasTools && hasStructuredResponse) {
|
|
523
|
-
this.logger.
|
|
524
|
+
this.logger.warn("Gemini does not support streaming responses with both tool calls and structured response formats. Response format will be ignored.");
|
|
524
525
|
this.statelogClient?.debug("Google Gemini: streaming with tools + structured response not supported, ignoring response format", {});
|
|
525
526
|
request.config.responseMimeType = undefined;
|
|
526
527
|
request.config.responseJsonSchema = undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BaseClient } from "./baseClient.js";
|
|
2
|
+
import type { EmbedProvider } from "../embed.js";
|
|
2
3
|
/**
|
|
3
4
|
* Minimal structural view of smoltalk-llama-cpp's module. Declared here (not
|
|
4
5
|
* imported from the plugin) so smoltalk compiles without the plugin installed
|
|
@@ -7,6 +8,9 @@ import type { BaseClient } from "./baseClient.js";
|
|
|
7
8
|
export type LlamaCppModule = {
|
|
8
9
|
LlamaCPP: typeof BaseClient;
|
|
9
10
|
resolveModel: (uriOrPath: string, cacheDir: string) => Promise<string>;
|
|
11
|
+
/** Present from smoltalk-llama-cpp 0.5.0. Absent on older plugins, which
|
|
12
|
+
* then serve chat only. */
|
|
13
|
+
embed?: EmbedProvider;
|
|
10
14
|
};
|
|
11
15
|
type ImportFn = (specifier: string) => Promise<Record<string, unknown>>;
|
|
12
16
|
/**
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { EmbedConfig, EmbedResult } from "../embed.js";
|
|
2
|
+
import { Result } from "../types/result.js";
|
|
3
|
+
/**
|
|
4
|
+
* Embeddings from an MLX server on localhost (`agency local serve
|
|
5
|
+
* --embedding …`, or any server with an OpenAI-shaped /v1/embeddings
|
|
6
|
+
* route). The same call the OpenAI helper makes, with the two things the
|
|
7
|
+
* chat client also fixes: the key is a placeholder the server ignores, and
|
|
8
|
+
* the cost is zero. `dimensions` is passed through as-is; whether the
|
|
9
|
+
* server honours it depends on the server and model.
|
|
10
|
+
*
|
|
11
|
+
* Float encoding is requested because a base URL is given (see
|
|
12
|
+
* openaiEmbed): a local server that ignores the SDK's base64 default and
|
|
13
|
+
* returns float arrays would otherwise yield empty vectors.
|
|
14
|
+
*/
|
|
15
|
+
export declare function mlxEmbed(inputs: string[], config: EmbedConfig, baseURL: string): Promise<Result<EmbedResult>>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { success } from "../types/result.js";
|
|
2
|
+
import { openaiEmbed } from "./openai.js";
|
|
3
|
+
/**
|
|
4
|
+
* Embeddings from an MLX server on localhost (`agency local serve
|
|
5
|
+
* --embedding …`, or any server with an OpenAI-shaped /v1/embeddings
|
|
6
|
+
* route). The same call the OpenAI helper makes, with the two things the
|
|
7
|
+
* chat client also fixes: the key is a placeholder the server ignores, and
|
|
8
|
+
* the cost is zero. `dimensions` is passed through as-is; whether the
|
|
9
|
+
* server honours it depends on the server and model.
|
|
10
|
+
*
|
|
11
|
+
* Float encoding is requested because a base URL is given (see
|
|
12
|
+
* openaiEmbed): a local server that ignores the SDK's base64 default and
|
|
13
|
+
* returns float arrays would otherwise yield empty vectors.
|
|
14
|
+
*/
|
|
15
|
+
export async function mlxEmbed(inputs, config, baseURL) {
|
|
16
|
+
const result = await openaiEmbed(inputs, config, "mlx-local", baseURL);
|
|
17
|
+
if (!result.success) {
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
return success({
|
|
21
|
+
...result.value,
|
|
22
|
+
costEstimate: { inputCost: 0, outputCost: 0, totalCost: 0, currency: "USD" },
|
|
23
|
+
});
|
|
24
|
+
}
|
package/dist/embed/openai.d.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import { EmbedConfig, EmbedResult } from "../embed.js";
|
|
2
2
|
import { Result } from "../types/result.js";
|
|
3
|
+
export type OpenAiEmbedOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* Wire encoding to ask the server for. Left unset, real OpenAI (no
|
|
6
|
+
* baseURL) keeps the SDK's base64 default, and every other backend gets
|
|
7
|
+
* "float": the SDK decodes an unrequested reply as base64 no matter what
|
|
8
|
+
* came back, so a server that ignores the field and returns float arrays
|
|
9
|
+
* would yield empty vectors. Set explicitly to override either default.
|
|
10
|
+
*/
|
|
11
|
+
encodingFormat?: "float" | "base64";
|
|
12
|
+
};
|
|
3
13
|
/**
|
|
4
14
|
* OpenAI-compatible embedding call. Used by openai directly and by other
|
|
5
15
|
* OpenAI-shape backends (deepinfra, litellm, openai-compat) which pass a
|
|
6
16
|
* custom `baseURL`. Cost comes from the smoltalk model registry; provider-
|
|
7
17
|
* returned cost fields aren't standardized on this endpoint.
|
|
8
18
|
*/
|
|
9
|
-
export declare function openaiEmbed(inputs: string[], config: EmbedConfig, apiKey: string, baseURL?: string): Promise<Result<EmbedResult>>;
|
|
19
|
+
export declare function openaiEmbed(inputs: string[], config: EmbedConfig, apiKey: string, baseURL?: string, options?: OpenAiEmbedOptions): Promise<Result<EmbedResult>>;
|
package/dist/embed/openai.js
CHANGED
|
@@ -2,25 +2,47 @@ import OpenAI from "openai";
|
|
|
2
2
|
import { success, failure } from "../types/result.js";
|
|
3
3
|
import { getModel, isEmbeddingsModel } from "../models.js";
|
|
4
4
|
import { round } from "../util/util.js";
|
|
5
|
+
/**
|
|
6
|
+
* A server may answer a float request with base64 anyway (some always
|
|
7
|
+
* encode). Once we name an encoding the SDK returns the body untouched, so
|
|
8
|
+
* handle both shapes here. Little-endian float32, the same layout the SDK's
|
|
9
|
+
* own decoder assumes.
|
|
10
|
+
*/
|
|
11
|
+
function toFloats(embedding) {
|
|
12
|
+
if (typeof embedding === "string") {
|
|
13
|
+
const bytes = Buffer.from(embedding, "base64");
|
|
14
|
+
const view = new Float32Array(bytes.buffer, bytes.byteOffset, Math.floor(bytes.byteLength / 4));
|
|
15
|
+
return Array.from(view);
|
|
16
|
+
}
|
|
17
|
+
return embedding;
|
|
18
|
+
}
|
|
5
19
|
/**
|
|
6
20
|
* OpenAI-compatible embedding call. Used by openai directly and by other
|
|
7
21
|
* OpenAI-shape backends (deepinfra, litellm, openai-compat) which pass a
|
|
8
22
|
* custom `baseURL`. Cost comes from the smoltalk model registry; provider-
|
|
9
23
|
* returned cost fields aren't standardized on this endpoint.
|
|
10
24
|
*/
|
|
11
|
-
export async function openaiEmbed(inputs, config, apiKey, baseURL) {
|
|
25
|
+
export async function openaiEmbed(inputs, config, apiKey, baseURL, options) {
|
|
12
26
|
try {
|
|
13
27
|
const client = new OpenAI({ apiKey, ...(baseURL ? { baseURL } : {}) });
|
|
14
|
-
const
|
|
28
|
+
const body = {
|
|
15
29
|
model: config.model,
|
|
16
30
|
input: inputs,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
31
|
+
};
|
|
32
|
+
if (config.dimensions !== undefined) {
|
|
33
|
+
body.dimensions = config.dimensions;
|
|
34
|
+
}
|
|
35
|
+
let encodingFormat = options?.encodingFormat;
|
|
36
|
+
if (encodingFormat === undefined && baseURL !== undefined) {
|
|
37
|
+
encodingFormat = "float";
|
|
38
|
+
}
|
|
39
|
+
if (encodingFormat !== undefined) {
|
|
40
|
+
body.encoding_format = encodingFormat;
|
|
41
|
+
}
|
|
42
|
+
const response = await client.embeddings.create(body);
|
|
21
43
|
const embeddings = [...response.data]
|
|
22
44
|
.sort((a, b) => a.index - b.index)
|
|
23
|
-
.map((d) => d.embedding);
|
|
45
|
+
.map((d) => toFloats(d.embedding));
|
|
24
46
|
const inputTokens = response.usage.prompt_tokens;
|
|
25
47
|
const costEstimate = calculateEmbeddingCost(config.model, inputTokens, config.modelData);
|
|
26
48
|
return success({
|
package/dist/embed.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export type EmbedConfig = {
|
|
|
25
25
|
deepInfra?: string;
|
|
26
26
|
liteLlm?: string;
|
|
27
27
|
openAiCompat?: string;
|
|
28
|
+
mlx?: string;
|
|
28
29
|
/** Arbitrary provider names, for URLs targeting a custom-registered provider. */
|
|
29
30
|
[provider: string]: string | undefined;
|
|
30
31
|
};
|
|
@@ -39,4 +40,9 @@ export type EmbedResult = {
|
|
|
39
40
|
};
|
|
40
41
|
export type EmbedProvider = (inputs: string[], config: EmbedConfig) => Promise<Result<EmbedResult>>;
|
|
41
42
|
export declare function registerEmbeddingProvider(name: string, fn: EmbedProvider): void;
|
|
43
|
+
/** True when `name` has an embed provider registered through
|
|
44
|
+
* registerEmbeddingProvider. The built-in cases in embed() are not its
|
|
45
|
+
* concern, the same as hasProvider in client.ts. */
|
|
46
|
+
export declare function hasEmbeddingProvider(name: string): boolean;
|
|
47
|
+
export declare function unregisterEmbeddingProvider(name: string): boolean;
|
|
42
48
|
export declare function embed(input: string | string[], config: EmbedConfig): Promise<Result<EmbedResult>>;
|
package/dist/embed.js
CHANGED
|
@@ -3,12 +3,33 @@ import { resolveProvider, resolveApiKey, resolveBaseUrl } from "./util/provider.
|
|
|
3
3
|
import { openaiEmbed } from "./embed/openai.js";
|
|
4
4
|
import { googleEmbed } from "./embed/google.js";
|
|
5
5
|
import { ollamaEmbed } from "./embed/ollama.js";
|
|
6
|
+
import { mlxEmbed } from "./embed/mlx.js";
|
|
7
|
+
import { loadLlamaCpp } from "./clients/llamaCppLoader.js";
|
|
8
|
+
function errorMessage(err) {
|
|
9
|
+
if (err instanceof Error) {
|
|
10
|
+
return err.message;
|
|
11
|
+
}
|
|
12
|
+
return String(err);
|
|
13
|
+
}
|
|
6
14
|
// Null-prototype so provider names like "toString"/"__proto__" can't collide
|
|
7
15
|
// with Object.prototype or pollute the registry.
|
|
8
16
|
const registeredEmbedProviders = Object.create(null);
|
|
9
17
|
export function registerEmbeddingProvider(name, fn) {
|
|
10
18
|
registeredEmbedProviders[name] = fn;
|
|
11
19
|
}
|
|
20
|
+
/** True when `name` has an embed provider registered through
|
|
21
|
+
* registerEmbeddingProvider. The built-in cases in embed() are not its
|
|
22
|
+
* concern, the same as hasProvider in client.ts. */
|
|
23
|
+
export function hasEmbeddingProvider(name) {
|
|
24
|
+
return name in registeredEmbedProviders;
|
|
25
|
+
}
|
|
26
|
+
export function unregisterEmbeddingProvider(name) {
|
|
27
|
+
if (name in registeredEmbedProviders) {
|
|
28
|
+
delete registeredEmbedProviders[name];
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
12
33
|
export async function embed(input, config) {
|
|
13
34
|
const inputs = Array.isArray(input) ? input : [input];
|
|
14
35
|
let provider;
|
|
@@ -63,6 +84,31 @@ export async function embed(input, config) {
|
|
|
63
84
|
}
|
|
64
85
|
return openaiEmbed(inputs, config, apiKey, baseURL);
|
|
65
86
|
}
|
|
87
|
+
case "mlx": {
|
|
88
|
+
// resolveBaseUrl always returns a value for "mlx" (it has a default).
|
|
89
|
+
return mlxEmbed(inputs, config, resolveBaseUrl("mlx", config));
|
|
90
|
+
}
|
|
91
|
+
case "llama-cpp": {
|
|
92
|
+
// A hand-registered provider wins, the same rule loadLlamaCpp applies
|
|
93
|
+
// to the chat class. Otherwise load the plugin the way text() does;
|
|
94
|
+
// the loader caches its import.
|
|
95
|
+
const custom = registeredEmbedProviders[provider];
|
|
96
|
+
if (custom) {
|
|
97
|
+
return custom(inputs, config);
|
|
98
|
+
}
|
|
99
|
+
let plugin;
|
|
100
|
+
try {
|
|
101
|
+
plugin = await loadLlamaCpp();
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
return failure(errorMessage(err));
|
|
105
|
+
}
|
|
106
|
+
if (typeof plugin.embed !== "function") {
|
|
107
|
+
return failure("Your installed smoltalk-llama-cpp has no embeddings support. " +
|
|
108
|
+
"Upgrade it (npm i smoltalk-llama-cpp@latest; >=0.5.0 required).");
|
|
109
|
+
}
|
|
110
|
+
return plugin.embed(inputs, config);
|
|
111
|
+
}
|
|
66
112
|
default: {
|
|
67
113
|
const custom = registeredEmbedProviders[provider];
|
|
68
114
|
if (custom) {
|
|
@@ -36,3 +36,17 @@ export declare function responseFormatToJsonSchema(schema: {
|
|
|
36
36
|
* there are left as-is and only an object subschema is sanitized.
|
|
37
37
|
*/
|
|
38
38
|
export declare function sanitizeJsonSchema(node: unknown): unknown;
|
|
39
|
+
/**
|
|
40
|
+
* Rewrite `const` to a one-value `enum`, recursing through the same
|
|
41
|
+
* subschema positions as `sanitizeJsonSchema` (value slots and the
|
|
42
|
+
* anyOf/oneOf/allOf/prefixItems arrays; assertion positions such as `not` and
|
|
43
|
+
* `contains` are left untouched), and collapse an `anyOf` whose branches are
|
|
44
|
+
* all single-type enums of the same type into one `{type, enum}` node.
|
|
45
|
+
*
|
|
46
|
+
* Gemini's `responseJsonSchema` silently ignores `const` (verified live: a Zod
|
|
47
|
+
* union of string literals — emitted as `anyOf: [{type:"string", const:"a"}, …]`
|
|
48
|
+
* — came back as free prose), but it enforces `enum`. Other providers accept
|
|
49
|
+
* `const`, so only the Google client applies this. Annotations on each node
|
|
50
|
+
* are preserved; a mixed-type anyOf stays an anyOf with each const rewritten.
|
|
51
|
+
*/
|
|
52
|
+
export declare function constToEnum(node: unknown): unknown;
|
package/dist/util/jsonSchema.js
CHANGED
|
@@ -131,3 +131,86 @@ function sanitizeSubschema(value) {
|
|
|
131
131
|
return value;
|
|
132
132
|
return sanitizeJsonSchema(value);
|
|
133
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Rewrite `const` to a one-value `enum`, recursing through the same
|
|
136
|
+
* subschema positions as `sanitizeJsonSchema` (value slots and the
|
|
137
|
+
* anyOf/oneOf/allOf/prefixItems arrays; assertion positions such as `not` and
|
|
138
|
+
* `contains` are left untouched), and collapse an `anyOf` whose branches are
|
|
139
|
+
* all single-type enums of the same type into one `{type, enum}` node.
|
|
140
|
+
*
|
|
141
|
+
* Gemini's `responseJsonSchema` silently ignores `const` (verified live: a Zod
|
|
142
|
+
* union of string literals — emitted as `anyOf: [{type:"string", const:"a"}, …]`
|
|
143
|
+
* — came back as free prose), but it enforces `enum`. Other providers accept
|
|
144
|
+
* `const`, so only the Google client applies this. Annotations on each node
|
|
145
|
+
* are preserved; a mixed-type anyOf stays an anyOf with each const rewritten.
|
|
146
|
+
*/
|
|
147
|
+
export function constToEnum(node) {
|
|
148
|
+
if (typeof node !== "object" || node === null || Array.isArray(node)) {
|
|
149
|
+
return node;
|
|
150
|
+
}
|
|
151
|
+
const out = { ...node };
|
|
152
|
+
if ("const" in out) {
|
|
153
|
+
out.enum = [out.const];
|
|
154
|
+
delete out.const;
|
|
155
|
+
}
|
|
156
|
+
for (const key of SCHEMA_KEYS) {
|
|
157
|
+
if (key in out) {
|
|
158
|
+
out[key] = constToEnumSubschema(out[key]);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const key of SCHEMA_MAP_KEYS) {
|
|
162
|
+
const map = out[key];
|
|
163
|
+
if (map && typeof map === "object" && !Array.isArray(map)) {
|
|
164
|
+
const rewritten = Object.create(null);
|
|
165
|
+
for (const [name, sub] of Object.entries(map)) {
|
|
166
|
+
rewritten[name] = constToEnum(sub);
|
|
167
|
+
}
|
|
168
|
+
out[key] = rewritten;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
for (const key of SCHEMA_ARRAY_KEYS) {
|
|
172
|
+
const arr = out[key];
|
|
173
|
+
if (Array.isArray(arr)) {
|
|
174
|
+
out[key] = arr.map((sub) => constToEnum(sub));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if ("additionalProperties" in out) {
|
|
178
|
+
out.additionalProperties = constToEnumSubschema(out.additionalProperties);
|
|
179
|
+
}
|
|
180
|
+
return collapseEnumAnyOf(out);
|
|
181
|
+
}
|
|
182
|
+
function constToEnumSubschema(value) {
|
|
183
|
+
if (typeof value === "boolean") {
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
return constToEnum(value);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* `{anyOf: [{type:"string", enum:["a"]}, {type:"string", enum:["b"]}]}` →
|
|
190
|
+
* `{type:"string", enum:["a","b"]}`. Only collapses when every branch is
|
|
191
|
+
* exactly `{type, enum}` with one shared `type`, so no constraint is lost.
|
|
192
|
+
*/
|
|
193
|
+
function collapseEnumAnyOf(node) {
|
|
194
|
+
const branches = node.anyOf;
|
|
195
|
+
if (!Array.isArray(branches) || branches.length === 0) {
|
|
196
|
+
return node;
|
|
197
|
+
}
|
|
198
|
+
const first = branches[0];
|
|
199
|
+
const sharedType = typeof first === "object" && first !== null
|
|
200
|
+
? first.type
|
|
201
|
+
: undefined;
|
|
202
|
+
if (typeof sharedType !== "string") {
|
|
203
|
+
return node;
|
|
204
|
+
}
|
|
205
|
+
const isPlainEnum = (branch) => typeof branch === "object" &&
|
|
206
|
+
branch !== null &&
|
|
207
|
+
Object.keys(branch).every((key) => key === "type" || key === "enum") &&
|
|
208
|
+
branch.type === sharedType &&
|
|
209
|
+
Array.isArray(branch.enum);
|
|
210
|
+
if (!branches.every(isPlainEnum)) {
|
|
211
|
+
return node;
|
|
212
|
+
}
|
|
213
|
+
const { anyOf, ...rest } = node;
|
|
214
|
+
const values = branches.flatMap((branch) => branch.enum);
|
|
215
|
+
return { ...rest, type: sharedType, enum: values };
|
|
216
|
+
}
|