dsh-plugin-subscription-oauth 0.0.0-stage → 0.2.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.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +413 -2
  3. package/cordis.patch.yml +13 -0
  4. package/lib/auth/device-flow.d.ts +112 -0
  5. package/lib/auth/device-flow.js +319 -0
  6. package/lib/auth/device-id.d.ts +31 -0
  7. package/lib/auth/device-id.js +58 -0
  8. package/lib/auth/flow.d.ts +33 -0
  9. package/lib/auth/flow.js +12 -0
  10. package/lib/auth/glm-flow.d.ts +99 -0
  11. package/lib/auth/glm-flow.js +194 -0
  12. package/lib/auth/headers.d.ts +61 -0
  13. package/lib/auth/headers.js +106 -0
  14. package/lib/auth/pkce-flow.d.ts +78 -0
  15. package/lib/auth/pkce-flow.js +205 -0
  16. package/lib/auth/provider-controller.d.ts +241 -0
  17. package/lib/auth/provider-controller.js +502 -0
  18. package/lib/auth/registry.d.ts +91 -0
  19. package/lib/auth/registry.js +149 -0
  20. package/lib/auth/rpc.d.ts +31 -0
  21. package/lib/auth/rpc.js +200 -0
  22. package/lib/auth/store.d.ts +99 -0
  23. package/lib/auth/store.js +213 -0
  24. package/lib/auth/tokens.d.ts +76 -0
  25. package/lib/auth/tokens.js +103 -0
  26. package/lib/client/SubscriptionsSection.d.ts +34 -0
  27. package/lib/client/SubscriptionsSection.js +742 -0
  28. package/lib/client/index.d.ts +36 -0
  29. package/lib/client/index.js +47 -0
  30. package/lib/client/locales.d.ts +187 -0
  31. package/lib/client/locales.js +185 -0
  32. package/lib/client/styles.d.ts +18 -0
  33. package/lib/client/styles.js +257 -0
  34. package/lib/client/subscriptions-rpc.d.ts +24 -0
  35. package/lib/client/subscriptions-rpc.js +44 -0
  36. package/lib/client.js +2078 -0
  37. package/lib/client.js.map +1 -0
  38. package/lib/constants.d.ts +79 -0
  39. package/lib/constants.js +91 -0
  40. package/lib/errors.d.ts +28 -0
  41. package/lib/errors.js +34 -0
  42. package/lib/http.d.ts +24 -0
  43. package/lib/http.js +98 -0
  44. package/lib/index.d.ts +57 -0
  45. package/lib/index.js +185 -0
  46. package/lib/protocol.d.ts +316 -0
  47. package/lib/protocol.js +29 -0
  48. package/lib/providers/antigravity-catalog.d.ts +53 -0
  49. package/lib/providers/antigravity-catalog.js +269 -0
  50. package/lib/providers/antigravity.d.ts +331 -0
  51. package/lib/providers/antigravity.js +821 -0
  52. package/lib/providers/auto.d.ts +151 -0
  53. package/lib/providers/auto.js +351 -0
  54. package/lib/providers/catalog.d.ts +69 -0
  55. package/lib/providers/catalog.js +78 -0
  56. package/lib/providers/common.d.ts +51 -0
  57. package/lib/providers/common.js +169 -0
  58. package/lib/providers/glm-provider.d.ts +41 -0
  59. package/lib/providers/glm-provider.js +165 -0
  60. package/lib/providers/glm.d.ts +159 -0
  61. package/lib/providers/glm.js +445 -0
  62. package/lib/providers/grok.d.ts +224 -0
  63. package/lib/providers/grok.js +700 -0
  64. package/lib/providers/kimi-provider.d.ts +45 -0
  65. package/lib/providers/kimi-provider.js +148 -0
  66. package/lib/providers/kimi.d.ts +151 -0
  67. package/lib/providers/kimi.js +400 -0
  68. package/lib/providers/responses-stream.d.ts +58 -0
  69. package/lib/providers/responses-stream.js +115 -0
  70. package/lib/providers/usage.d.ts +65 -0
  71. package/lib/providers/usage.js +309 -0
  72. package/lib/settings.d.ts +122 -0
  73. package/lib/settings.js +333 -0
  74. package/lib/translate/antigravity-schema.d.ts +24 -0
  75. package/lib/translate/antigravity-schema.js +174 -0
  76. package/lib/translate/antigravity.d.ts +184 -0
  77. package/lib/translate/antigravity.js +419 -0
  78. package/lib/translate/responses.d.ts +156 -0
  79. package/lib/translate/responses.js +395 -0
  80. package/lib/translate/sse.d.ts +27 -0
  81. package/lib/translate/sse.js +82 -0
  82. package/package.json +126 -4
@@ -0,0 +1,333 @@
1
+ /**
2
+ * Model-visibility and model-order preferences.
3
+ *
4
+ * Which models appear in the session model picker, and in which order, is a user
5
+ * choice, so it lives on the node half beside the adapter that answers the
6
+ * picker's catalog query — not in the browser. The file is a small JSON document
7
+ * keyed by provider; a missing or corrupt section means "everything visible, in
8
+ * the curated order", which is the shipped default.
9
+ *
10
+ * The visibility set is a HIDDEN list rather than a visible list: a model that a
11
+ * later release adds to the catalog must appear without the user re-selecting it.
12
+ * The order is a partial rank: ids it does not mention keep their catalog
13
+ * position, so a new model lands without disturbing the user's arrangement.
14
+ *
15
+ * @module dsh-plugin-subscription-oauth/settings
16
+ */
17
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
18
+ import path from 'node:path';
19
+ /**
20
+ * Provider whose earlier, flat preference shape is migrated on load.
21
+ *
22
+ * Before this plugin served more than one route the document was
23
+ * `{ hidden: string[] }`; that list belonged to Kimi Code.
24
+ */
25
+ export const LEGACY_FLAT_PROVIDER = 'kimi';
26
+ /** Read the `auto` section, keeping only usable keys. */
27
+ function readAuto(document) {
28
+ if (!isRecord(document.auto))
29
+ return { order: [], off: [] };
30
+ return {
31
+ order: readIds(document.auto.order),
32
+ off: readIds(document.auto.off),
33
+ };
34
+ }
35
+ /** True for a plain object usable as a JSON record. */
36
+ function isRecord(value) {
37
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
38
+ }
39
+ /** Read one id list, keeping only usable ids and dropping repeats. */
40
+ function readIds(value) {
41
+ if (!Array.isArray(value))
42
+ return [];
43
+ const result = [];
44
+ const seen = new Set();
45
+ for (const id of value) {
46
+ if (typeof id !== 'string' || id.length === 0 || seen.has(id))
47
+ continue;
48
+ seen.add(id);
49
+ result.push(id);
50
+ }
51
+ return result;
52
+ }
53
+ /** Coerce the document into a per-provider hidden map, migrating the flat shape. */
54
+ function readHidden(document) {
55
+ const hidden = document.hidden;
56
+ if (Array.isArray(hidden)) {
57
+ const legacy = readIds(hidden);
58
+ return legacy.length === 0 ? {} : { [LEGACY_FLAT_PROVIDER]: new Set(legacy) };
59
+ }
60
+ if (!isRecord(hidden))
61
+ return {};
62
+ const result = {};
63
+ for (const [provider, ids] of Object.entries(hidden)) {
64
+ const set = new Set(readIds(ids));
65
+ if (set.size > 0)
66
+ result[provider] = set;
67
+ }
68
+ return result;
69
+ }
70
+ /** Read the user-added ids, per provider. */
71
+ function readExtra(document) {
72
+ if (!isRecord(document.extra))
73
+ return {};
74
+ const result = {};
75
+ for (const [provider, ids] of Object.entries(document.extra)) {
76
+ const list = readIds(ids);
77
+ if (list.length > 0)
78
+ result[provider] = list;
79
+ }
80
+ return result;
81
+ }
82
+ /** Coerce the document into a per-provider order map. */
83
+ function readOrder(document) {
84
+ const order = document.order;
85
+ if (!isRecord(order))
86
+ return {};
87
+ const result = {};
88
+ for (const [provider, ids] of Object.entries(order)) {
89
+ const list = readIds(ids);
90
+ if (list.length > 0)
91
+ result[provider] = list;
92
+ }
93
+ return result;
94
+ }
95
+ /**
96
+ * Owns the model preference document.
97
+ *
98
+ * Reads are cached; every write updates the cache and the file together, so the
99
+ * picker's next catalog query sees the change without a restart.
100
+ */
101
+ export class ModelPreferences {
102
+ file;
103
+ hidden = {};
104
+ order = {};
105
+ auto = { order: [], off: [] };
106
+ extra = {};
107
+ loaded = false;
108
+ /** The in-flight first read, so concurrent callers share one. */
109
+ loading;
110
+ constructor(file) {
111
+ this.file = file;
112
+ }
113
+ /**
114
+ * Load the document once; `reload` forces a re-read.
115
+ *
116
+ * Callers read several sections at once (`hidden`, `order`, `extra`), so the load
117
+ * is single-flight: marking the object loaded before the file had been read let a
118
+ * concurrent first call return an empty snapshot, which silently dropped the
119
+ * user's stored choices for whichever read lost the race.
120
+ */
121
+ async ensureLoaded(reload = false) {
122
+ if (reload) {
123
+ this.loaded = false;
124
+ this.loading = undefined;
125
+ }
126
+ if (this.loaded)
127
+ return;
128
+ this.loading ??= this.read().finally(() => { this.loading = undefined; });
129
+ await this.loading;
130
+ }
131
+ /** Read the file into memory and record that the state is loaded. */
132
+ async read() {
133
+ let raw;
134
+ try {
135
+ raw = await readFile(this.file, 'utf8');
136
+ }
137
+ catch {
138
+ this.hidden = {};
139
+ this.order = {};
140
+ this.auto = { order: [], off: [] };
141
+ this.extra = {};
142
+ this.loaded = true;
143
+ return;
144
+ }
145
+ try {
146
+ const parsed = JSON.parse(raw);
147
+ const document = (isRecord(parsed) ? parsed : {});
148
+ this.hidden = readHidden(document);
149
+ this.order = readOrder(document);
150
+ this.auto = readAuto(document);
151
+ this.extra = readExtra(document);
152
+ }
153
+ catch {
154
+ this.hidden = {};
155
+ this.order = {};
156
+ this.auto = { order: [], off: [] };
157
+ this.extra = {};
158
+ }
159
+ this.loaded = true;
160
+ }
161
+ /** Re-read the file, discarding in-memory state. */
162
+ async reload() {
163
+ await this.ensureLoaded(true);
164
+ }
165
+ /** The ids one provider's picker must not offer. */
166
+ async hiddenIds(provider) {
167
+ await this.ensureLoaded();
168
+ return new Set(this.hidden[provider] ?? []);
169
+ }
170
+ /** The user's model rank for one provider, most preferred first. */
171
+ async orderOf(provider) {
172
+ await this.ensureLoaded();
173
+ return [...this.order[provider] ?? []];
174
+ }
175
+ /** The ids the user added by hand for one provider. */
176
+ async extraIds(provider) {
177
+ await this.ensureLoaded();
178
+ return [...this.extra[provider] ?? []];
179
+ }
180
+ /**
181
+ * Add one model id by hand.
182
+ *
183
+ * The point of the button is a model the catalog does not know yet, so nothing
184
+ * here validates the id against an endpoint — only that it is a plausible id.
185
+ *
186
+ * @param provider - provider route key.
187
+ * @param id - the model id the endpoint accepts.
188
+ * @returns whether the stored list actually changed.
189
+ */
190
+ async addExtra(provider, id) {
191
+ await this.ensureLoaded();
192
+ const current = this.extra[provider] ?? [];
193
+ if (current.includes(id))
194
+ return false;
195
+ this.extra = { ...this.extra, [provider]: [...current, id] };
196
+ await this.persist();
197
+ return true;
198
+ }
199
+ /**
200
+ * Forget one hand-added id.
201
+ * @param provider - provider route key.
202
+ * @param id - the model id to drop.
203
+ * @returns whether the stored list actually changed.
204
+ */
205
+ async removeExtra(provider, id) {
206
+ await this.ensureLoaded();
207
+ const current = this.extra[provider] ?? [];
208
+ if (!current.includes(id))
209
+ return false;
210
+ const next = current.filter((entry) => entry !== id);
211
+ const copy = { ...this.extra };
212
+ if (next.length === 0)
213
+ delete copy[provider];
214
+ else
215
+ copy[provider] = next;
216
+ this.extra = copy;
217
+ await this.persist();
218
+ return true;
219
+ }
220
+ /** The `auto` provider's arrangement. */
221
+ async autoPreferences() {
222
+ await this.ensureLoaded();
223
+ return { order: [...this.auto.order], off: [...this.auto.off] };
224
+ }
225
+ /**
226
+ * Replace the `auto` provider's order.
227
+ * @param keys - `provider/model` keys, most preferred first.
228
+ * @returns whether the stored order actually changed.
229
+ */
230
+ async setAutoOrder(keys) {
231
+ await this.ensureLoaded();
232
+ const next = readIds([...keys]);
233
+ const current = this.auto.order;
234
+ if (current.length === next.length && current.every((key, index) => key === next[index]))
235
+ return false;
236
+ this.auto = { ...this.auto, order: next };
237
+ await this.persist();
238
+ return true;
239
+ }
240
+ /**
241
+ * Record whether `auto` may use one model.
242
+ * @param key - `provider/model` key.
243
+ * @param enabled - whether `auto` may fall back to it.
244
+ * @returns whether the stored state actually changed.
245
+ */
246
+ async setAutoEnabled(key, enabled) {
247
+ await this.ensureLoaded();
248
+ const current = new Set(this.auto.off);
249
+ const shouldOff = !enabled;
250
+ if (current.has(key) === shouldOff)
251
+ return false;
252
+ if (shouldOff)
253
+ current.add(key);
254
+ else
255
+ current.delete(key);
256
+ this.auto = { ...this.auto, off: [...current] };
257
+ await this.persist();
258
+ return true;
259
+ }
260
+ /**
261
+ * Record one model's visibility.
262
+ * @param provider - provider route key.
263
+ * @param id - model id as the catalog reports it.
264
+ * @param visible - whether the picker should offer it.
265
+ * @returns whether the stored state actually changed.
266
+ */
267
+ async setVisible(provider, id, visible) {
268
+ await this.ensureLoaded();
269
+ const current = this.hidden[provider] ?? new Set();
270
+ const shouldHide = !visible;
271
+ if (current.has(id) === shouldHide)
272
+ return false;
273
+ if (shouldHide)
274
+ current.add(id);
275
+ else
276
+ current.delete(id);
277
+ if (current.size === 0)
278
+ delete this.hidden[provider];
279
+ else
280
+ this.hidden[provider] = current;
281
+ await this.persist();
282
+ return true;
283
+ }
284
+ /**
285
+ * Replace one provider's model rank.
286
+ * @param provider - provider route key.
287
+ * @param ids - the complete rank, most preferred first.
288
+ * @returns whether the stored order actually changed.
289
+ */
290
+ async setOrder(provider, ids) {
291
+ await this.ensureLoaded();
292
+ const next = readIds([...ids]);
293
+ const current = this.order[provider] ?? [];
294
+ if (current.length === next.length && current.every((id, index) => id === next[index]))
295
+ return false;
296
+ if (next.length === 0)
297
+ delete this.order[provider];
298
+ else
299
+ this.order[provider] = next;
300
+ await this.persist();
301
+ return true;
302
+ }
303
+ /** Write the document atomically with owner-only permissions. */
304
+ async persist() {
305
+ await mkdir(path.dirname(this.file), { recursive: true });
306
+ const hidden = {};
307
+ for (const [provider, ids] of Object.entries(this.hidden)) {
308
+ if (ids.size > 0)
309
+ hidden[provider] = [...ids].sort();
310
+ }
311
+ const order = {};
312
+ for (const [provider, ids] of Object.entries(this.order)) {
313
+ if (ids.length > 0)
314
+ order[provider] = [...ids];
315
+ }
316
+ const document = {
317
+ hidden,
318
+ order,
319
+ auto: { order: [...this.auto.order], off: [...this.auto.off] },
320
+ extra: Object.fromEntries(Object.entries(this.extra).filter(([, ids]) => ids.length > 0)),
321
+ };
322
+ const temporary = `${this.file}.${process.pid}.tmp`;
323
+ await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
324
+ try {
325
+ await rename(temporary, this.file);
326
+ await chmod(this.file, 0o600);
327
+ }
328
+ catch (error) {
329
+ await rm(temporary, { force: true });
330
+ throw error;
331
+ }
332
+ }
333
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Tool-schema preparation for Antigravity's v1internal bridge.
3
+ *
4
+ * Two worlds meet here. Gemini-backed models take a JSON-Schema-ish
5
+ * `parametersJsonSchema`, but only after local `$ref`s are resolved and
6
+ * `$defs` are gone. Claude- and GPT-OSS-backed models take a much smaller
7
+ * `parameters` dialect and reject the rest, so their schema is reduced to
8
+ * `type`/`description`/`properties`/`required`/`items`/`enum` with every
9
+ * alternative folded into one shape that is never narrower than the original.
10
+ *
11
+ * @module dsh-plugin-subscription-oauth/translate/antigravity-schema
12
+ */
13
+ /** A JSON-schema-shaped object. */
14
+ type Schema = Record<string, unknown>;
15
+ /**
16
+ * Detach and normalize one tool's root object without changing the registry schema.
17
+ *
18
+ * @param parameters - the tool's declared argument schema.
19
+ * @param legacy - whether the target model takes the reduced `parameters` dialect.
20
+ * @returns the schema to send.
21
+ * @throws {LlmError} for an unsupported reference or a non-object root.
22
+ */
23
+ export declare function antigravityToolParameters(parameters: Record<string, unknown>, legacy: boolean): Schema;
24
+ export {};
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Tool-schema preparation for Antigravity's v1internal bridge.
3
+ *
4
+ * Two worlds meet here. Gemini-backed models take a JSON-Schema-ish
5
+ * `parametersJsonSchema`, but only after local `$ref`s are resolved and
6
+ * `$defs` are gone. Claude- and GPT-OSS-backed models take a much smaller
7
+ * `parameters` dialect and reject the rest, so their schema is reduced to
8
+ * `type`/`description`/`properties`/`required`/`items`/`enum` with every
9
+ * alternative folded into one shape that is never narrower than the original.
10
+ *
11
+ * @module dsh-plugin-subscription-oauth/translate/antigravity-schema
12
+ */
13
+ import { LlmError } from '@deepseek-ai/dsh-llm';
14
+ /** Keywords the Claude/GPT-OSS dialect keeps. */
15
+ const CUSTOM_KEYS = new Set(['type', 'description', 'properties', 'required', 'items', 'enum']);
16
+ /** True for a plain object usable as a schema node. */
17
+ function object(value) {
18
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
19
+ }
20
+ /** Resolve local references before definitions are removed; reject cycles and remote refs. */
21
+ function resolve(value, root, refs = new Set()) {
22
+ if (Array.isArray(value))
23
+ return value.map((item) => resolve(item, root, refs));
24
+ if (!object(value))
25
+ return value;
26
+ if (typeof value.$ref === 'string') {
27
+ const ref = value.$ref;
28
+ if (!ref.startsWith('#/') || refs.has(ref)) {
29
+ throw new LlmError(`Antigravity tool schema has an unsupported reference: ${ref}`, 'INVALID_REQUEST');
30
+ }
31
+ let target = root;
32
+ for (const segment of ref.slice(2).split('/')) {
33
+ const key = segment.replaceAll('~1', '/').replaceAll('~0', '~');
34
+ target = object(target) && Object.hasOwn(target, key) ? target[key] : undefined;
35
+ }
36
+ if (!object(target))
37
+ throw new LlmError(`Antigravity tool schema reference is missing: ${ref}`, 'INVALID_REQUEST');
38
+ const { $ref: _ref, ...siblings } = value;
39
+ return resolve({ ...target, ...siblings }, root, new Set([...refs, ref]));
40
+ }
41
+ return Object.fromEntries(Object.entries(value)
42
+ .filter(([key]) => !['$schema', '$id', '$defs', 'definitions'].includes(key))
43
+ .map(([key, item]) => [key, key === 'properties' && object(item)
44
+ ? Object.fromEntries(Object.entries(item).map(([name, property]) => [name, resolve(property, root, refs)]))
45
+ : resolve(item, root, refs)]));
46
+ }
47
+ /** Numeric types widen to `number`: every integer is also a number. */
48
+ const NUMERIC = new Set(['integer', 'number']);
49
+ /** The strings one alternative is limited to (`enum` or `const`). */
50
+ function stringChoices(schema) {
51
+ if (typeof schema.const === 'string')
52
+ return [schema.const];
53
+ return Array.isArray(schema.enum) && schema.enum.every((entry) => typeof entry === 'string')
54
+ ? schema.enum
55
+ : undefined;
56
+ }
57
+ /**
58
+ * Fold schema alternatives into one shape this bridge can express, never
59
+ * narrower than any alternative: picking one branch would make the tool
60
+ * uncallable with the others' values, which is worse than a looser contract the
61
+ * tool still validates. Objects keep the union of their properties and only the
62
+ * fields every alternative requires; scalars of one type keep that type (and a
63
+ * string choice union); anything heterogeneous is unconstrained.
64
+ */
65
+ function widen(variants) {
66
+ const members = variants.filter(object);
67
+ if (members.length === 0 || members.length !== variants.length)
68
+ return {};
69
+ const types = new Set(members.map((member) => member.type));
70
+ const type = types.size === 1
71
+ ? [...types][0]
72
+ : [...types].every((entry) => typeof entry === 'string' && NUMERIC.has(entry)) ? 'number' : undefined;
73
+ if (typeof type !== 'string')
74
+ return {};
75
+ if (type === 'object') {
76
+ const properties = {};
77
+ for (const member of members) {
78
+ if (!object(member.properties))
79
+ continue;
80
+ for (const [name, property] of Object.entries(member.properties)) {
81
+ properties[name] = !Object.hasOwn(properties, name) || JSON.stringify(properties[name]) === JSON.stringify(property)
82
+ ? property
83
+ : widen([properties[name], property]);
84
+ }
85
+ }
86
+ const required = members
87
+ .map((member) => Array.isArray(member.required)
88
+ ? member.required.filter((entry) => typeof entry === 'string')
89
+ : [])
90
+ .reduce((shared, next) => shared.filter((name) => next.includes(name)));
91
+ return {
92
+ type,
93
+ ...Object.keys(properties).length === 0 ? {} : { properties },
94
+ ...required.length === 0 ? {} : { required },
95
+ };
96
+ }
97
+ if (type === 'array') {
98
+ return { type, items: widen(members.map((member) => member.items ?? {})) };
99
+ }
100
+ const choices = members.map(stringChoices);
101
+ return choices.every((entry) => entry !== undefined)
102
+ ? { type, enum: [...new Set(choices.flat())] }
103
+ : { type };
104
+ }
105
+ /** Merge `extra` into `schema`: properties and required accumulate, other keywords override. */
106
+ function absorb(schema, extra) {
107
+ const { properties, required, ...rest } = extra;
108
+ Object.assign(schema, rest);
109
+ if (object(properties)) {
110
+ schema.properties = { ...object(schema.properties) ? schema.properties : {}, ...properties };
111
+ }
112
+ if (Array.isArray(required)) {
113
+ schema.required = [...new Set([...(Array.isArray(schema.required) ? schema.required : []), ...required])];
114
+ }
115
+ }
116
+ /** Keep property names intact while reducing schema keywords for Claude/GPT-OSS. */
117
+ function custom(value) {
118
+ if (!object(value))
119
+ return value;
120
+ const schema = { ...value };
121
+ // An intersection holds every member's constraints at once.
122
+ if (Array.isArray(schema.allOf)) {
123
+ const members = schema.allOf.filter(object);
124
+ delete schema.allOf;
125
+ for (const member of members)
126
+ absorb(schema, member);
127
+ }
128
+ for (const keyword of ['anyOf', 'oneOf']) {
129
+ if (!Array.isArray(schema[keyword]))
130
+ continue;
131
+ const variants = schema[keyword].filter((item) => !object(item) || item.type !== 'null');
132
+ delete schema[keyword];
133
+ absorb(schema, variants.length === 1 && object(variants[0]) ? variants[0] : widen(variants));
134
+ }
135
+ const out = {};
136
+ for (const [key, value] of Object.entries(schema)) {
137
+ if (!CUSTOM_KEYS.has(key))
138
+ continue;
139
+ if (key === 'properties' && object(value)) {
140
+ out[key] = Object.fromEntries(Object.entries(value).map(([name, property]) => [name, custom(property)]));
141
+ }
142
+ else if (key === 'items') {
143
+ out[key] = custom(value);
144
+ }
145
+ else if (key === 'type' && Array.isArray(value)) {
146
+ const types = value.filter((entry) => entry !== 'null');
147
+ const type = types.length === 1
148
+ ? types[0]
149
+ : types.length > 0 && types.every((entry) => NUMERIC.has(entry)) ? 'number' : undefined;
150
+ if (type !== undefined)
151
+ out[key] = type;
152
+ }
153
+ else if (key !== 'enum' || (Array.isArray(value) && value.every((entry) => typeof entry === 'string'))) {
154
+ out[key] = value;
155
+ }
156
+ }
157
+ return out;
158
+ }
159
+ /**
160
+ * Detach and normalize one tool's root object without changing the registry schema.
161
+ *
162
+ * @param parameters - the tool's declared argument schema.
163
+ * @param legacy - whether the target model takes the reduced `parameters` dialect.
164
+ * @returns the schema to send.
165
+ * @throws {LlmError} for an unsupported reference or a non-object root.
166
+ */
167
+ export function antigravityToolParameters(parameters, legacy) {
168
+ const resolved = resolve(parameters, parameters);
169
+ const root = { ...resolved, type: resolved.type ?? 'object' };
170
+ if (root.type !== 'object') {
171
+ throw new LlmError('Antigravity tool parameters must be an object schema', 'INVALID_REQUEST');
172
+ }
173
+ return legacy ? custom(root) : root;
174
+ }