PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.8
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.8
1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 0.8.6 All 33 releases
desktop-mode / includes / ai-copilot / schema.php

schema.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.8, at includes/ai-copilot/schema.php

131 lines 5.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — AI Copilot: structured-output schema normalization.
4 *
5 * Every schema handed to `as_json_response()` becomes the provider's
6 * structured-output contract (`output_format.schema` on Anthropic, the
7 * equivalent response-schema field elsewhere). Providers validate that
8 * contract in STRICT mode, which is narrower than JSON Schema: an object
9 * subschema that does not say `additionalProperties: false` is rejected
10 * outright —
11 *
12 * Bad Request (400) - output_format.schema: For 'object' type,
13 * 'additionalProperties' must be explicitly set to false
14 *
15 * — and the whole request fails, not just the offending branch. That is a
16 * trap for the schemas we ship (one missing key anywhere in the tree kills
17 * the feature) and worse for the filtered ones: `openstation_ai_schema_comment`,
18 * `openstation_drafts_ai_schema`, and any plugin that adds a nested object
19 * would otherwise have to know the provider's strict-mode rules to add a
20 * property safely.
21 *
22 * So normalization runs at the choke point instead of relying on every
23 * author getting it right: `openstation_ai_normalize_response_schema()`
24 * walks the tree and stamps `additionalProperties: false` on every object
25 * subschema, at every depth, after the filters have run.
26 *
27 * Tool INPUT schemas are a different contract with different rules — see
28 * `openstation_ai_normalize_tool_schema()` in search.php.
29 *
30 * @package OpenStation
31 */
32
33 defined( 'ABSPATH' ) || exit;
34
35 /**
36 * Forces a structured-output schema into the strict shape providers require.
37 *
38 * The only rewrite is `additionalProperties: false` on object subschemas —
39 * the key providers demand and JSON Schema treats as optional. A node counts
40 * as an object when its `type` is (or includes) `"object"`, or when it
41 * declares `properties` / `patternProperties` without a `type` at all. An
42 * existing `additionalProperties` is overwritten: `true` and schema-shaped
43 * values are exactly what strict mode rejects, so preserving them would only
44 * preserve the 400.
45 *
46 * The walk is structure-aware, covering every position a subschema can
47 * occupy: property values (maps keyed by PROPERTY NAME, so a property
48 * literally called `items` is never mistaken for the keyword), `items` in
49 * both single-schema and tuple form, `prefixItems`, `not`, the `oneOf` /
50 * `allOf` / `anyOf` branches, and `$defs` / `definitions` pools.
51 *
52 * @param array $schema A structured-output (sub)schema.
53 * @return array The schema with `additionalProperties: false` on every object node.
54 */
55 function openstation_ai_normalize_response_schema( array $schema ) {
56 if ( openstation_ai_schema_is_object_node( $schema ) ) {
57 $schema['additionalProperties'] = false;
58 if ( isset( $schema['properties'] ) && is_array( $schema['properties'] ) && array() !== $schema['properties'] ) {
59 // Strict structured output (OpenAI `strict: true`) also
60 // demands that `required` lists EVERY key in `properties` —
61 // optional fields do not exist in strict mode, and one
62 // missing key 400s the whole request ("'required' is
63 // required to be supplied and to be an array including
64 // every key in properties").
65 $schema['required'] = array_keys( $schema['properties'] );
66 }
67 }
68
69 foreach ( array( 'properties', 'patternProperties', '$defs', 'definitions' ) as $map_key ) {
70 if ( isset( $schema[ $map_key ] ) && is_array( $schema[ $map_key ] ) ) {
71 foreach ( $schema[ $map_key ] as $name => $sub ) {
72 if ( is_array( $sub ) ) {
73 $schema[ $map_key ][ $name ] = openstation_ai_normalize_response_schema( $sub );
74 }
75 }
76 }
77 }
78
79 if ( isset( $schema['items'] ) && is_array( $schema['items'] ) ) {
80 $items = $schema['items'];
81 $is_list = array_keys( $items ) === range( 0, count( $items ) - 1 );
82 if ( $is_list && array() !== $items ) {
83 // Tuple form — a list of schemas.
84 foreach ( $items as $i => $sub ) {
85 if ( is_array( $sub ) ) {
86 $items[ $i ] = openstation_ai_normalize_response_schema( $sub );
87 }
88 }
89 $schema['items'] = $items;
90 } else {
91 $schema['items'] = openstation_ai_normalize_response_schema( $items );
92 }
93 }
94
95 foreach ( array( 'oneOf', 'allOf', 'anyOf', 'prefixItems' ) as $list_key ) {
96 if ( isset( $schema[ $list_key ] ) && is_array( $schema[ $list_key ] ) ) {
97 foreach ( $schema[ $list_key ] as $i => $sub ) {
98 if ( is_array( $sub ) ) {
99 $schema[ $list_key ][ $i ] = openstation_ai_normalize_response_schema( $sub );
100 }
101 }
102 }
103 }
104
105 if ( isset( $schema['not'] ) && is_array( $schema['not'] ) ) {
106 $schema['not'] = openstation_ai_normalize_response_schema( $schema['not'] );
107 }
108
109 return $schema;
110 }
111
112 /**
113 * Whether a (sub)schema describes an object, and so needs the strict key.
114 *
115 * Covers the three ways a schema says "object": the literal type, a type
116 * union that includes it (`array( 'object', 'null' )` — how a nullable
117 * branch is usually written), and the untyped-but-shaped form that declares
118 * `properties` with no `type` at all.
119 *
120 * @param array $schema A structured-output (sub)schema.
121 * @return bool
122 */
123 function openstation_ai_schema_is_object_node( array $schema ) {
124 if ( isset( $schema['type'] ) ) {
125 $types = is_array( $schema['type'] ) ? $schema['type'] : array( $schema['type'] );
126 return in_array( 'object', $types, true );
127 }
128
129 return isset( $schema['properties'] ) || isset( $schema['patternProperties'] );
130 }
131