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

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 (35) hide show
  1. package/package.json +1 -1
  2. package/src/__tests__/openai-responses-prompt-cache.test.ts +3 -1
  3. package/src/__tests__/pricing.test.ts +33 -0
  4. package/src/__tests__/skill-load-tool.test.ts +42 -11
  5. package/src/__tests__/skill-tool-factory.test.ts +50 -2
  6. package/src/__tests__/validate-input.test.ts +73 -0
  7. package/src/cli/commands/__tests__/plugins.test.ts +29 -1
  8. package/src/cli/commands/plugins.ts +16 -0
  9. package/src/cli/lib/__tests__/inspect-plugin.test.ts +29 -0
  10. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +32 -2
  11. package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +15 -0
  12. package/src/cli/lib/__tests__/plugin-catalog-resolve.test.ts +21 -0
  13. package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +65 -0
  14. package/src/cli/lib/__tests__/plugin-details.test.ts +43 -0
  15. package/src/cli/lib/__tests__/plugins-install-offline.test.ts +26 -0
  16. package/src/cli/lib/inspect-plugin.ts +5 -0
  17. package/src/cli/lib/plugin-catalog-cache.ts +24 -2
  18. package/src/cli/lib/plugin-catalog-local.ts +9 -0
  19. package/src/cli/lib/plugin-catalog-visibility.ts +41 -0
  20. package/src/cli/lib/plugin-details.ts +10 -7
  21. package/src/cli/lib/search-plugins.ts +2 -0
  22. package/src/config/feature-flag-registry.json +9 -1
  23. package/src/plugins/defaults/memory/substrate/skill-content.ts +1 -1
  24. package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +3 -0
  25. package/src/providers/__tests__/connection-model-compat.test.ts +2 -0
  26. package/src/providers/anthropic/client.ts +2 -2
  27. package/src/providers/model-catalog.ts +234 -0
  28. package/src/providers/openai/codex-models.ts +2 -0
  29. package/src/runtime/routes/__tests__/plugins-routes.test.ts +8 -0
  30. package/src/runtime/routes/plugins-routes.ts +5 -2
  31. package/src/skills/validate-input.ts +75 -11
  32. package/src/tools/skills/execute.ts +1 -1
  33. package/src/tools/skills/format-input-schema.ts +15 -0
  34. package/src/tools/skills/load.ts +2 -26
  35. package/src/tools/skills/skill-tool-factory.ts +2 -1
@@ -172,6 +172,17 @@ describe("plugins install by name — disable-platform mode", () => {
172
172
  expect(message).toContain("not in the bundled marketplace catalog");
173
173
  });
174
174
 
175
+ test("does not install a feature-gated bundled integration", async () => {
176
+ process.env.VELLUM_DISABLE_PLATFORM = "true";
177
+ delete process.env.IS_PLATFORM;
178
+
179
+ await runInstall("gamma");
180
+
181
+ expect(installPluginCalls).toHaveLength(0);
182
+ expect(platformInstallCalls).toHaveLength(0);
183
+ expect(process.exitCode).toBe(1);
184
+ });
185
+
175
186
  test("uses the platform install endpoint when platform features are enabled", async () => {
176
187
  delete process.env.VELLUM_DISABLE_PLATFORM;
177
188
  delete process.env.IS_PLATFORM;
@@ -184,6 +195,21 @@ describe("plugins install by name — disable-platform mode", () => {
184
195
  expect(process.exitCode).not.toBe(1);
185
196
  });
186
197
 
198
+ test("does not install a name absent from a successful platform catalog", async () => {
199
+ delete process.env.VELLUM_DISABLE_PLATFORM;
200
+ delete process.env.IS_PLATFORM;
201
+ const errSpy = spyOn(console, "error").mockImplementation(() => {});
202
+
203
+ await runInstall("gamma");
204
+
205
+ expect(installPluginCalls).toHaveLength(0);
206
+ expect(platformInstallCalls).toHaveLength(0);
207
+ expect(process.exitCode).toBe(1);
208
+ expect(String(errSpy.mock.calls[0]?.[0])).toContain(
209
+ "not in the marketplace catalog",
210
+ );
211
+ });
212
+
187
213
  test("installs a platform-visible local package without the platform tar endpoint", async () => {
188
214
  delete process.env.VELLUM_DISABLE_PLATFORM;
189
215
  delete process.env.IS_PLATFORM;
@@ -31,6 +31,7 @@ import {
31
31
  arePlatformFeaturesEnabled,
32
32
  readBundledLocalPluginCatalog,
33
33
  } from "./plugin-catalog-local.js";
34
+ import { isPluginCatalogEntryVisible } from "./plugin-catalog-visibility.js";
34
35
  import { DEFAULT_PLUGIN_REF } from "./plugin-constants.js";
35
36
  import {
36
37
  compareFingerprint,
@@ -338,6 +339,10 @@ export async function inspectPlugin(
338
339
  const local = entry ? readLocal(entry, readInstallMeta(entry.target)) : null;
339
340
  const surfaces = entry ? detectPluginSurfaces(entry.target) : null;
340
341
 
342
+ if (!installed && !isPluginCatalogEntryVisible(name)) {
343
+ throw new PluginInspectNotFoundError(name);
344
+ }
345
+
341
346
  let remote: PluginRemoteInfo | null = null;
342
347
  let remoteError: string | null = null;
343
348
  const installedLocalSource = local?.source?.kind === "local";
@@ -14,6 +14,7 @@ import {
14
14
  readBundledPluginCatalog,
15
15
  } from "./plugin-catalog-local.js";
16
16
  import { fetchPluginCatalogFromPlatform } from "./plugin-catalog-platform.js";
17
+ import { filterPluginCatalogByFeatureFlags } from "./plugin-catalog-visibility.js";
17
18
  import type { PluginCatalog, SearchPluginsDeps } from "./search-plugins.js";
18
19
 
19
20
  /** How long a fetched catalog is served before a refresh is attempted. */
@@ -24,6 +25,8 @@ interface CacheEntry {
24
25
  timestamp: number;
25
26
  }
26
27
 
28
+ type PluginCatalogLoadDeps = Pick<SearchPluginsDeps, "fetch">;
29
+
27
30
  const cache = new Map<string, CacheEntry>();
28
31
 
29
32
  /** Add bundled local packages without overriding platform-authoritative rows. */
@@ -50,9 +53,9 @@ export function mergePlatformCatalogWithBundledLocals(
50
53
  * fetch failure propagates so the caller can surface it (e.g. map a rate-limit
51
54
  * to 503) — no stale catalog is ever served.
52
55
  */
53
- export async function getPluginCatalog(
56
+ async function loadPluginCatalog(
54
57
  ref: string,
55
- deps: SearchPluginsDeps,
58
+ deps: PluginCatalogLoadDeps,
56
59
  ): Promise<PluginCatalog> {
57
60
  if (!arePlatformFeaturesEnabled()) {
58
61
  return { ...readBundledPluginCatalog(), ref };
@@ -69,6 +72,25 @@ export async function getPluginCatalog(
69
72
  return merged;
70
73
  }
71
74
 
75
+ /** Installable catalog with feature-flag visibility applied. */
76
+ export async function getPluginCatalog(
77
+ ref: string,
78
+ deps: SearchPluginsDeps,
79
+ ): Promise<PluginCatalog> {
80
+ return filterPluginCatalogByFeatureFlags(
81
+ await loadPluginCatalog(ref, deps),
82
+ deps.featureFlagEnabled,
83
+ );
84
+ }
85
+
86
+ /** Category metadata for plugins already installed, including hidden entries. */
87
+ export async function getPluginCatalogForInstalledMetadata(
88
+ ref: string,
89
+ deps: PluginCatalogLoadDeps,
90
+ ): Promise<PluginCatalog> {
91
+ return loadPluginCatalog(ref, deps);
92
+ }
93
+
72
94
  /** Invalidate the cache (for testing or forced refresh). */
73
95
  export function invalidatePluginCatalogCache(): void {
74
96
  cache.clear();
@@ -13,6 +13,10 @@
13
13
  */
14
14
 
15
15
  import bundledManifest from "./bundled-marketplace.json" with { type: "json" };
16
+ import {
17
+ isPluginCatalogEntryVisible,
18
+ type PluginCatalogFeatureFlagResolver,
19
+ } from "./plugin-catalog-visibility.js";
16
20
  import {
17
21
  type MarketplaceEntry,
18
22
  marketplaceManifestSchema,
@@ -27,6 +31,7 @@ import {
27
31
  // Re-exported so `local`-tagged CLI commands can gate on platform features
28
32
  // without importing `platform/` directly (cli/no-daemon-internals allows lib).
29
33
  export { arePlatformFeaturesEnabled } from "../../platform/feature-gate.js";
34
+ export { isPluginCatalogEntryVisible };
30
35
 
31
36
  let memoizedEntries: readonly MarketplaceEntry[] | undefined;
32
37
 
@@ -80,6 +85,10 @@ export function readBundledLocalPluginCatalog(): PluginCatalog {
80
85
  */
81
86
  export function resolveBundledPluginSource(
82
87
  name: string,
88
+ isFeatureFlagEnabled?: PluginCatalogFeatureFlagResolver,
83
89
  ): ResolvedPluginSource | null {
90
+ if (!isPluginCatalogEntryVisible(name, isFeatureFlagEnabled)) {
91
+ return null;
92
+ }
84
93
  return resolveMarketplaceSource(name, bundledEntries());
85
94
  }
@@ -0,0 +1,41 @@
1
+ import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
2
+ import type { PluginCatalog } from "./search-plugins.js";
3
+
4
+ export const MCP_CATALOG_QA_INTEGRATIONS_FLAG = "mcp-catalog-qa-integrations";
5
+
6
+ const MCP_CATALOG_QA_INTEGRATION_NAMES = new Set([
7
+ "gamma",
8
+ "guru",
9
+ "intercom",
10
+ "ramp",
11
+ "semrush",
12
+ "typeform",
13
+ ]);
14
+
15
+ export type PluginCatalogFeatureFlagResolver = (key: string) => boolean;
16
+
17
+ export function isPluginCatalogEntryVisible(
18
+ name: string,
19
+ isFeatureFlagEnabled: PluginCatalogFeatureFlagResolver = isAssistantFeatureFlagEnabled,
20
+ ): boolean {
21
+ return (
22
+ !MCP_CATALOG_QA_INTEGRATION_NAMES.has(name) ||
23
+ isFeatureFlagEnabled(MCP_CATALOG_QA_INTEGRATIONS_FLAG)
24
+ );
25
+ }
26
+
27
+ /**
28
+ * Hide catalog entries whose OAuth flows are still being validated. Installed
29
+ * plugins are unaffected because this filter only applies to catalog reads.
30
+ */
31
+ export function filterPluginCatalogByFeatureFlags(
32
+ catalog: PluginCatalog,
33
+ isFeatureFlagEnabled: PluginCatalogFeatureFlagResolver = isAssistantFeatureFlagEnabled,
34
+ ): PluginCatalog {
35
+ const matches = catalog.matches.filter((match) =>
36
+ isPluginCatalogEntryVisible(match.name, isFeatureFlagEnabled),
37
+ );
38
+ return matches.length === catalog.matches.length
39
+ ? catalog
40
+ : { ...catalog, matches };
41
+ }
@@ -46,6 +46,7 @@ import {
46
46
  findCatalogEntry,
47
47
  resolveSourceFromMatch,
48
48
  } from "./plugin-catalog-resolve.js";
49
+ import { isPluginCatalogEntryVisible } from "./plugin-catalog-visibility.js";
49
50
  import { DEFAULT_PLUGIN_REF } from "./plugin-constants.js";
50
51
  import { readValidatedPluginIcon } from "./plugin-icon-file.js";
51
52
  import { fetchMarketplaceEntries } from "./plugin-marketplace.js";
@@ -156,8 +157,8 @@ export class PluginDetailsNotFoundError extends Error {
156
157
  * Resolve the detail view for {@link opts.name}.
157
158
  *
158
159
  * Throws {@link PluginDetailsNotFoundError} when the name is neither installed
159
- * locally nor present in the catalog (gated at the default ref, the GitHub
160
- * marketplace at an explicit historical ref). A gated-catalog outage
160
+ * locally nor present in the visibility-gated catalog at either the default
161
+ * or an explicit historical ref. A gated-catalog outage
161
162
  * ({@link PluginCatalogUnavailableError}) degrades to the on-disk fields only
162
163
  * when a local copy exists; with nothing installed to render it propagates so
163
164
  * the caller can map the transient failure to a retryable 503 rather than a
@@ -400,11 +401,10 @@ async function fetchRawFile(
400
401
  /**
401
402
  * Resolve the external catalog entry claiming {@link name}.
402
403
  *
403
- * The default ref reads the gated catalog (the same source search / install
404
- * use). An explicit historical {@link ref} reads the GitHub marketplace
405
- * manifest at that revision — the gated catalog is ref-agnostic, so honoring a
406
- * reviewed/rolled-back revision is inherently a git lookup, matching the pin
407
- * history / inspect carve-out.
404
+ * Both default and historical-ref lookups apply catalog visibility first. The
405
+ * default ref then reads the gated catalog used by search and install, while an
406
+ * explicit historical {@link ref} reads the GitHub marketplace manifest at
407
+ * that revision.
408
408
  *
409
409
  * A gated-catalog outage (fail-hard {@link PluginCatalogUnavailableError})
410
410
  * propagates when nothing is installed — there is nothing to render and the
@@ -420,6 +420,9 @@ async function resolveCatalogEntry(
420
420
  fetchFn: FetchLike,
421
421
  installed: boolean,
422
422
  ): Promise<PluginSearchMatch | null> {
423
+ if (!isPluginCatalogEntryVisible(name)) {
424
+ return null;
425
+ }
423
426
  try {
424
427
  const match =
425
428
  ref === DEFAULT_PLUGIN_REF
@@ -19,6 +19,8 @@ import type { MarketplaceEntry } from "./plugin-marketplace.js";
19
19
  export interface SearchPluginsDeps {
20
20
  /** HTTP client. Production callers pass `globalThis.fetch.bind(globalThis)`. */
21
21
  readonly fetch: FetchLike;
22
+ /** Optional feature-flag resolver for deterministic tests. */
23
+ readonly featureFlagEnabled?: (key: string) => boolean;
22
24
  }
23
25
 
24
26
  /** Where a catalog match comes from. */
@@ -338,6 +338,14 @@
338
338
  "description": "Show the Add Server action on the MCP settings page. The MCP page itself remains visible; this flag gates only creating new Model Context Protocol server connections.",
339
339
  "defaultEnabled": false
340
340
  },
341
+ {
342
+ "id": "mcp-catalog-qa-integrations",
343
+ "scope": "assistant",
344
+ "key": "mcp-catalog-qa-integrations",
345
+ "label": "MCP Catalog QA Integrations",
346
+ "description": "Show Gamma, Guru, Intercom, Ramp, Semrush, and Typeform in the installable plugin catalog while their OAuth flows are validated. When off, catalog search, detail, and install-by-name cannot resolve them. Existing installed connections remain available.",
347
+ "defaultEnabled": false
348
+ },
341
349
  {
342
350
  "id": "os-beta",
343
351
  "scope": "assistant",
@@ -557,7 +565,7 @@
557
565
  "scope": "both",
558
566
  "key": "sidebar-done",
559
567
  "label": "Sidebar Done",
560
- "description": "Turns archiving a chat into marking it done: the sidebar row carries a Done check instead of the overflow menu, an Old chats page lists the full history, and done chats stay findable in search by default. Off keeps today's Archive wording, the overflow menu, and search that hides archived rows unless the query asks for them.",
568
+ "description": "Turns archiving a chat into marking it done: the sidebar row carries a Done check instead of the overflow menu, an All chats page lists the full history, and done chats stay findable in search by default. Off keeps today's Archive wording, the overflow menu, and search that hides archived rows unless the query asks for them.",
561
569
  "defaultEnabled": false
562
570
  }
563
571
  ]
@@ -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",
@@ -165,8 +165,14 @@ const getCatalogSpy = mock(
165
165
  },
166
166
  );
167
167
 
168
+ const getInstalledMetadataCatalogSpy = mock(
169
+ (ref: string, deps: SearchPluginsDeps): Promise<PluginCatalog> =>
170
+ getCatalogSpy(ref, deps),
171
+ );
172
+
168
173
  mock.module("../../../cli/lib/plugin-catalog-cache.js", () => ({
169
174
  getPluginCatalog: getCatalogSpy,
175
+ getPluginCatalogForInstalledMetadata: getInstalledMetadataCatalogSpy,
170
176
  }));
171
177
 
172
178
  // Mock uninstallPlugin. The handler's error mapping is the wiring under
@@ -471,6 +477,7 @@ beforeEach(() => {
471
477
  describe("GET /v1/plugins", () => {
472
478
  beforeEach(() => {
473
479
  getCatalogSpy.mockClear();
480
+ getInstalledMetadataCatalogSpy.mockClear();
474
481
  // Default to an empty catalog so every installed plugin's category resolves
475
482
  // to `null` (bucketed under "system"). Tests that exercise real categories
476
483
  // override this.
@@ -849,6 +856,7 @@ describe("GET /v1/plugins", () => {
849
856
  );
850
857
 
851
858
  const map = await loadCategoryMapBounded();
859
+ expect(getInstalledMetadataCatalogSpy).toHaveBeenCalledTimes(1);
852
860
  expect(map.get("alpha")).toBe("productivity");
853
861
  });
854
862
 
@@ -48,7 +48,10 @@ import {
48
48
  type InstalledPluginInfo,
49
49
  listInstalledPlugins,
50
50
  } from "../../cli/lib/list-installed-plugins.js";
51
- import { getPluginCatalog } from "../../cli/lib/plugin-catalog-cache.js";
51
+ import {
52
+ getPluginCatalog,
53
+ getPluginCatalogForInstalledMetadata,
54
+ } from "../../cli/lib/plugin-catalog-cache.js";
52
55
  import { resolvePluginSourceFromCatalog } from "../../cli/lib/plugin-catalog-resolve.js";
53
56
  import {
54
57
  DEFAULT_PIN_HISTORY_LIMIT,
@@ -969,7 +972,7 @@ export async function loadCategoryMapBounded(
969
972
  let timer: ReturnType<typeof setTimeout> | undefined;
970
973
  try {
971
974
  const catalog = await Promise.race([
972
- getPluginCatalog(DEFAULT_PLUGIN_REF, {
975
+ getPluginCatalogForInstalledMetadata(DEFAULT_PLUGIN_REF, {
973
976
  fetch: globalThis.fetch.bind(globalThis),
974
977
  }),
975
978
  new Promise<null>((resolve) => {