@vellumai/assistant 0.12.4-staging.2 → 0.12.4-staging.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.12.4-staging.2",
3
+ "version": "0.12.4-staging.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -446,7 +446,7 @@ describe("OpenAIResponsesProvider explicit prompt caching (GPT-5.6+)", () => {
446
446
  expect(JSON.stringify(messages)).not.toContain("prompt_cache_breakpoint");
447
447
  });
448
448
 
449
- test("catalog flags GPT-5.6 and GPT-6 Astra direct-openai rows", () => {
449
+ test("catalog flags GPT-5.6 and GPT-6 direct-openai rows", () => {
450
450
  const openai = PROVIDER_CATALOG.find((p) => p.id === "openai");
451
451
  const flagged = (openai?.models ?? [])
452
452
  .filter((m) => m.supportsPromptCacheBreakpoints)
@@ -457,6 +457,8 @@ describe("OpenAIResponsesProvider explicit prompt caching (GPT-5.6+)", () => {
457
457
  "gpt-5.6-sol",
458
458
  "gpt-5.6-terra",
459
459
  "gpt-6-astra",
460
+ "gpt-6-luna",
461
+ "gpt-6-sol",
460
462
  ]);
461
463
  });
462
464
  });
@@ -135,6 +135,39 @@ describe("resolvePricing", () => {
135
135
  expect(result.estimatedCostUsd).toBeCloseTo(0.5 + 0.625 + 0.05, 10);
136
136
  });
137
137
 
138
+ test("bills GPT-6 Sol at published short-context rates", () => {
139
+ const result = resolvePricingForUsage("openai", "gpt-6-sol", {
140
+ directInputTokens: 50_000,
141
+ outputTokens: 50_000,
142
+ cacheCreationInputTokens: 50_000,
143
+ cacheReadInputTokens: 50_000,
144
+ anthropicCacheCreation: null,
145
+ });
146
+
147
+ expect(result.pricingStatus).toBe("priced");
148
+ // 0.05M x $2 direct + 0.05M x $2.5 write + 0.05M x $0.2 read
149
+ // + 0.05M x $10 output.
150
+ expect(result.estimatedCostUsd).toBeCloseTo(0.1 + 0.125 + 0.01 + 0.5, 10);
151
+ });
152
+
153
+ test("bills GPT-6 Luna at the long-context tier above 272k", () => {
154
+ const result = resolvePricingForUsage("openai", "gpt-6-luna", {
155
+ directInputTokens: 500_000,
156
+ outputTokens: 1_000_000,
157
+ cacheCreationInputTokens: 500_000,
158
+ cacheReadInputTokens: 1_000_000,
159
+ anthropicCacheCreation: null,
160
+ });
161
+
162
+ expect(result.pricingStatus).toBe("priced");
163
+ // Tier rates: 0.5M x $0.2 direct + 0.5M x $0.25 write + 1M x $0.02 read
164
+ // + 1M x $0.75 output.
165
+ expect(result.estimatedCostUsd).toBeCloseTo(
166
+ 0.1 + 0.125 + 0.02 + 0.75,
167
+ 10,
168
+ );
169
+ });
170
+
138
171
  test("bills GPT-6 Astra at the long-context tier above 272k", () => {
139
172
  const result = resolvePricingForUsage("openai", "gpt-6-astra", {
140
173
  directInputTokens: 500_000,
@@ -1,4 +1,4 @@
1
- import { mkdirSync, symlinkSync, writeFileSync } from "node:fs";
1
+ import { mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { beforeEach, describe, expect, mock, test } from "bun:test";
4
4
 
@@ -781,16 +781,48 @@ describe("skill_load tool", () => {
781
781
  expect(result.content).toContain("Deploy the application to production");
782
782
  expect(result.content).toContain("Rollback to previous version");
783
783
 
784
- // Should list parameters with types and required/optional markers
785
- expect(result.content).toContain(
786
- "- environment (string, required): Target environment",
784
+ const schemas = [
785
+ ...result.content.matchAll(/```json\n([\s\S]*?)\n```/g),
786
+ ].map((match) => JSON.parse(match[1]));
787
+ expect(schemas[0].required).toEqual(["environment"]);
788
+ expect(schemas[0].properties.environment).toEqual({
789
+ type: "string",
790
+ description: "Target environment",
791
+ });
792
+ expect(schemas[0].properties.force.type).toBe("boolean");
793
+ expect(schemas[1].required).toEqual(["version"]);
794
+ });
795
+
796
+ test("discloses the complete annotation mark schema before execution", async () => {
797
+ const manifest = JSON.parse(
798
+ readFileSync(
799
+ join(
800
+ import.meta.dir,
801
+ "../config/bundled-skills/screen-annotation/TOOLS.json",
802
+ ),
803
+ "utf-8",
804
+ ),
787
805
  );
788
- expect(result.content).toContain(
789
- "- force (boolean, optional): Force deploy even if checks fail",
806
+ writeSkill(
807
+ "annotation-schema",
808
+ "Annotation Schema",
809
+ "Point at a control",
810
+ "Name the control.",
790
811
  );
791
- expect(result.content).toContain(
792
- "- version (string, required): Version to rollback to",
812
+ writeToolsJson("annotation-schema", manifest.tools);
813
+
814
+ const result = await executeSkillLoad(
815
+ { skill: "annotation-schema" },
816
+ "macos",
793
817
  );
818
+ expect(result.isError).toBe(false);
819
+ const schemaMatch = result.content.match(/```json\n([\s\S]*?)\n```/);
820
+ expect(schemaMatch).not.toBeNull();
821
+ const schema = JSON.parse(schemaMatch![1]);
822
+ expect(schema).toEqual(manifest.tools[0].input_schema);
823
+ expect(schema.properties.marks.items.oneOf).toHaveLength(2);
824
+ expect(schema.properties.marks.items.properties.target.minLength).toBe(1);
825
+ expect(schema.properties.marks.items.properties.caption.maxLength).toBe(80);
794
826
  });
795
827
 
796
828
  test("skill without TOOLS.json does not include tool schemas section", async () => {
@@ -862,9 +894,8 @@ describe("skill_load tool", () => {
862
894
  // The child skill's tool schemas should appear (#### level under ### Tools from …)
863
895
  expect(result.content).toContain("#### child_action");
864
896
  expect(result.content).toContain("A child tool action");
865
- expect(result.content).toContain(
866
- "- target (string, required): Action target",
867
- );
897
+ const schemaMatch = result.content.match(/```json\n([\s\S]*?)\n```/);
898
+ expect(JSON.parse(schemaMatch![1]).required).toEqual(["target"]);
868
899
  expect(result.content).toContain(
869
900
  "Use `skill_execute` to call these tools.",
870
901
  );
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
6
6
 
7
- import type { SkillToolEntry } from "../config/skills.js";
7
+ import { getBundledSkillsDir, type SkillToolEntry } from "../config/skills.js";
8
8
  import { RiskLevel } from "../permissions/types.js";
9
9
  import { computeSkillVersionHash } from "../skills/version-hash.js";
10
10
  import { resolveSubagentRole } from "../subagent/role-resolution.js";
@@ -261,6 +261,54 @@ describe("createSkillTool — unknown parameter validation", () => {
261
261
  expect(result.content).toContain('Unknown parameter "bar"');
262
262
  });
263
263
 
264
+ test.each([
265
+ { description: "Filters" },
266
+ { marks: [{ description: "Filters" }] },
267
+ { marks: [{}] },
268
+ { marks: [{ target: 42 }] },
269
+ { marks: [{ x: 0.1, y: 0.2 }] },
270
+ { marks: [{ target: "Filters", x: 0.1, y: 0.2, width: 0.3, height: 0.4 }] },
271
+ ])(
272
+ "an annotation validation error provides enough schema for one corrected retry: %j",
273
+ async (invalid) => {
274
+ const skillDir = join(getBundledSkillsDir(), "screen-annotation");
275
+ const manifest = JSON.parse(
276
+ readFileSync(join(skillDir, "TOOLS.json"), "utf-8"),
277
+ );
278
+ const entry = manifest.tools.find(
279
+ (tool: SkillToolEntry) => tool.name === "screen_point_at",
280
+ );
281
+ const tool = createSkillTool(
282
+ entry,
283
+ skillDir,
284
+ computeSkillVersionHash(skillDir),
285
+ BUNDLED,
286
+ );
287
+ const calls: unknown[] = [];
288
+ const context = makeContext({
289
+ proxyToolResolver: async (name, input) => {
290
+ calls.push({ name, input });
291
+ return { content: "Drew 1 mark", isError: false };
292
+ },
293
+ });
294
+
295
+ const rejected = await tool.execute(invalid, context);
296
+ expect(rejected.isError).toBe(true);
297
+ expect(calls).toHaveLength(0);
298
+ expect(rejected.content).toContain("skill_load");
299
+ const schemaMatch = rejected.content.match(/```json\n([\s\S]*?)\n```/);
300
+ expect(schemaMatch).not.toBeNull();
301
+ expect(JSON.parse(schemaMatch![1])).toEqual(entry.input_schema);
302
+
303
+ const input = {
304
+ marks: [{ target: "Filters", caption: "Click Filters" }],
305
+ };
306
+ const corrected = await tool.execute(input, context);
307
+ expect(corrected.isError).toBe(false);
308
+ expect(calls).toEqual([{ name: "computer_use_point_at", input }]);
309
+ },
310
+ );
311
+
264
312
  test("allows input with only known parameters", async () => {
265
313
  const hash = computeSkillVersionHash(tempDir);
266
314
  const tool = createSkillTool(
@@ -471,7 +519,7 @@ describe("createSkillTool — required/type/enum validation", () => {
471
519
 
472
520
  expect(result.isError).toBe(true);
473
521
  expect(result.content).toContain(
474
- "auto_open must be a boolean — pass true or false as a JSON boolean, not a string",
522
+ "auto_open must be a boolean: pass true or false as a JSON boolean, not a string",
475
523
  );
476
524
  });
477
525
 
@@ -6,6 +6,79 @@ import {
6
6
  validateInputAgainstSchema,
7
7
  } from "../skills/validate-input.js";
8
8
 
9
+ describe("nested input validation", () => {
10
+ const schema = {
11
+ properties: {
12
+ options: {
13
+ type: "object",
14
+ properties: { label: { type: "string" } },
15
+ required: ["label"],
16
+ },
17
+ },
18
+ };
19
+
20
+ test("reports nested required fields and types with their path", () => {
21
+ expect(
22
+ validateInputAgainstSchema("example", { options: {} }, schema),
23
+ ).toEqual({
24
+ ok: false,
25
+ errors: ["options.label is required"],
26
+ });
27
+ expect(
28
+ validateInputAgainstSchema("example", { options: { label: 42 } }, schema),
29
+ ).toEqual({
30
+ ok: false,
31
+ errors: ["options.label must be a string"],
32
+ });
33
+ });
34
+
35
+ test("preserves open nested objects and only rejects explicitly closed extra fields", () => {
36
+ const input = { options: { label: "Example", extra: true } };
37
+ expect(validateInputAgainstSchema("example", input, schema)).toEqual({
38
+ ok: true,
39
+ });
40
+ expect(
41
+ validateInputAgainstSchema("example", input, {
42
+ properties: {
43
+ options: {
44
+ ...schema.properties.options,
45
+ additionalProperties: false,
46
+ },
47
+ },
48
+ }),
49
+ ).toEqual({
50
+ ok: false,
51
+ errors: ['Unknown parameter "options.extra". Supported: "label"'],
52
+ });
53
+ });
54
+
55
+ test("does not treat a richer oneOf as a presence-only union", () => {
56
+ expect(
57
+ validateInputAgainstSchema(
58
+ "example",
59
+ { options: { label: "Example" } },
60
+ {
61
+ properties: {
62
+ options: {
63
+ ...schema.properties.options,
64
+ oneOf: [
65
+ {
66
+ required: ["label"],
67
+ properties: { label: { const: "Example" } },
68
+ },
69
+ {
70
+ required: ["label"],
71
+ properties: { label: { const: "Other" } },
72
+ },
73
+ ],
74
+ },
75
+ },
76
+ },
77
+ ),
78
+ ).toEqual({ ok: true });
79
+ });
80
+ });
81
+
9
82
  // ---------------------------------------------------------------------------
10
83
  // required
11
84
  // ---------------------------------------------------------------------------
@@ -34,7 +34,7 @@ export const DEFAULT_CARD_CHARS = 500;
34
34
  * only, so a missing product is not evidence it is unsupported.
35
35
  */
36
36
  export const SKILLS_INJECTION_CATALOG_HINT =
37
- "Injected skills are only ones currently in the workspace. Run `assistant plugins search <name>` and `assistant skills search <name>` before concluding a given integration or skill is unsupported";
37
+ "These skill cards describe availability, not tool instructions. When skill_load is available, use it to load a skill's current instructions and input schemas before calling its tools; follow those schemas over remembered arguments. Injected skills are only ones currently in the workspace. Run `assistant plugins search <name>` and `assistant skills search <name>` before concluding a given integration or skill is unsupported";
38
38
 
39
39
  /**
40
40
  * Render the prose-style capability statement embedded into the unified
@@ -31,6 +31,9 @@ describe("renderInjectionBlockInner", () => {
31
31
  expect(inner).toContain("assistant plugins search <name>");
32
32
  expect(inner).toContain("assistant skills search <name>");
33
33
  expect(inner).toContain("currently in the workspace");
34
+ expect(inner).toContain("When skill_load is available, use it");
35
+ expect(inner).toContain("before calling its tools");
36
+ expect(inner).toContain("over remembered arguments");
34
37
  expect(inner.indexOf("# Skills")).toBeLessThan(inner.indexOf("# Skill:"));
35
38
  expect(inner.startsWith(`${V3_INJECTION_HEADER}\n\n# Skills\n`)).toBe(true);
36
39
  });
@@ -57,6 +57,8 @@ describe("isConnectionCompatibleWithModel", () => {
57
57
  test("oauth_subscription connection is compatible with a Codex model", () => {
58
58
  const conn = { auth: oauthAuth };
59
59
  expect(isConnectionCompatibleWithModel(conn, "gpt-6-astra")).toBe(true);
60
+ expect(isConnectionCompatibleWithModel(conn, "gpt-6-sol")).toBe(true);
61
+ expect(isConnectionCompatibleWithModel(conn, "gpt-6-luna")).toBe(true);
60
62
  expect(isConnectionCompatibleWithModel(conn, "gpt-5.6-sol")).toBe(true);
61
63
  expect(isConnectionCompatibleWithModel(conn, "gpt-5.6-terra")).toBe(true);
62
64
  expect(isConnectionCompatibleWithModel(conn, "gpt-5.6-luna")).toBe(true);
@@ -1004,7 +1004,7 @@ export class AnthropicProvider implements Provider {
1004
1004
  const isHaiku = effectiveModel.includes("haiku");
1005
1005
  // Effort support is per-model: Haiku and Sonnet 4.5 reject the param (see isEffortSupported).
1006
1006
  const supportsEffort = isEffortSupported(effectiveModel);
1007
- // opus-4-7 / opus-4-8 / opus-5 and sonnet-5 reject `temperature`,
1007
+ // opus-4-7 / opus-4-8 / opus-5 / opus-5-5 and sonnet-5 reject `temperature`,
1008
1008
  // `top_p`, and `top_k` with a 400 "`temperature`/`top_p` is deprecated
1009
1009
  // for this model" — model-wide, not effort-conditional (verified
1010
1010
  // 2026-06-23). opus-4-6 / sonnet-4-6 / haiku-4-5 still accept them.
@@ -1016,7 +1016,7 @@ export class AnthropicProvider implements Provider {
1016
1016
  // suffix is what matches.
1017
1017
  const deprecatesSamplingParams =
1018
1018
  /claude-opus-4-[78]\b/.test(effectiveModel) ||
1019
- /claude-opus-5\b/.test(effectiveModel) ||
1019
+ /claude-opus-5(-5)?\b/.test(effectiveModel) ||
1020
1020
  /claude-sonnet-5\b/.test(effectiveModel) ||
1021
1021
  /claude-fable-/.test(effectiveModel);
1022
1022
  const mergedOutputConfig = {
@@ -245,6 +245,24 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
245
245
  cacheReadPer1mTokens: 1,
246
246
  },
247
247
  },
248
+ {
249
+ id: "claude-opus-5-5",
250
+ displayName: "Claude Opus 5.5",
251
+ contextWindowTokens: 1000000,
252
+ maxOutputTokens: 128000,
253
+ longContextPricingThresholdTokens: 200000,
254
+ supportsThinking: true,
255
+ adaptiveThinkingOnly: true,
256
+ supportsCaching: true,
257
+ supportsVision: true,
258
+ supportsToolUse: true,
259
+ pricing: {
260
+ inputPer1mTokens: 4,
261
+ outputPer1mTokens: 20,
262
+ cacheWritePer1mTokens: 5,
263
+ cacheReadPer1mTokens: 0.2,
264
+ },
265
+ },
248
266
  {
249
267
  id: "claude-opus-5",
250
268
  displayName: "Claude Opus 5",
@@ -456,6 +474,66 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
456
474
  ],
457
475
  },
458
476
  },
477
+ {
478
+ id: "gpt-6-sol",
479
+ displayName: "GPT-6 Sol",
480
+ contextWindowTokens: 1050000,
481
+ maxOutputTokens: 128000,
482
+ longContextPricingThresholdTokens:
483
+ OPENAI_LONG_CONTEXT_PRICING_THRESHOLD_TOKENS,
484
+ supportsThinking: true,
485
+ supportsCaching: true,
486
+ supportsVision: true,
487
+ supportsToolUse: true,
488
+ supportsPromptCacheBreakpoints: true,
489
+ maxEffort: "max",
490
+ supportedEfforts: ["low", "medium", "high", "xhigh", "max"],
491
+ pricing: {
492
+ inputPer1mTokens: 2,
493
+ outputPer1mTokens: 10,
494
+ cacheWritePer1mTokens: 2.5,
495
+ cacheReadPer1mTokens: 0.2,
496
+ tiers: [
497
+ {
498
+ inputTokenThreshold: OPENAI_LONG_CONTEXT_PRICING_THRESHOLD_TOKENS,
499
+ inputPer1mTokens: 4,
500
+ outputPer1mTokens: 15,
501
+ cacheWritePer1mTokens: 5,
502
+ cacheReadPer1mTokens: 0.4,
503
+ },
504
+ ],
505
+ },
506
+ },
507
+ {
508
+ id: "gpt-6-luna",
509
+ displayName: "GPT-6 Luna",
510
+ contextWindowTokens: 1050000,
511
+ maxOutputTokens: 128000,
512
+ longContextPricingThresholdTokens:
513
+ OPENAI_LONG_CONTEXT_PRICING_THRESHOLD_TOKENS,
514
+ supportsThinking: true,
515
+ supportsCaching: true,
516
+ supportsVision: true,
517
+ supportsToolUse: true,
518
+ supportsPromptCacheBreakpoints: true,
519
+ maxEffort: "max",
520
+ supportedEfforts: ["low", "medium", "high", "xhigh", "max"],
521
+ pricing: {
522
+ inputPer1mTokens: 0.1,
523
+ outputPer1mTokens: 0.5,
524
+ cacheWritePer1mTokens: 0.125,
525
+ cacheReadPer1mTokens: 0.01,
526
+ tiers: [
527
+ {
528
+ inputTokenThreshold: OPENAI_LONG_CONTEXT_PRICING_THRESHOLD_TOKENS,
529
+ inputPer1mTokens: 0.2,
530
+ outputPer1mTokens: 0.75,
531
+ cacheWritePer1mTokens: 0.25,
532
+ cacheReadPer1mTokens: 0.02,
533
+ },
534
+ ],
535
+ },
536
+ },
459
537
  // GPT-5.6 family (Sol / Terra / Luna). cacheRead is the 90% cached-read
460
538
  // discount; cacheWrite is the 1.25x-input rate GPT-5.6+ bills for
461
539
  // prompt tokens written to the cache (reported as `cache_write_tokens`
@@ -1209,6 +1287,24 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
1209
1287
  cacheReadPer1mTokens: 1,
1210
1288
  },
1211
1289
  },
1290
+ {
1291
+ id: "anthropic/claude-opus-5.5",
1292
+ displayName: "Claude Opus 5.5",
1293
+ contextWindowTokens: 1000000,
1294
+ maxOutputTokens: 128000,
1295
+ longContextPricingThresholdTokens: 200000,
1296
+ supportsThinking: true,
1297
+ adaptiveThinkingOnly: true,
1298
+ supportsCaching: true,
1299
+ supportsVision: true,
1300
+ supportsToolUse: true,
1301
+ pricing: {
1302
+ inputPer1mTokens: 4,
1303
+ outputPer1mTokens: 20,
1304
+ cacheWritePer1mTokens: 5,
1305
+ cacheReadPer1mTokens: 0.2,
1306
+ },
1307
+ },
1212
1308
  {
1213
1309
  id: "anthropic/claude-opus-5",
1214
1310
  displayName: "Claude Opus 5",
@@ -1431,6 +1527,126 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
1431
1527
  ],
1432
1528
  },
1433
1529
  },
1530
+ {
1531
+ id: "openai/gpt-6-sol",
1532
+ displayName: "GPT-6 Sol",
1533
+ contextWindowTokens: 1050000,
1534
+ maxOutputTokens: 128000,
1535
+ longContextPricingThresholdTokens:
1536
+ OPENAI_LONG_CONTEXT_PRICING_THRESHOLD_TOKENS,
1537
+ supportsThinking: true,
1538
+ supportsCaching: true,
1539
+ supportsVision: true,
1540
+ supportsToolUse: true,
1541
+ supportsPromptCacheBreakpoints: true,
1542
+ maxEffort: "max",
1543
+ supportedEfforts: ["low", "medium", "high", "xhigh", "max"],
1544
+ pricing: {
1545
+ inputPer1mTokens: 2,
1546
+ outputPer1mTokens: 10,
1547
+ cacheWritePer1mTokens: 2.5,
1548
+ cacheReadPer1mTokens: 0.2,
1549
+ tiers: [
1550
+ {
1551
+ inputTokenThreshold: OPENAI_LONG_CONTEXT_PRICING_THRESHOLD_TOKENS,
1552
+ inputPer1mTokens: 4,
1553
+ outputPer1mTokens: 15,
1554
+ cacheWritePer1mTokens: 5,
1555
+ cacheReadPer1mTokens: 0.4,
1556
+ },
1557
+ ],
1558
+ },
1559
+ },
1560
+ {
1561
+ id: "openai/gpt-6-sol-pro",
1562
+ displayName: "GPT-6 Sol Pro",
1563
+ contextWindowTokens: 1050000,
1564
+ maxOutputTokens: 128000,
1565
+ longContextPricingThresholdTokens:
1566
+ OPENAI_LONG_CONTEXT_PRICING_THRESHOLD_TOKENS,
1567
+ supportsThinking: true,
1568
+ supportsCaching: true,
1569
+ supportsVision: true,
1570
+ supportsToolUse: true,
1571
+ supportsPromptCacheBreakpoints: true,
1572
+ maxEffort: "max",
1573
+ supportedEfforts: ["low", "medium", "high", "xhigh", "max"],
1574
+ pricing: {
1575
+ inputPer1mTokens: 2,
1576
+ outputPer1mTokens: 10,
1577
+ cacheWritePer1mTokens: 2.5,
1578
+ cacheReadPer1mTokens: 0.2,
1579
+ tiers: [
1580
+ {
1581
+ inputTokenThreshold: OPENAI_LONG_CONTEXT_PRICING_THRESHOLD_TOKENS,
1582
+ inputPer1mTokens: 4,
1583
+ outputPer1mTokens: 15,
1584
+ cacheWritePer1mTokens: 5,
1585
+ cacheReadPer1mTokens: 0.4,
1586
+ },
1587
+ ],
1588
+ },
1589
+ },
1590
+ {
1591
+ id: "openai/gpt-6-luna",
1592
+ displayName: "GPT-6 Luna",
1593
+ contextWindowTokens: 1050000,
1594
+ maxOutputTokens: 128000,
1595
+ longContextPricingThresholdTokens:
1596
+ OPENAI_LONG_CONTEXT_PRICING_THRESHOLD_TOKENS,
1597
+ supportsThinking: true,
1598
+ supportsCaching: true,
1599
+ supportsVision: true,
1600
+ supportsToolUse: true,
1601
+ supportsPromptCacheBreakpoints: true,
1602
+ maxEffort: "max",
1603
+ supportedEfforts: ["low", "medium", "high", "xhigh", "max"],
1604
+ pricing: {
1605
+ inputPer1mTokens: 0.1,
1606
+ outputPer1mTokens: 0.5,
1607
+ cacheWritePer1mTokens: 0.125,
1608
+ cacheReadPer1mTokens: 0.01,
1609
+ tiers: [
1610
+ {
1611
+ inputTokenThreshold: OPENAI_LONG_CONTEXT_PRICING_THRESHOLD_TOKENS,
1612
+ inputPer1mTokens: 0.2,
1613
+ outputPer1mTokens: 0.75,
1614
+ cacheWritePer1mTokens: 0.25,
1615
+ cacheReadPer1mTokens: 0.02,
1616
+ },
1617
+ ],
1618
+ },
1619
+ },
1620
+ {
1621
+ id: "openai/gpt-6-luna-pro",
1622
+ displayName: "GPT-6 Luna Pro",
1623
+ contextWindowTokens: 1050000,
1624
+ maxOutputTokens: 128000,
1625
+ longContextPricingThresholdTokens:
1626
+ OPENAI_LONG_CONTEXT_PRICING_THRESHOLD_TOKENS,
1627
+ supportsThinking: true,
1628
+ supportsCaching: true,
1629
+ supportsVision: true,
1630
+ supportsToolUse: true,
1631
+ supportsPromptCacheBreakpoints: true,
1632
+ maxEffort: "max",
1633
+ supportedEfforts: ["low", "medium", "high", "xhigh", "max"],
1634
+ pricing: {
1635
+ inputPer1mTokens: 0.1,
1636
+ outputPer1mTokens: 0.5,
1637
+ cacheWritePer1mTokens: 0.125,
1638
+ cacheReadPer1mTokens: 0.01,
1639
+ tiers: [
1640
+ {
1641
+ inputTokenThreshold: OPENAI_LONG_CONTEXT_PRICING_THRESHOLD_TOKENS,
1642
+ inputPer1mTokens: 0.2,
1643
+ outputPer1mTokens: 0.75,
1644
+ cacheWritePer1mTokens: 0.25,
1645
+ cacheReadPer1mTokens: 0.02,
1646
+ },
1647
+ ],
1648
+ },
1649
+ },
1434
1650
  // GPT-5.6 family (Sol / Terra / Luna). The `*-pro` slugs are the same
1435
1651
  // underlying models served with `reasoning.mode: pro` at identical
1436
1652
  // rates. cacheWrite is the 1.25x-input rate GPT-5.6+ bills for prompt
@@ -2142,6 +2358,24 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
2142
2358
  cacheReadPer1mTokens: 1,
2143
2359
  },
2144
2360
  },
2361
+ {
2362
+ id: "anthropic/claude-opus-5.5",
2363
+ displayName: "Claude Opus 5.5",
2364
+ contextWindowTokens: 1000000,
2365
+ maxOutputTokens: 128000,
2366
+ longContextPricingThresholdTokens: 200000,
2367
+ supportsThinking: true,
2368
+ adaptiveThinkingOnly: true,
2369
+ supportsCaching: true,
2370
+ supportsVision: true,
2371
+ supportsToolUse: true,
2372
+ pricing: {
2373
+ inputPer1mTokens: 4,
2374
+ outputPer1mTokens: 20,
2375
+ cacheWritePer1mTokens: 5,
2376
+ cacheReadPer1mTokens: 0.2,
2377
+ },
2378
+ },
2145
2379
  {
2146
2380
  id: "anthropic/claude-opus-5",
2147
2381
  displayName: "Claude Opus 5",
@@ -10,6 +10,8 @@
10
10
  */
11
11
  export const CODEX_SUBSCRIPTION_MODEL_IDS: ReadonlySet<string> = new Set([
12
12
  "gpt-6-astra",
13
+ "gpt-6-sol",
14
+ "gpt-6-luna",
13
15
  "gpt-5.6-sol",
14
16
  "gpt-5.6-terra",
15
17
  "gpt-5.6-luna",
@@ -3,10 +3,11 @@
3
3
  *
4
4
  * Deliberate strict subset of JSON Schema: it covers only the keywords
5
5
  * actually used by `assistant/src/config/bundled-skills/**\/TOOLS.json`
6
- * (required / type / enum / items.type) plus unknown-key detection. Anything
7
- * else (`$ref`, `oneOf`, `anyOf`, `allOf`, `format`, `pattern`, `minimum`,
8
- * `maximum`, object-shape `additionalProperties`, etc.) is silently skipped so
9
- * a richer schema can never cause us to reject a legitimate call.
6
+ * (required / type / enum / nested properties and items / required-only oneOf)
7
+ * plus unknown-key detection. Anything else (`$ref`, richer `oneOf`, `anyOf`,
8
+ * `allOf`, `format`, `pattern`, `minimum`, `maximum`, object-shape
9
+ * `additionalProperties`, etc.) is skipped. Nested objects allow extra keys
10
+ * unless their schema explicitly closes them.
10
11
  *
11
12
  * Each error message is written to be agent-readable and self-correcting
12
13
  * (e.g. `surface_id is required`, `mode must be one of "replace", "append"`).
@@ -313,6 +314,15 @@ export function validateInputAgainstSchema(
313
314
  _toolName: string,
314
315
  input: Record<string, unknown>,
315
316
  schema: Record<string, unknown> | undefined,
317
+ ): InputValidationResult {
318
+ return validateObjectInput(input, schema, "", true);
319
+ }
320
+
321
+ function validateObjectInput(
322
+ input: Record<string, unknown>,
323
+ schema: Record<string, unknown> | undefined,
324
+ path: string,
325
+ rejectUnknown: boolean,
316
326
  ): InputValidationResult {
317
327
  // Skip when there's no schema or no properties block — matches today's
318
328
  // lenient behaviour for tools that declare only `{ type: "object" }`.
@@ -338,17 +348,46 @@ export function validateInputAgainstSchema(
338
348
  continue;
339
349
  }
340
350
  if (!(key in input)) {
341
- errors.push(`${key} is required`);
351
+ errors.push(`${path}${key} is required`);
342
352
  }
343
353
  }
344
354
  }
345
355
 
356
+ // Only enforce alternatives whose complete contract is field presence.
357
+ // Richer unions stay permissive because unsupported keywords can decide
358
+ // which branch matches.
359
+ const alternatives = schema.oneOf;
360
+ if (
361
+ Array.isArray(alternatives) &&
362
+ alternatives.length > 0 &&
363
+ alternatives.every(
364
+ (branch) =>
365
+ isPlainObject(branch) &&
366
+ Object.keys(branch).every(
367
+ (key) => key === "required" || key === "description",
368
+ ) &&
369
+ Array.isArray(branch.required) &&
370
+ branch.required.every((key: unknown) => typeof key === "string"),
371
+ )
372
+ ) {
373
+ const choices = alternatives as { required: string[] }[];
374
+ if (
375
+ choices.filter((branch) => branch.required.every((key) => key in input))
376
+ .length !== 1
377
+ ) {
378
+ errors.push(
379
+ `${path ? path.slice(0, -1) : "input"} must match exactly one required field set: ${choices.map((branch) => quoteList(branch.required)).join(" or ")}`,
380
+ );
381
+ }
382
+ }
383
+
346
384
  // 2. Per-property checks: type, enum, items.type.
347
- for (const [key, rawSubSchema] of Object.entries(properties)) {
348
- if (!(key in input)) {
385
+ for (const [property, rawSubSchema] of Object.entries(properties)) {
386
+ if (!(property in input)) {
349
387
  continue;
350
388
  }
351
- const value = input[key];
389
+ const key = `${path}${property}`;
390
+ const value = input[property];
352
391
  // Skip type-checking for absent values; presence is enforced by the
353
392
  // `required` check above. Note: `null` IS a present value and is
354
393
  // type-checked below — only schemas that explicitly opt in to null via
@@ -381,13 +420,25 @@ export function validateInputAgainstSchema(
381
420
  if (!matchesType(value, type)) {
382
421
  errors.push(
383
422
  type === "boolean" && typeof value === "string"
384
- ? `${key} must be a boolean — pass true or false as a JSON boolean, not a string`
423
+ ? `${key} must be a boolean: pass true or false as a JSON boolean, not a string`
385
424
  : `${key} must be ${typeArticle(type)} ${type}`,
386
425
  );
387
426
  // No point checking enum/items if the base type is wrong.
388
427
  continue;
389
428
  }
390
429
 
430
+ if (type === "object" && isPlainObject(value)) {
431
+ const nested = validateObjectInput(
432
+ value,
433
+ rawSubSchema,
434
+ `${key}.`,
435
+ rawSubSchema.additionalProperties === false,
436
+ );
437
+ if (!nested.ok) {
438
+ errors.push(...nested.errors);
439
+ }
440
+ }
441
+
391
442
  // 2a. Enum (string enums are the only shape used today).
392
443
  const enumValues = rawSubSchema.enum;
393
444
  if (Array.isArray(enumValues) && enumValues.length > 0) {
@@ -412,6 +463,17 @@ export function validateInputAgainstSchema(
412
463
  errors.push(
413
464
  `${key}[${index}] must be ${typeArticle(itemTypeName)} ${itemTypeName}`,
414
465
  );
466
+ } else if (itemTypeName === "object" && isPlainObject(element)) {
467
+ const itemSchema = rawSubSchema.items as Record<string, unknown>;
468
+ const nested = validateObjectInput(
469
+ element,
470
+ itemSchema,
471
+ `${key}[${index}].`,
472
+ itemSchema.additionalProperties === false,
473
+ );
474
+ if (!nested.ok) {
475
+ errors.push(...nested.errors);
476
+ }
415
477
  }
416
478
  });
417
479
  }
@@ -420,10 +482,12 @@ export function validateInputAgainstSchema(
420
482
  }
421
483
 
422
484
  // 3. Unknown keys — parity with the previous `validateNoUnknownParams`.
423
- const unknownKeys = Object.keys(input).filter((k) => !knownKeySet.has(k));
485
+ const unknownKeys = rejectUnknown
486
+ ? Object.keys(input).filter((k) => !knownKeySet.has(k))
487
+ : [];
424
488
  for (const key of unknownKeys) {
425
489
  errors.push(
426
- `Unknown parameter "${key}". Supported: ${quoteList(knownKeys)}`,
490
+ `Unknown parameter "${path}${key}". Supported: ${quoteList(knownKeys)}`,
427
491
  );
428
492
  }
429
493
 
@@ -178,7 +178,7 @@ export function augmentSkillExecuteError(
178
178
  export const skillExecuteTool = {
179
179
  name: "skill_execute",
180
180
  description:
181
- "Execute a tool provided by a loaded skill. Use this instead of calling skill tools directly. The skill's instructions (from skill_load) describe available tools and their parameters. For browser automation, use the `assistant browser` CLI commands instead.",
181
+ "Execute a tool provided by a loaded skill. When skill_load is available, call it for the owning skill before first use, even if a capability card says it is available. Use the current instructions and input schema over remembered arguments. Use this instead of calling skill tools directly. For browser automation, use the `assistant browser` CLI commands instead.",
182
182
  category: "skills",
183
183
  executionTarget: "sandbox",
184
184
  defaultRiskLevel: RiskLevel.Low,
@@ -0,0 +1,15 @@
1
+ import { getWorkspaceDirDisplay } from "../../util/platform.js";
2
+
3
+ /** Preserve nested fields and alternatives when disclosing a skill's input. */
4
+ export function formatSkillInputSchema(schema: object): string {
5
+ const workspaceDir = getWorkspaceDirDisplay();
6
+ const json = JSON.stringify(
7
+ schema,
8
+ (_key, value: unknown) =>
9
+ typeof value === "string"
10
+ ? value.replaceAll("{workspaceDir}", workspaceDir)
11
+ : value,
12
+ 2,
13
+ );
14
+ return `Input schema for \`skill_execute.input\`:\n\`\`\`json\n${json}\n\`\`\``;
15
+ }
@@ -35,6 +35,7 @@ import type {
35
35
  ToolDefinition,
36
36
  ToolExecutionResult,
37
37
  } from "../types.js";
38
+ import { formatSkillInputSchema } from "./format-input-schema.js";
38
39
 
39
40
  /** Skill sources eligible for inline command expansion in v1. */
40
41
  const INLINE_COMMAND_ELIGIBLE_SOURCES = new Set([
@@ -123,32 +124,7 @@ function formatToolSchemas(
123
124
  tool.description.replaceAll("{workspaceDir}", getWorkspaceDirDisplay()),
124
125
  );
125
126
 
126
- const schema = tool.input_schema;
127
- const properties = schema.properties as
128
- | Record<string, Record<string, unknown>>
129
- | undefined;
130
- if (properties && Object.keys(properties).length > 0) {
131
- const requiredSet = new Set<string>(
132
- Array.isArray(schema.required) ? (schema.required as string[]) : [],
133
- );
134
-
135
- lines.push("Parameters:");
136
- for (const [paramName, paramDef] of Object.entries(properties)) {
137
- const paramType =
138
- typeof paramDef.type === "string" ? paramDef.type : "any";
139
- const requiredLabel = requiredSet.has(paramName)
140
- ? "required"
141
- : "optional";
142
- const descPart =
143
- typeof paramDef.description === "string"
144
- ? `: ${paramDef.description.replaceAll("{workspaceDir}", getWorkspaceDirDisplay())}`
145
- : "";
146
- lines.push(
147
- `- ${paramName} (${paramType}, ${requiredLabel})${descPart}`,
148
- );
149
- }
150
- }
151
-
127
+ lines.push(formatSkillInputSchema(tool.input_schema));
152
128
  lines.push("");
153
129
  }
154
130
 
@@ -15,6 +15,7 @@ import { bundledToolInputMisuseMessage } from "../shared/input-misuse.js";
15
15
  import { bundledToolInputRepairs } from "../shared/input-repairs.js";
16
16
  import type { ExecutionTarget } from "../tool-types.js";
17
17
  import type { Tool, ToolContext, ToolExecutionResult } from "../types.js";
18
+ import { formatSkillInputSchema } from "./format-input-schema.js";
18
19
  import { runSkillToolScript } from "./skill-script-runner.js";
19
20
 
20
21
  const riskMap: Record<SkillToolEntry["risk"], RiskLevel> = {
@@ -94,7 +95,7 @@ export function createSkillTool(
94
95
  return {
95
96
  content:
96
97
  misuse ??
97
- `Invalid input for tool "${entry.name}": ${validation.errors.join("; ")}. Fix the arguments and retry.`,
98
+ `Invalid input for tool "${entry.name}": ${validation.errors.join("; ")}. If skill_load is available, load the owning skill for its current instructions. Retry with arguments matching this schema.\n\n${formatSkillInputSchema(entry.input_schema)}`,
98
99
  isError: true,
99
100
  };
100
101
  }