@gajae-code/ai 0.6.0 → 0.6.2
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/CHANGELOG.md +17 -0
- package/README.md +1 -0
- package/dist/types/providers/anthropic.d.ts +0 -6
- package/dist/types/types.d.ts +11 -0
- package/dist/types/utils/schema/index.d.ts +1 -0
- package/dist/types/utils/schema/root-combinator.d.ts +12 -0
- package/package.json +2 -2
- package/src/models.json +1 -1
- package/src/provider-models/openai-compat.ts +52 -1
- package/src/providers/amazon-bedrock.ts +2 -2
- package/src/providers/anthropic.ts +8 -123
- package/src/providers/azure-openai-responses.ts +2 -2
- package/src/providers/cursor.ts +2 -2
- package/src/providers/ollama.ts +2 -2
- package/src/providers/openai-codex-responses.ts +25 -5
- package/src/providers/openai-completions-compat.ts +2 -0
- package/src/providers/openai-completions.ts +12 -2
- package/src/providers/openai-responses.ts +8 -2
- package/src/types.ts +11 -0
- package/src/utils/schema/index.ts +1 -0
- package/src/utils/schema/root-combinator.ts +143 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.6.2] - 2026-06-19
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added opt-in `compat.sendSessionHeaders` for the `openai-completions` provider. When enabled (default off), the agent session id is forwarded as vendor-neutral `session_id` and `x-session-id` request headers to any OpenAI-compatible endpoint, letting relays/proxies do session-affinity routing and reuse a server-side prompt cache keyed by the session. Previously only the `openai-responses` provider injected session headers, and only against a first-party OpenAI base URL. Injection runs after the caller's `headers`/`extraHeaders` are merged and before `requestTransform`, and uses `??=` so any header the caller already set always wins; it is skipped entirely when the flag is off or no session id is available, leaving existing provider behavior byte-identical. The first-party `openai-responses` gating is unchanged.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Prevented OpenAI Codex Responses `invalid_function_parameters` / tool-schema validation error events from being treated as retryable `server_error`s, so malformed request schemas fail fast instead of burning the full retry budget.
|
|
13
|
+
- Read LM Studio `/v1/models` nested metadata such as `meta.n_ctx`, `meta.n_ctx_train`, and `details.max_tokens` when normalizing dynamically discovered GGUF-backed local models.
|
|
14
|
+
- Corrected the bundled `openai-codex/gpt-5.5` context window from an overstated 400K back to its true 272K (272,000-token) window, so context-cap / auto-compaction thresholds no longer let gpt-5.5 sessions overrun the model's real limit before compaction (#873).
|
|
15
|
+
|
|
16
|
+
## [0.6.1] - 2026-06-18
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Generalized tool `input_schema` root-combinator flattening across providers so discriminated-union tool inputs (e.g. the `computer` tool, a `z.union`) no longer ship a bare top-level `anyOf`/`oneOf`/`allOf` root that strict validators reject. The Anthropic-only fix from 0.5.4 is now the shared, provider-agnostic `flattenToolRootCombinators` (in `utils/schema`) and is applied by Amazon Bedrock, OpenAI Chat Completions / Responses / Codex-Responses / Azure-Responses, Ollama, and Cursor. Previously only Anthropic flattened the root, so those providers forwarded the union root verbatim and a union-root tool failed upstream — Bedrock Converse (including via Kiro/CodeWhisperer relays) returned `400 TOOL_SCHEMA_INVALID: The value at toolConfig.tools.N.toolSpec.inputSchema.json.type must be one of the following: object`. Anthropic behavior is unchanged (it now calls the shared util), Google / Cloud Code Assist keep their own object-merge, and object-root tools, nested combinators, and runtime Zod validation are all untouched.
|
|
21
|
+
|
|
5
22
|
## [0.6.0] - 2026-06-18
|
|
6
23
|
### Fixed
|
|
7
24
|
|
package/README.md
CHANGED
|
@@ -778,6 +778,7 @@ The `openai-completions` API is implemented by many providers with minor differe
|
|
|
778
778
|
interface OpenAICompat {
|
|
779
779
|
supportsStore?: boolean; // Whether provider supports the `store` field (default: true)
|
|
780
780
|
supportsDeveloperRole?: boolean; // Whether provider supports `developer` role vs `system` (default: true)
|
|
781
|
+
sendSessionHeaders?: boolean; // Forward the session id as `session_id`/`x-session-id` headers for relay session-affinity & prompt-cache reuse (default: false)
|
|
781
782
|
supportsReasoningEffort?: boolean; // Whether provider supports `reasoning_effort` (default: true)
|
|
782
783
|
maxTokensField?: "max_completion_tokens" | "max_tokens"; // Which field name to use (default: max_completion_tokens)
|
|
783
784
|
extraBody?: Record<string, unknown>; // Extra request-body fields for custom proxy routing or provider-specific options
|
|
@@ -189,10 +189,4 @@ export declare function convertAnthropicMessages(messages: Message[], model: Mod
|
|
|
189
189
|
* object, so callers like the resolve tool keep working open-map semantics.
|
|
190
190
|
*/
|
|
191
191
|
export declare function normalizeAnthropicToolSchema(schema: unknown): unknown;
|
|
192
|
-
/**
|
|
193
|
-
* Anthropic rejects tool `input_schema` roots containing top-level oneOf/anyOf/allOf.
|
|
194
|
-
* Keep the generic normalizer schema-preserving, then flatten only provider-emitted
|
|
195
|
-
* tool roots into one object while leaving nested combinators untouched.
|
|
196
|
-
*/
|
|
197
|
-
export declare function normalizeAnthropicToolRootInputSchema(schema: Record<string, unknown>): Record<string, unknown>;
|
|
198
192
|
export {};
|
package/dist/types/types.d.ts
CHANGED
|
@@ -606,6 +606,17 @@ export interface OpenAICompat extends ToolChoiceCompat {
|
|
|
606
606
|
supportsStore?: boolean;
|
|
607
607
|
/** Whether the provider supports the `developer` role (vs `system`). Default: auto-detected from URL. */
|
|
608
608
|
supportsDeveloperRole?: boolean;
|
|
609
|
+
/**
|
|
610
|
+
* Whether to forward the agent session id as vendor-neutral session-identity
|
|
611
|
+
* headers (`session_id`, `x-session-id`) on every chat-completions request.
|
|
612
|
+
* Off by default. Opt in for OpenAI-compatible proxies/relays that route on
|
|
613
|
+
* session affinity or reuse a server-side prompt cache keyed by session.
|
|
614
|
+
* First-party OpenAI does not need this (it has its own gated injection in
|
|
615
|
+
* the openai-responses provider). Headers are only added when a non-empty
|
|
616
|
+
* session id is available and are never allowed to overwrite a header the
|
|
617
|
+
* caller already set via `headers`/`requestTransform`.
|
|
618
|
+
*/
|
|
619
|
+
sendSessionHeaders?: boolean;
|
|
609
620
|
/**
|
|
610
621
|
* Whether the provider's chat-completions endpoint accepts multiple
|
|
611
622
|
* leading `system`/`developer` messages. When false, ordered system
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** True when a JSON Schema node describes an object (explicit `type` or `properties`). */
|
|
2
|
+
export declare function isJsonSchemaObjectNode(schema: Record<string, unknown>): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Flatten a provider-emitted tool ROOT whose top level is a `oneOf`/`anyOf`/`allOf`
|
|
5
|
+
* combinator into one `type: "object"` schema: merge object-branch properties,
|
|
6
|
+
* derive the discriminant (`action`) enum, keep the common required set, and demote
|
|
7
|
+
* leftover combinators plus per-branch guidance into the description. Nested
|
|
8
|
+
* combinators (inside individual properties) are left untouched.
|
|
9
|
+
*
|
|
10
|
+
* Idempotent: a root that already lacks top-level combinators is returned unchanged.
|
|
11
|
+
*/
|
|
12
|
+
export declare function flattenToolRootCombinators(schema: Record<string, unknown>): Record<string, unknown>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/ai",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.2",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://gaebal-gajae.dev",
|
|
7
7
|
"author": "Yeachan-Heo",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@anthropic-ai/sdk": "^0.94.0",
|
|
45
45
|
"@bufbuild/protobuf": "^2.12.0",
|
|
46
|
-
"@gajae-code/utils": "0.6.
|
|
46
|
+
"@gajae-code/utils": "0.6.2",
|
|
47
47
|
"openai": "^6.36.0",
|
|
48
48
|
"partial-json": "^0.1.7",
|
|
49
49
|
"zod": "4.4.3"
|
package/src/models.json
CHANGED
|
@@ -53332,7 +53332,7 @@
|
|
|
53332
53332
|
"cacheRead": 0.5,
|
|
53333
53333
|
"cacheWrite": 0
|
|
53334
53334
|
},
|
|
53335
|
-
"contextWindow":
|
|
53335
|
+
"contextWindow": 272000,
|
|
53336
53336
|
"maxTokens": 128000,
|
|
53337
53337
|
"preferWebsockets": true,
|
|
53338
53338
|
"priority": 9,
|
|
@@ -176,6 +176,57 @@ function mapWithBundledReference<TApi extends Api>(
|
|
|
176
176
|
};
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
function getNestedModelValue(entry: OpenAICompatibleModelRecord, path: readonly string[]): unknown {
|
|
180
|
+
let current: unknown = entry;
|
|
181
|
+
for (const segment of path) {
|
|
182
|
+
if (!isRecord(current)) {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
current = current[segment];
|
|
186
|
+
}
|
|
187
|
+
return current;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function firstPositiveModelNumber(fallback: number, ...candidates: readonly unknown[]): number {
|
|
191
|
+
for (const candidate of candidates) {
|
|
192
|
+
const value = toNumber(candidate);
|
|
193
|
+
if (value !== undefined && value > 0) {
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return fallback;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function mapLmStudioModel(
|
|
201
|
+
entry: OpenAICompatibleModelRecord,
|
|
202
|
+
defaults: Model<"openai-completions">,
|
|
203
|
+
reference: Model<"openai-completions"> | undefined,
|
|
204
|
+
): Model<"openai-completions"> {
|
|
205
|
+
const model = mapWithBundledReference(entry, defaults, reference);
|
|
206
|
+
return {
|
|
207
|
+
...model,
|
|
208
|
+
contextWindow: firstPositiveModelNumber(
|
|
209
|
+
model.contextWindow,
|
|
210
|
+
entry.context_length,
|
|
211
|
+
entry.max_context_length,
|
|
212
|
+
getNestedModelValue(entry, ["meta", "n_ctx"]),
|
|
213
|
+
getNestedModelValue(entry, ["details", "context_length"]),
|
|
214
|
+
getNestedModelValue(entry, ["details", "n_ctx"]),
|
|
215
|
+
getNestedModelValue(entry, ["meta", "n_ctx_train"]),
|
|
216
|
+
),
|
|
217
|
+
maxTokens: firstPositiveModelNumber(
|
|
218
|
+
model.maxTokens,
|
|
219
|
+
entry.max_completion_tokens,
|
|
220
|
+
entry.max_tokens,
|
|
221
|
+
entry.max_output_tokens,
|
|
222
|
+
getNestedModelValue(entry, ["details", "max_completion_tokens"]),
|
|
223
|
+
getNestedModelValue(entry, ["details", "max_tokens"]),
|
|
224
|
+
getNestedModelValue(entry, ["meta", "max_completion_tokens"]),
|
|
225
|
+
getNestedModelValue(entry, ["meta", "max_tokens"]),
|
|
226
|
+
),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
179
230
|
function normalizeAnthropicBaseUrl(baseUrl: string | undefined, fallback: string): string {
|
|
180
231
|
const value = baseUrl?.trim();
|
|
181
232
|
if (!value) {
|
|
@@ -1215,7 +1266,7 @@ export function lmStudioModelManagerOptions(
|
|
|
1215
1266
|
apiKey,
|
|
1216
1267
|
mapModel: (entry, defaults) => {
|
|
1217
1268
|
const reference = references.get(defaults.id);
|
|
1218
|
-
return
|
|
1269
|
+
return mapLmStudioModel(entry, defaults, reference);
|
|
1219
1270
|
},
|
|
1220
1271
|
}),
|
|
1221
1272
|
};
|
|
@@ -33,7 +33,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
|
33
33
|
import { appendRawHttpRequestDumpFor400, type RawHttpRequestDump, withHttpStatus } from "../utils/http-inspector";
|
|
34
34
|
import { parseStreamingJson } from "../utils/json-parse";
|
|
35
35
|
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
36
|
-
import { toolWireSchema } from "../utils/schema
|
|
36
|
+
import { flattenToolRootCombinators, toolWireSchema } from "../utils/schema";
|
|
37
37
|
import {
|
|
38
38
|
isForcedToolChoiceUnsupportedError,
|
|
39
39
|
markToolChoiceIncapability,
|
|
@@ -808,7 +808,7 @@ export function convertToolConfig(
|
|
|
808
808
|
toolSpec: {
|
|
809
809
|
name: tool.name,
|
|
810
810
|
description: tool.description || "",
|
|
811
|
-
inputSchema: { json: toolWireSchema(tool) },
|
|
811
|
+
inputSchema: { json: flattenToolRootCombinators(toolWireSchema(tool)) },
|
|
812
812
|
},
|
|
813
813
|
}));
|
|
814
814
|
|
|
@@ -62,7 +62,13 @@ import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
|
|
|
62
62
|
import { notifyProviderResponse } from "../utils/provider-response";
|
|
63
63
|
import { isCopilotTransientModelError } from "../utils/retry";
|
|
64
64
|
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
65
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
COMBINATOR_KEYS,
|
|
67
|
+
flattenToolRootCombinators,
|
|
68
|
+
isJsonSchemaObjectNode,
|
|
69
|
+
NO_STRICT,
|
|
70
|
+
toolWireSchema,
|
|
71
|
+
} from "../utils/schema";
|
|
66
72
|
import { spillToDescription } from "../utils/schema/spill";
|
|
67
73
|
import { notifyRawSseEvent, wrapFetchForSseDebug } from "../utils/sse-debug";
|
|
68
74
|
import {
|
|
@@ -2406,22 +2412,6 @@ const MAX_ANTHROPIC_STRICT_TOOLS = 20;
|
|
|
2406
2412
|
const MAX_ANTHROPIC_STRICT_OPTIONAL_PARAMETERS = 24;
|
|
2407
2413
|
const MAX_ANTHROPIC_STRICT_UNION_PARAMETERS = 16;
|
|
2408
2414
|
|
|
2409
|
-
/** `minItems` / `maxItems` apply to arrays; Anthropic rejects them on `type: "object"` (including `minItems: 0`/`1`). */
|
|
2410
|
-
function isJsonSchemaArrayNode(schema: Record<string, unknown>): boolean {
|
|
2411
|
-
const t = schema.type;
|
|
2412
|
-
if (t === "array") return true;
|
|
2413
|
-
if (Array.isArray(t) && t.includes("array") && !t.includes("object")) return true;
|
|
2414
|
-
return false;
|
|
2415
|
-
}
|
|
2416
|
-
|
|
2417
|
-
function isJsonSchemaObjectNode(schema: Record<string, unknown>): boolean {
|
|
2418
|
-
if (isJsonSchemaArrayNode(schema)) return false;
|
|
2419
|
-
if (schema.type === "object") return true;
|
|
2420
|
-
if (Array.isArray(schema.type) && schema.type.includes("object")) return true;
|
|
2421
|
-
if (isRecord(schema.properties)) return true;
|
|
2422
|
-
return false;
|
|
2423
|
-
}
|
|
2424
|
-
|
|
2425
2415
|
/**
|
|
2426
2416
|
* Pick the principal non-null scalar type from a `type` keyword. Anthropic accepts
|
|
2427
2417
|
* `type` as either a single string or an array (e.g. `["number", "null"]` for a
|
|
@@ -2573,111 +2563,6 @@ export function normalizeAnthropicToolSchema(schema: unknown): unknown {
|
|
|
2573
2563
|
return result;
|
|
2574
2564
|
}
|
|
2575
2565
|
|
|
2576
|
-
function getRequiredNames(schema: Record<string, unknown>): Set<string> {
|
|
2577
|
-
return new Set(
|
|
2578
|
-
Array.isArray(schema.required)
|
|
2579
|
-
? schema.required.filter((entry): entry is string => typeof entry === "string")
|
|
2580
|
-
: [],
|
|
2581
|
-
);
|
|
2582
|
-
}
|
|
2583
|
-
|
|
2584
|
-
function getSingleLiteralValue(schema: unknown): unknown | undefined {
|
|
2585
|
-
if (!isRecord(schema)) return undefined;
|
|
2586
|
-
if (Object.hasOwn(schema, "const")) return schema.const;
|
|
2587
|
-
if (Array.isArray(schema.enum) && schema.enum.length === 1) return schema.enum[0];
|
|
2588
|
-
return undefined;
|
|
2589
|
-
}
|
|
2590
|
-
|
|
2591
|
-
function describeAnthropicRootBranch(index: number, branch: Record<string, unknown>, action: unknown): string {
|
|
2592
|
-
const required = [...getRequiredNames(branch)];
|
|
2593
|
-
const parts = [`Branch ${index + 1}`];
|
|
2594
|
-
if (typeof action === "string" || typeof action === "number" || typeof action === "boolean") {
|
|
2595
|
-
parts.push(`action ${JSON.stringify(action)}`);
|
|
2596
|
-
}
|
|
2597
|
-
if (required.length > 0) parts.push(`branch-required fields: ${required.join(", ")}`);
|
|
2598
|
-
if (typeof branch.description === "string" && branch.description.length > 0) parts.push(branch.description);
|
|
2599
|
-
return parts.join("; ");
|
|
2600
|
-
}
|
|
2601
|
-
function collectAnthropicRootObjectBranches(schema: unknown): Record<string, unknown>[] | undefined {
|
|
2602
|
-
if (!isRecord(schema)) return undefined;
|
|
2603
|
-
if (isJsonSchemaObjectNode(schema)) return [schema];
|
|
2604
|
-
|
|
2605
|
-
const combinatorKeys = COMBINATOR_KEYS.filter(key => Array.isArray(schema[key]));
|
|
2606
|
-
if (combinatorKeys.length === 0) return undefined;
|
|
2607
|
-
|
|
2608
|
-
const branches: Record<string, unknown>[] = [];
|
|
2609
|
-
for (const key of combinatorKeys) {
|
|
2610
|
-
const variants = schema[key];
|
|
2611
|
-
if (!Array.isArray(variants) || variants.length === 0) return undefined;
|
|
2612
|
-
for (const variant of variants) {
|
|
2613
|
-
const nestedBranches = collectAnthropicRootObjectBranches(variant);
|
|
2614
|
-
if (nestedBranches === undefined) return undefined;
|
|
2615
|
-
branches.push(...nestedBranches);
|
|
2616
|
-
}
|
|
2617
|
-
}
|
|
2618
|
-
return branches;
|
|
2619
|
-
}
|
|
2620
|
-
|
|
2621
|
-
/**
|
|
2622
|
-
* Anthropic rejects tool `input_schema` roots containing top-level oneOf/anyOf/allOf.
|
|
2623
|
-
* Keep the generic normalizer schema-preserving, then flatten only provider-emitted
|
|
2624
|
-
* tool roots into one object while leaving nested combinators untouched.
|
|
2625
|
-
*/
|
|
2626
|
-
export function normalizeAnthropicToolRootInputSchema(schema: Record<string, unknown>): Record<string, unknown> {
|
|
2627
|
-
const result: Record<string, unknown> = { ...schema };
|
|
2628
|
-
const rootCombinators = COMBINATOR_KEYS.filter(key => Array.isArray(result[key]));
|
|
2629
|
-
if (rootCombinators.length === 0) return result;
|
|
2630
|
-
|
|
2631
|
-
const baseProperties = isRecord(result.properties) ? { ...result.properties } : {};
|
|
2632
|
-
const flattenedBranches = rootCombinators
|
|
2633
|
-
.map(key => ({ key, branches: collectAnthropicRootObjectBranches({ [key]: result[key] }) }))
|
|
2634
|
-
.find(entry => entry.branches !== undefined && entry.branches.length > 0);
|
|
2635
|
-
|
|
2636
|
-
result.type = "object";
|
|
2637
|
-
result.properties = baseProperties;
|
|
2638
|
-
result.additionalProperties = result.additionalProperties === undefined ? false : result.additionalProperties;
|
|
2639
|
-
|
|
2640
|
-
if (flattenedBranches?.branches !== undefined) {
|
|
2641
|
-
const variants = flattenedBranches.branches;
|
|
2642
|
-
const commonRequired = variants.map(variant => getRequiredNames(variant));
|
|
2643
|
-
const required = [...commonRequired[0]].filter(name => commonRequired.every(set => set.has(name)));
|
|
2644
|
-
const actionValues: unknown[] = [];
|
|
2645
|
-
const guidance: string[] = [];
|
|
2646
|
-
|
|
2647
|
-
for (const [index, branch] of variants.entries()) {
|
|
2648
|
-
if (isRecord(branch.properties)) {
|
|
2649
|
-
Object.assign(baseProperties, branch.properties);
|
|
2650
|
-
const actionValue = getSingleLiteralValue(branch.properties.action);
|
|
2651
|
-
if (actionValue !== undefined && !actionValues.includes(actionValue)) actionValues.push(actionValue);
|
|
2652
|
-
guidance.push(describeAnthropicRootBranch(index, branch, actionValue));
|
|
2653
|
-
} else {
|
|
2654
|
-
guidance.push(describeAnthropicRootBranch(index, branch, undefined));
|
|
2655
|
-
}
|
|
2656
|
-
}
|
|
2657
|
-
|
|
2658
|
-
if (actionValues.length > 0) {
|
|
2659
|
-
const existingAction = isRecord(baseProperties.action) ? { ...baseProperties.action } : {};
|
|
2660
|
-
delete existingAction.const;
|
|
2661
|
-
baseProperties.action = { ...existingAction, enum: actionValues };
|
|
2662
|
-
}
|
|
2663
|
-
result.required = required;
|
|
2664
|
-
spillToDescription(result, [
|
|
2665
|
-
["rootCombinatorGuidance", guidance],
|
|
2666
|
-
...rootCombinators
|
|
2667
|
-
.filter(key => key !== flattenedBranches.key)
|
|
2668
|
-
.map(key => [key, result[key]] as [string, unknown]),
|
|
2669
|
-
]);
|
|
2670
|
-
} else {
|
|
2671
|
-
spillToDescription(
|
|
2672
|
-
result,
|
|
2673
|
-
rootCombinators.map(key => [key, result[key]]),
|
|
2674
|
-
);
|
|
2675
|
-
}
|
|
2676
|
-
|
|
2677
|
-
for (const key of COMBINATOR_KEYS) delete result[key];
|
|
2678
|
-
return result;
|
|
2679
|
-
}
|
|
2680
|
-
|
|
2681
2566
|
type AnthropicToolInputSchema = Anthropic.Messages.Tool["input_schema"];
|
|
2682
2567
|
|
|
2683
2568
|
type AnthropicToolSchemaPlan = {
|
|
@@ -2845,7 +2730,7 @@ function normalizeAnthropicStrictSchema(
|
|
|
2845
2730
|
|
|
2846
2731
|
function buildAnthropicBaseToolInputSchema(tool: Tool): Record<string, unknown> {
|
|
2847
2732
|
const jsonSchema = toolWireSchema(tool);
|
|
2848
|
-
return
|
|
2733
|
+
return flattenToolRootCombinators(
|
|
2849
2734
|
normalizeAnthropicToolSchema({
|
|
2850
2735
|
...jsonSchema,
|
|
2851
2736
|
type: "object",
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
iterateWithIdleTimeout,
|
|
28
28
|
} from "../utils/idle-iterator";
|
|
29
29
|
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
30
|
-
import { sanitizeSchemaForOpenAIResponses, toolWireSchema } from "../utils/schema";
|
|
30
|
+
import { flattenToolRootCombinators, sanitizeSchemaForOpenAIResponses, toolWireSchema } from "../utils/schema";
|
|
31
31
|
import { wrapFetchForSseDebug } from "../utils/sse-debug";
|
|
32
32
|
import { mapToOpenAIResponsesToolChoice } from "../utils/tool-choice";
|
|
33
33
|
import {
|
|
@@ -373,7 +373,7 @@ function convertTools(tools: Tool[]): OpenAITool[] {
|
|
|
373
373
|
type: "function",
|
|
374
374
|
name: tool.name,
|
|
375
375
|
description: tool.description || "",
|
|
376
|
-
parameters: sanitizeSchemaForOpenAIResponses(toolWireSchema(tool)),
|
|
376
|
+
parameters: sanitizeSchemaForOpenAIResponses(flattenToolRootCombinators(toolWireSchema(tool))),
|
|
377
377
|
strict: false,
|
|
378
378
|
}));
|
|
379
379
|
}
|
package/src/providers/cursor.ts
CHANGED
|
@@ -29,7 +29,7 @@ import { normalizeSystemPrompts } from "../utils";
|
|
|
29
29
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
30
30
|
import { parseStreamingJson } from "../utils/json-parse";
|
|
31
31
|
import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
|
|
32
|
-
import { toolWireSchema } from "../utils/schema
|
|
32
|
+
import { flattenToolRootCombinators, toolWireSchema } from "../utils/schema";
|
|
33
33
|
import { COMPOSER_EDIT_DISCIPLINE_PROMPT, isComposerHarnessModel } from "./composer-discipline";
|
|
34
34
|
import type { McpToolDefinition } from "./cursor/gen/agent_pb";
|
|
35
35
|
import {
|
|
@@ -2183,7 +2183,7 @@ function buildMcpToolDefinitions(tools: Tool[] | undefined): McpToolDefinition[]
|
|
|
2183
2183
|
}
|
|
2184
2184
|
|
|
2185
2185
|
return advertisedTools.map(tool => {
|
|
2186
|
-
const jsonSchema = toolWireSchema(tool);
|
|
2186
|
+
const jsonSchema = flattenToolRootCombinators(toolWireSchema(tool));
|
|
2187
2187
|
const schemaValue: JsonValue =
|
|
2188
2188
|
jsonSchema && typeof jsonSchema === "object"
|
|
2189
2189
|
? (jsonSchema as JsonValue)
|
package/src/providers/ollama.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
|
19
19
|
import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
|
|
20
20
|
import { parseStreamingJson } from "../utils/json-parse";
|
|
21
21
|
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
22
|
-
import { toolWireSchema } from "../utils/schema
|
|
22
|
+
import { flattenToolRootCombinators, toolWireSchema } from "../utils/schema";
|
|
23
23
|
import {
|
|
24
24
|
isForcedToolChoiceUnsupportedError,
|
|
25
25
|
markToolChoiceIncapability,
|
|
@@ -253,7 +253,7 @@ function convertTools(tools: Tool[] | undefined): OllamaFunctionTool[] | undefin
|
|
|
253
253
|
function: {
|
|
254
254
|
name: tool.name,
|
|
255
255
|
description: tool.description,
|
|
256
|
-
parameters: toolWireSchema(tool),
|
|
256
|
+
parameters: flattenToolRootCombinators(toolWireSchema(tool)),
|
|
257
257
|
},
|
|
258
258
|
}));
|
|
259
259
|
}
|
|
@@ -50,7 +50,13 @@ import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-ins
|
|
|
50
50
|
import { getOpenAIStreamIdleTimeoutMs, iterateWithIdleTimeout } from "../utils/idle-iterator";
|
|
51
51
|
import { parseStreamingJson } from "../utils/json-parse";
|
|
52
52
|
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
53
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
adaptSchemaForStrict,
|
|
55
|
+
flattenToolRootCombinators,
|
|
56
|
+
NO_STRICT,
|
|
57
|
+
sanitizeSchemaForOpenAIResponses,
|
|
58
|
+
toolWireSchema,
|
|
59
|
+
} from "../utils/schema";
|
|
54
60
|
import {
|
|
55
61
|
isForcedToolChoiceUnsupportedError,
|
|
56
62
|
markToolChoiceIncapability,
|
|
@@ -98,6 +104,14 @@ const CODEX_WEBSOCKET_RETRY_BUDGET = CODEX_MAX_RETRIES;
|
|
|
98
104
|
const CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX = "Codex websocket transport error";
|
|
99
105
|
const CODEX_PREVIOUS_RESPONSE_STALE_CODES = new Set(["previous_response_not_found", "codex_previous_response_stale"]);
|
|
100
106
|
const CODEX_RETRYABLE_EVENT_CODES = new Set(["model_error", "server_error", "internal_error"]);
|
|
107
|
+
const CODEX_NON_RETRYABLE_EVENT_CODES = new Set([
|
|
108
|
+
"invalid_function_parameters",
|
|
109
|
+
"invalid_request_error",
|
|
110
|
+
"invalid_schema",
|
|
111
|
+
"invalid_tool_schema",
|
|
112
|
+
]);
|
|
113
|
+
const CODEX_NON_RETRYABLE_EVENT_MESSAGE =
|
|
114
|
+
/invalid[_ -]function[_ -]parameters|invalid schema for function|invalid[_ -]tool[_ -]schema|schema must have type ["']?object["']?/i;
|
|
101
115
|
const CODEX_RETRYABLE_EVENT_MESSAGE =
|
|
102
116
|
/processing your request|retry your request|temporar(?:y|ily)|overloaded|service.?unavailable|internal error|server error/i;
|
|
103
117
|
const CODEX_PROVIDER_SESSION_STATE_KEY = "openai-codex-responses";
|
|
@@ -2658,7 +2672,7 @@ export function convertOpenAICodexResponsesTools(
|
|
|
2658
2672
|
};
|
|
2659
2673
|
}
|
|
2660
2674
|
const strict = !!(!NO_STRICT && tool.strict);
|
|
2661
|
-
const baseParameters = sanitizeSchemaForOpenAIResponses(toolWireSchema(tool));
|
|
2675
|
+
const baseParameters = sanitizeSchemaForOpenAIResponses(flattenToolRootCombinators(toolWireSchema(tool)));
|
|
2662
2676
|
const { schema: parameters, strict: effectiveStrict } = adaptSchemaForStrict(baseParameters, strict);
|
|
2663
2677
|
return {
|
|
2664
2678
|
type: "function",
|
|
@@ -2703,11 +2717,17 @@ class CodexProviderStreamError extends Error {
|
|
|
2703
2717
|
}
|
|
2704
2718
|
|
|
2705
2719
|
function isRetryableCodexFailureEvent(rawEvent: Record<string, unknown>): boolean {
|
|
2706
|
-
const code = getCodexEventErrorCode(rawEvent);
|
|
2707
|
-
|
|
2720
|
+
const code = getCodexEventErrorCode(rawEvent).toLowerCase();
|
|
2721
|
+
const message = getCodexEventErrorMessage(rawEvent);
|
|
2722
|
+
if (
|
|
2723
|
+
(code && CODEX_NON_RETRYABLE_EVENT_CODES.has(code)) ||
|
|
2724
|
+
(!!message && CODEX_NON_RETRYABLE_EVENT_MESSAGE.test(message))
|
|
2725
|
+
) {
|
|
2726
|
+
return false;
|
|
2727
|
+
}
|
|
2728
|
+
if (code && CODEX_RETRYABLE_EVENT_CODES.has(code)) {
|
|
2708
2729
|
return true;
|
|
2709
2730
|
}
|
|
2710
|
-
const message = getCodexEventErrorMessage(rawEvent);
|
|
2711
2731
|
return !!message && CODEX_RETRYABLE_EVENT_MESSAGE.test(message);
|
|
2712
2732
|
}
|
|
2713
2733
|
|
|
@@ -189,6 +189,7 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB
|
|
|
189
189
|
return {
|
|
190
190
|
supportsStore: !isNonStandard,
|
|
191
191
|
supportsDeveloperRole: !isNonStandard,
|
|
192
|
+
sendSessionHeaders: false,
|
|
192
193
|
supportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,
|
|
193
194
|
supportsReasoningEffort: !isGrok && !isZai,
|
|
194
195
|
reasoningEffortMap,
|
|
@@ -254,6 +255,7 @@ export function resolveOpenAICompat(
|
|
|
254
255
|
return {
|
|
255
256
|
supportsStore: model.compat.supportsStore ?? detected.supportsStore,
|
|
256
257
|
supportsDeveloperRole: model.compat.supportsDeveloperRole ?? detected.supportsDeveloperRole,
|
|
258
|
+
sendSessionHeaders: model.compat.sendSessionHeaders ?? detected.sendSessionHeaders,
|
|
257
259
|
supportsMultipleSystemMessages:
|
|
258
260
|
model.compat.supportsMultipleSystemMessages ?? detected.supportsMultipleSystemMessages,
|
|
259
261
|
supportsReasoningEffort: model.compat.supportsReasoningEffort ?? detected.supportsReasoningEffort,
|
|
@@ -57,7 +57,7 @@ import { getKimiCommonHeaders } from "../utils/oauth/kimi";
|
|
|
57
57
|
import { notifyProviderResponse } from "../utils/provider-response";
|
|
58
58
|
import { callWithCopilotModelRetry } from "../utils/retry";
|
|
59
59
|
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
60
|
-
import { adaptSchemaForStrict, NO_STRICT, toolWireSchema } from "../utils/schema";
|
|
60
|
+
import { adaptSchemaForStrict, flattenToolRootCombinators, NO_STRICT, toolWireSchema } from "../utils/schema";
|
|
61
61
|
import { wrapFetchForSseDebug } from "../utils/sse-debug";
|
|
62
62
|
import { type HealedToolCall, modelMayLeakKimiToolCalls, ToolCallHealer } from "../utils/tool-call-healing";
|
|
63
63
|
import { isForcedToolChoice, mapToOpenAICompletionsToolChoice } from "../utils/tool-choice";
|
|
@@ -453,6 +453,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
453
453
|
options?.streamFirstEventTimeoutMs,
|
|
454
454
|
options?.authCredentialType,
|
|
455
455
|
options?.requestMaxRetries,
|
|
456
|
+
options?.sessionId,
|
|
456
457
|
);
|
|
457
458
|
const premiumRequestsTotal = copilotPremiumRequests;
|
|
458
459
|
getCapturedErrorResponse = captureErrorResponse;
|
|
@@ -943,6 +944,7 @@ async function createClient(
|
|
|
943
944
|
streamFirstEventTimeoutOverride?: number,
|
|
944
945
|
authCredentialType?: OpenAICompletionsOptions["authCredentialType"],
|
|
945
946
|
requestMaxRetries?: number,
|
|
947
|
+
sessionId?: string,
|
|
946
948
|
): Promise<{
|
|
947
949
|
client: OpenAI;
|
|
948
950
|
copilotPremiumRequests: number | undefined;
|
|
@@ -981,6 +983,14 @@ async function createClient(
|
|
|
981
983
|
headers["X-OpenRouter-Cache-TTL"] = "3600";
|
|
982
984
|
}
|
|
983
985
|
Object.assign(headers, extraHeaders);
|
|
986
|
+
if (sessionId && resolveOpenAICompat(model).sendSessionHeaders) {
|
|
987
|
+
// Forward the agent session id as vendor-neutral session-identity headers so
|
|
988
|
+
// OpenAI-compatible proxies/relays can do session-affinity routing and reuse a
|
|
989
|
+
// server-side prompt cache. Opt-in via `compat.sendSessionHeaders`; never
|
|
990
|
+
// overwrite a header the caller already set (model.headers / requestTransform).
|
|
991
|
+
headers.session_id ??= sessionId;
|
|
992
|
+
headers["x-session-id"] ??= sessionId;
|
|
993
|
+
}
|
|
984
994
|
if (model.provider === "kimi-code") {
|
|
985
995
|
headers = { ...getKimiCommonHeaders(), ...headers };
|
|
986
996
|
}
|
|
@@ -1832,7 +1842,7 @@ function convertTools(
|
|
|
1832
1842
|
): BuiltOpenAICompletionTools {
|
|
1833
1843
|
const adaptedTools = tools.map(tool => {
|
|
1834
1844
|
const strict = !NO_STRICT && compat.supportsStrictMode !== false && tool.strict !== false;
|
|
1835
|
-
const baseParameters = toolWireSchema(tool);
|
|
1845
|
+
const baseParameters = flattenToolRootCombinators(toolWireSchema(tool));
|
|
1836
1846
|
const adapted = adaptSchemaForStrict(baseParameters, strict);
|
|
1837
1847
|
return {
|
|
1838
1848
|
tool,
|
|
@@ -50,7 +50,13 @@ import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
|
|
|
50
50
|
import { notifyProviderResponse } from "../utils/provider-response";
|
|
51
51
|
import { callWithCopilotModelRetry } from "../utils/retry";
|
|
52
52
|
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
53
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
adaptSchemaForStrict,
|
|
55
|
+
flattenToolRootCombinators,
|
|
56
|
+
NO_STRICT,
|
|
57
|
+
sanitizeSchemaForOpenAIResponses,
|
|
58
|
+
toolWireSchema,
|
|
59
|
+
} from "../utils/schema";
|
|
54
60
|
import { wrapFetchForSseDebug } from "../utils/sse-debug";
|
|
55
61
|
import { mapToOpenAIResponsesToolChoice, type OpenAIResponsesToolChoice } from "../utils/tool-choice";
|
|
56
62
|
import {
|
|
@@ -724,7 +730,7 @@ export function convertTools(tools: Tool[], strictMode: boolean, model: Model<"o
|
|
|
724
730
|
} as unknown as OpenAITool;
|
|
725
731
|
}
|
|
726
732
|
const strict = !NO_STRICT && strictMode && tool.strict !== false;
|
|
727
|
-
const baseParameters = toolWireSchema(tool);
|
|
733
|
+
const baseParameters = flattenToolRootCombinators(toolWireSchema(tool));
|
|
728
734
|
const responseParameters = sanitizeSchemaForOpenAIResponses(baseParameters);
|
|
729
735
|
const { schema: parameters, strict: effectiveStrict } = adaptSchemaForStrict(responseParameters, strict);
|
|
730
736
|
return {
|
package/src/types.ts
CHANGED
|
@@ -742,6 +742,17 @@ export interface OpenAICompat extends ToolChoiceCompat {
|
|
|
742
742
|
supportsStore?: boolean;
|
|
743
743
|
/** Whether the provider supports the `developer` role (vs `system`). Default: auto-detected from URL. */
|
|
744
744
|
supportsDeveloperRole?: boolean;
|
|
745
|
+
/**
|
|
746
|
+
* Whether to forward the agent session id as vendor-neutral session-identity
|
|
747
|
+
* headers (`session_id`, `x-session-id`) on every chat-completions request.
|
|
748
|
+
* Off by default. Opt in for OpenAI-compatible proxies/relays that route on
|
|
749
|
+
* session affinity or reuse a server-side prompt cache keyed by session.
|
|
750
|
+
* First-party OpenAI does not need this (it has its own gated injection in
|
|
751
|
+
* the openai-responses provider). Headers are only added when a non-empty
|
|
752
|
+
* session id is available and are never allowed to overwrite a header the
|
|
753
|
+
* caller already set via `headers`/`requestTransform`.
|
|
754
|
+
*/
|
|
755
|
+
sendSessionHeaders?: boolean;
|
|
745
756
|
/**
|
|
746
757
|
* Whether the provider's chat-completions endpoint accepts multiple
|
|
747
758
|
* leading `system`/`developer` messages. When false, ordered system
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool `input_schema` root flattening.
|
|
3
|
+
*
|
|
4
|
+
* Tool/function-calling `input_schema` roots MUST be a single JSON Schema object.
|
|
5
|
+
* Bedrock Converse (`toolConfig.tools[*].toolSpec.inputSchema.json.type must be
|
|
6
|
+
* one of the following: object`), OpenAI strict mode, Gemini, and Anthropic all
|
|
7
|
+
* reject roots whose top level is a bare `oneOf`/`anyOf`/`allOf` combinator (the
|
|
8
|
+
* validators require `type: "object"`).
|
|
9
|
+
*
|
|
10
|
+
* Zod `z.union(...)` / `z.discriminatedUnion(...)` tool parameters serialize to
|
|
11
|
+
* exactly such a combinator root, so this normalization is provider-agnostic and
|
|
12
|
+
* runs inside the shared wire pipeline (`toolWireSchema`).
|
|
13
|
+
*/
|
|
14
|
+
import { isRecord } from "@gajae-code/utils";
|
|
15
|
+
import { COMBINATOR_KEYS } from "./fields";
|
|
16
|
+
import { spillToDescription } from "./spill";
|
|
17
|
+
|
|
18
|
+
/** `minItems` / `maxItems` apply to arrays; some validators reject them on `type: "object"`. */
|
|
19
|
+
function isJsonSchemaArrayNode(schema: Record<string, unknown>): boolean {
|
|
20
|
+
const t = schema.type;
|
|
21
|
+
if (t === "array") return true;
|
|
22
|
+
if (Array.isArray(t) && t.includes("array") && !t.includes("object")) return true;
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** True when a JSON Schema node describes an object (explicit `type` or `properties`). */
|
|
27
|
+
export function isJsonSchemaObjectNode(schema: Record<string, unknown>): boolean {
|
|
28
|
+
if (isJsonSchemaArrayNode(schema)) return false;
|
|
29
|
+
if (schema.type === "object") return true;
|
|
30
|
+
if (Array.isArray(schema.type) && schema.type.includes("object")) return true;
|
|
31
|
+
if (isRecord(schema.properties)) return true;
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getRequiredNames(schema: Record<string, unknown>): Set<string> {
|
|
36
|
+
return new Set(
|
|
37
|
+
Array.isArray(schema.required)
|
|
38
|
+
? schema.required.filter((entry): entry is string => typeof entry === "string")
|
|
39
|
+
: [],
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getSingleLiteralValue(schema: unknown): unknown | undefined {
|
|
44
|
+
if (!isRecord(schema)) return undefined;
|
|
45
|
+
if (Object.hasOwn(schema, "const")) return schema.const;
|
|
46
|
+
if (Array.isArray(schema.enum) && schema.enum.length === 1) return schema.enum[0];
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function describeRootBranch(index: number, branch: Record<string, unknown>, action: unknown): string {
|
|
51
|
+
const required = [...getRequiredNames(branch)];
|
|
52
|
+
const parts = [`Branch ${index + 1}`];
|
|
53
|
+
if (typeof action === "string" || typeof action === "number" || typeof action === "boolean") {
|
|
54
|
+
parts.push(`action ${JSON.stringify(action)}`);
|
|
55
|
+
}
|
|
56
|
+
if (required.length > 0) parts.push(`branch-required fields: ${required.join(", ")}`);
|
|
57
|
+
if (typeof branch.description === "string" && branch.description.length > 0) parts.push(branch.description);
|
|
58
|
+
return parts.join("; ");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function collectRootObjectBranches(schema: unknown): Record<string, unknown>[] | undefined {
|
|
62
|
+
if (!isRecord(schema)) return undefined;
|
|
63
|
+
if (isJsonSchemaObjectNode(schema)) return [schema];
|
|
64
|
+
|
|
65
|
+
const combinatorKeys = COMBINATOR_KEYS.filter(key => Array.isArray(schema[key]));
|
|
66
|
+
if (combinatorKeys.length === 0) return undefined;
|
|
67
|
+
|
|
68
|
+
const branches: Record<string, unknown>[] = [];
|
|
69
|
+
for (const key of combinatorKeys) {
|
|
70
|
+
const variants = schema[key];
|
|
71
|
+
if (!Array.isArray(variants) || variants.length === 0) return undefined;
|
|
72
|
+
for (const variant of variants) {
|
|
73
|
+
const nestedBranches = collectRootObjectBranches(variant);
|
|
74
|
+
if (nestedBranches === undefined) return undefined;
|
|
75
|
+
branches.push(...nestedBranches);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return branches;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Flatten a provider-emitted tool ROOT whose top level is a `oneOf`/`anyOf`/`allOf`
|
|
83
|
+
* combinator into one `type: "object"` schema: merge object-branch properties,
|
|
84
|
+
* derive the discriminant (`action`) enum, keep the common required set, and demote
|
|
85
|
+
* leftover combinators plus per-branch guidance into the description. Nested
|
|
86
|
+
* combinators (inside individual properties) are left untouched.
|
|
87
|
+
*
|
|
88
|
+
* Idempotent: a root that already lacks top-level combinators is returned unchanged.
|
|
89
|
+
*/
|
|
90
|
+
export function flattenToolRootCombinators(schema: Record<string, unknown>): Record<string, unknown> {
|
|
91
|
+
const rootCombinators = COMBINATOR_KEYS.filter(key => Array.isArray(schema[key]));
|
|
92
|
+
if (rootCombinators.length === 0) return schema;
|
|
93
|
+
const result: Record<string, unknown> = { ...schema };
|
|
94
|
+
|
|
95
|
+
const baseProperties = isRecord(result.properties) ? { ...result.properties } : {};
|
|
96
|
+
const flattenedBranches = rootCombinators
|
|
97
|
+
.map(key => ({ key, branches: collectRootObjectBranches({ [key]: result[key] }) }))
|
|
98
|
+
.find(entry => entry.branches !== undefined && entry.branches.length > 0);
|
|
99
|
+
|
|
100
|
+
result.type = "object";
|
|
101
|
+
result.properties = baseProperties;
|
|
102
|
+
result.additionalProperties = result.additionalProperties === undefined ? false : result.additionalProperties;
|
|
103
|
+
|
|
104
|
+
if (flattenedBranches?.branches !== undefined) {
|
|
105
|
+
const variants = flattenedBranches.branches;
|
|
106
|
+
const commonRequired = variants.map(variant => getRequiredNames(variant));
|
|
107
|
+
const required = [...commonRequired[0]].filter(name => commonRequired.every(set => set.has(name)));
|
|
108
|
+
const actionValues: unknown[] = [];
|
|
109
|
+
const guidance: string[] = [];
|
|
110
|
+
|
|
111
|
+
for (const [index, branch] of variants.entries()) {
|
|
112
|
+
if (isRecord(branch.properties)) {
|
|
113
|
+
Object.assign(baseProperties, branch.properties);
|
|
114
|
+
const actionValue = getSingleLiteralValue(branch.properties.action);
|
|
115
|
+
if (actionValue !== undefined && !actionValues.includes(actionValue)) actionValues.push(actionValue);
|
|
116
|
+
guidance.push(describeRootBranch(index, branch, actionValue));
|
|
117
|
+
} else {
|
|
118
|
+
guidance.push(describeRootBranch(index, branch, undefined));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (actionValues.length > 0) {
|
|
123
|
+
const existingAction = isRecord(baseProperties.action) ? { ...baseProperties.action } : {};
|
|
124
|
+
delete existingAction.const;
|
|
125
|
+
baseProperties.action = { ...existingAction, enum: actionValues };
|
|
126
|
+
}
|
|
127
|
+
result.required = required;
|
|
128
|
+
spillToDescription(result, [
|
|
129
|
+
["rootCombinatorGuidance", guidance],
|
|
130
|
+
...rootCombinators
|
|
131
|
+
.filter(key => key !== flattenedBranches.key)
|
|
132
|
+
.map(key => [key, result[key]] as [string, unknown]),
|
|
133
|
+
]);
|
|
134
|
+
} else {
|
|
135
|
+
spillToDescription(
|
|
136
|
+
result,
|
|
137
|
+
rootCombinators.map(key => [key, result[key]]),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const key of COMBINATOR_KEYS) delete result[key];
|
|
142
|
+
return result;
|
|
143
|
+
}
|