@vellumai/assistant 0.12.4-staging.1 → 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.
Files changed (51) hide show
  1. package/openapi.yaml +3 -1
  2. package/package.json +1 -1
  3. package/src/__tests__/config-loader-backfill.test.ts +1 -0
  4. package/src/__tests__/default-profile-catalog-fallback.test.ts +1 -0
  5. package/src/__tests__/inference-profile-validation-jev.test.ts +17 -0
  6. package/src/__tests__/llm-catalog-parity.test.ts +4 -0
  7. package/src/__tests__/llm-resolver-override-or-default.test.ts +43 -0
  8. package/src/__tests__/openai-responses-prompt-cache.test.ts +3 -1
  9. package/src/__tests__/plugin-api-model-profiles.test.ts +7 -0
  10. package/src/__tests__/pricing.test.ts +33 -0
  11. package/src/__tests__/skill-load-tool.test.ts +42 -11
  12. package/src/__tests__/skill-tool-factory.test.ts +50 -2
  13. package/src/__tests__/validate-input.test.ts +73 -0
  14. package/src/__tests__/workspace-migration-159-rename-colliding-jev-profile-name.test.ts +197 -0
  15. package/src/api/requests/dictation.ts +2 -2
  16. package/src/calls/__tests__/typesafe-noul.test.ts +66 -0
  17. package/src/calls/typesafe-noul.ts +8 -2
  18. package/src/calls/voice-escalation-judge.ts +5 -4
  19. package/src/config/__tests__/default-profile-catalog.test.ts +33 -1
  20. package/src/config/__tests__/jev-managed-profile.test.ts +82 -0
  21. package/src/config/call-site-defaults.ts +11 -11
  22. package/src/config/default-profile-catalog.ts +88 -18
  23. package/src/config/default-profile-names.ts +35 -0
  24. package/src/config/inference-profile-validation.ts +7 -6
  25. package/src/config/llm-resolver.ts +16 -0
  26. package/src/config/profile-text-generation.test.ts +9 -0
  27. package/src/config/schemas/call-site-catalog.ts +1 -1
  28. package/src/config/schemas/llm.ts +54 -9
  29. package/src/daemon/conversation-slash.ts +2 -2
  30. package/src/daemon/dictation-text-processing.ts +4 -4
  31. package/src/plugin-api/model-profiles.ts +5 -3
  32. package/src/plugins/defaults/memory/substrate/skill-content.ts +1 -1
  33. package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +3 -0
  34. package/src/providers/__tests__/connection-model-compat.test.ts +2 -0
  35. package/src/providers/anthropic/client.ts +2 -2
  36. package/src/providers/model-catalog.ts +257 -4
  37. package/src/providers/openai/codex-models.ts +2 -0
  38. package/src/runtime/routes/__tests__/dictation-processing.test.ts +174 -0
  39. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +16 -1
  40. package/src/runtime/routes/conversation-routes.ts +4 -2
  41. package/src/runtime/routes/diagnostics-routes.ts +66 -229
  42. package/src/runtime/routes/llm-call-sites-routes.ts +19 -10
  43. package/src/skills/validate-input.ts +75 -11
  44. package/src/tools/skills/execute.ts +1 -1
  45. package/src/tools/skills/format-input-schema.ts +15 -0
  46. package/src/tools/skills/load.ts +2 -26
  47. package/src/tools/skills/skill-tool-factory.ts +2 -1
  48. package/src/tools/workflows/manage-workflows.ts +4 -2
  49. package/src/workspace/migrations/159-rename-colliding-jev-profile-name.ts +344 -0
  50. package/src/workspace/migrations/registry.ts +2 -0
  51. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +0 -107
@@ -14,12 +14,14 @@ import {
14
14
  parseVellumModel,
15
15
  } from "../../providers/vellum-model-routing.js";
16
16
  import {
17
- BACKUP_PROFILE_KEYS,
18
17
  DEFAULT_PROFILE_KEYS,
19
18
  DEFAULT_PROFILE_PROVIDERS,
20
19
  FALLBACK_PROFILE_BY_KEY,
21
- isBackupProfileKey,
22
20
  isDefaultProfileKey,
21
+ isManagedOnlyProfileKey,
22
+ JEV_MANAGED_PROFILE_CALL_SITES,
23
+ JEV_MANAGED_PROFILE_KEY,
24
+ MANAGED_ONLY_PROFILE_KEYS,
23
25
  } from "../default-profile-names.js";
24
26
  import { InputModalitiesSchema } from "../input-modalities.js";
25
27
 
@@ -766,10 +768,12 @@ function unresolvableProfileReason(
766
768
  name: string,
767
769
  backupsResolve: boolean,
768
770
  ): string {
769
- return !backupsResolve &&
770
- (BACKUP_PROFILE_KEYS as readonly string[]).includes(name)
771
- ? "is a managed backup profile, which resolves only while llm.defaultProvider is the managed provider"
772
- : "is not defined in llm.profiles";
771
+ if (backupsResolve || !isManagedOnlyProfileKey(name)) {
772
+ return "is not defined in llm.profiles";
773
+ }
774
+ return name === JEV_MANAGED_PROFILE_KEY
775
+ ? "is the managed Jev profile, which resolves only while llm.defaultProvider is the managed provider"
776
+ : "is a managed backup profile, which resolves only while llm.defaultProvider is the managed provider";
773
777
  }
774
778
 
775
779
  /**
@@ -800,7 +804,7 @@ function referenceableProfileKeys(
800
804
  ): string[] {
801
805
  return Object.entries(profiles ?? {})
802
806
  .filter(([name, value]) => {
803
- if (backupsResolve || !isBackupProfileKey(name)) {
807
+ if (backupsResolve || !isManagedOnlyProfileKey(name)) {
804
808
  return true;
805
809
  }
806
810
  const entry =
@@ -854,7 +858,7 @@ export function collectFallbackProfileIssues(
854
858
  const profileNames = new Set([
855
859
  ...referenceableProfileKeys(profiles, backupsResolve),
856
860
  ...DEFAULT_PROFILE_KEYS,
857
- ...(backupsResolve ? BACKUP_PROFILE_KEYS : []),
861
+ ...(backupsResolve ? MANAGED_ONLY_PROFILE_KEYS : []),
858
862
  ]);
859
863
  const mixProfileNames = new Set(
860
864
  entries
@@ -1004,6 +1008,8 @@ export const LLMSchema = z
1004
1008
  // they join the set only under a managed `llm.defaultProvider`: on a BYOK
1005
1009
  // or ChatGPT default provider they have no body to resolve to, and
1006
1010
  // keeping the reference would strand a selection the picker cannot show.
1011
+ // The managed Jev profile (`JEV_MANAGED_PROFILE_KEY`) is scoped the same
1012
+ // way, as a pin target for the judge call sites.
1007
1013
  // The flag-gated `os-beta` is excluded: it resolves only while a
1008
1014
  // workspace entry exists, so a reference to it is valid only when that
1009
1015
  // entry is present in `config.profiles`. A backup name materialized as a
@@ -1018,9 +1024,23 @@ export const LLMSchema = z
1018
1024
  backupsResolve,
1019
1025
  ),
1020
1026
  ...DEFAULT_PROFILE_KEYS,
1021
- ...(backupsResolve ? BACKUP_PROFILE_KEYS : []),
1027
+ ...(backupsResolve ? MANAGED_ONLY_PROFILE_KEYS : []),
1022
1028
  ]);
1023
1029
  for (const [siteId, siteConfig] of Object.entries(config.callSites ?? {})) {
1030
+ // The Jev profile answers with structured verdicts, so only the sites
1031
+ // built to read one may pin it; a text-producing site would receive
1032
+ // output it cannot use. (Mix arms are covered by the profile-level
1033
+ // mix validation below, which bars the profile from any mix.)
1034
+ if (
1035
+ siteConfig?.profile === JEV_MANAGED_PROFILE_KEY &&
1036
+ !(JEV_MANAGED_PROFILE_CALL_SITES as readonly string[]).includes(siteId)
1037
+ ) {
1038
+ ctx.addIssue({
1039
+ code: "custom",
1040
+ path: ["callSites", siteId, "profile"],
1041
+ message: `Profile "${JEV_MANAGED_PROFILE_KEY}" returns structured answers rather than chat text, so it can only be pinned to ${JEV_MANAGED_PROFILE_CALL_SITES.join(", ")}`,
1042
+ });
1043
+ }
1024
1044
  if (siteConfig?.profile == null) {
1025
1045
  continue;
1026
1046
  }
@@ -1052,6 +1072,20 @@ export const LLMSchema = z
1052
1072
  message: `Profile "${config.advisorProfile}" referenced by llm.advisorProfile ${unresolvableProfileReason(config.advisorProfile, backupsResolve)}`,
1053
1073
  });
1054
1074
  }
1075
+ // The managed Jev profile is a valid reference target for call-site pins
1076
+ // only. Its model returns structured verdicts rather than chat text, so
1077
+ // the conversation positions reject it here as well as at the write
1078
+ // routes, which keeps a raw config write from handing the main agent or
1079
+ // the advisor a model that cannot answer in prose.
1080
+ for (const field of ["activeProfile", "advisorProfile"] as const) {
1081
+ if (config[field] === JEV_MANAGED_PROFILE_KEY) {
1082
+ ctx.addIssue({
1083
+ code: "custom",
1084
+ path: [field],
1085
+ message: `Profile "${JEV_MANAGED_PROFILE_KEY}" referenced by llm.${field} returns structured answers rather than chat text, so it cannot be the conversation or advisor profile`,
1086
+ });
1087
+ }
1088
+ }
1055
1089
 
1056
1090
  // --- Mix profile validation --------------------------------------------
1057
1091
  // Config keys a mix profile must NOT also set (a mix only references other
@@ -1105,6 +1139,17 @@ export const LLMSchema = z
1105
1139
  });
1106
1140
  continue;
1107
1141
  }
1142
+ // A mix is a conversation profile, so an arm that returns structured
1143
+ // verdicts rather than text would hand a chat turn to the decision
1144
+ // model whenever the seeded pick lands on it.
1145
+ if (arm.profile === JEV_MANAGED_PROFILE_KEY) {
1146
+ ctx.addIssue({
1147
+ code: "custom",
1148
+ path: ["profiles", name, "mix", index, "profile"],
1149
+ message: `Mix profile "${name}" references "${JEV_MANAGED_PROFILE_KEY}", which returns structured answers rather than chat text and cannot be a mix constituent.`,
1150
+ });
1151
+ continue;
1152
+ }
1108
1153
  // (b) No nesting — a mix arm must be a standard (non-mix) profile.
1109
1154
  if (mixProfileNames.has(arm.profile)) {
1110
1155
  ctx.addIssue({
@@ -1,5 +1,5 @@
1
1
  import type { InterfaceId } from "../channels/types.js";
2
- import { getUserSelectableProfilesForProvider } from "../config/default-profile-catalog.js";
2
+ import { getConversationProfilesForProvider } from "../config/default-profile-catalog.js";
3
3
  import { resolveEffectiveContextWindow } from "../config/llm-context-resolution.js";
4
4
  import { resolveCallSiteConfig } from "../config/llm-resolver.js";
5
5
  import {
@@ -148,7 +148,7 @@ async function resolveModelCommand(
148
148
  parse: ModelCommandParse,
149
149
  ): Promise<SlashResolution> {
150
150
  const config = getConfig();
151
- const profiles = getUserSelectableProfilesForProvider(
151
+ const profiles = getConversationProfilesForProvider(
152
152
  config.llm.profiles,
153
153
  config.llm.defaultProvider ?? null,
154
154
  );
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Pure text-processing functions for dictation profiles.
3
3
  *
4
- * - expandSnippets: pre-LLM, dictation mode only
5
- * - applyDictionary: post-LLM, dictation + command modes (including fallback paths)
4
+ * - expandSnippets: dictation mode only
5
+ * - applyDictionary: dictation and selected-text edits (including fallback paths)
6
6
  */
7
7
 
8
8
  import type {
@@ -33,7 +33,7 @@ function wrapWordBoundary(
33
33
  }
34
34
 
35
35
  /**
36
- * Expand snippet triggers in text before sending to the LLM.
36
+ * Expand user-configured snippet triggers in dictated text.
37
37
  *
38
38
  * - Only enabled snippets are considered
39
39
  * - Sorted by trigger length descending (longest match wins)
@@ -80,7 +80,7 @@ export function expandSnippets(
80
80
  }
81
81
 
82
82
  /**
83
- * Apply dictionary normalization to text after LLM processing (or on raw fallback).
83
+ * Apply user-configured dictionary replacements to dictated or edited text.
84
84
  *
85
85
  * - Sorted by spoken length descending
86
86
  * - Respects wholeWord (default true) and caseSensitive (default false) per entry
@@ -1,4 +1,4 @@
1
- import { getUserSelectableProfilesForProvider } from "../config/default-profile-catalog.js";
1
+ import { getConversationProfilesForProvider } from "../config/default-profile-catalog.js";
2
2
  import { getConfig } from "../config/loader.js";
3
3
  import { isDispatchableProfile } from "../config/profile-dispatchability.js";
4
4
  import { orderProfileKeys } from "../config/profile-order.js";
@@ -7,7 +7,9 @@ import type { ModelProfileInfo } from "./types.js";
7
7
  /**
8
8
  * List the workspace inference profiles a plugin can route to, in the order a
9
9
  * user-facing model picker presents them (`llm.profileOrder` first, then the
10
- * rest alphabetically). Managed backup routes are internal and omitted.
10
+ * rest alphabetically). Managed backup routes and the managed Jev profile
11
+ * are omitted with the rest of the conversation view: a plugin routes
12
+ * conversation turns, and a decision model cannot answer them.
11
13
  * Metadata-only entries without a provider, model, or mix are not routing
12
14
  * targets, so plugins never see them. Disabled profiles are included and flagged via
13
15
  * {@link ModelProfileInfo.isDisabled}; weighted "mix" profiles are included and
@@ -20,7 +22,7 @@ import type { ModelProfileInfo } from "./types.js";
20
22
  export function getModelProfiles(): ModelProfileInfo[] {
21
23
  const { llm } = getConfig();
22
24
  const { activeProfile } = llm;
23
- const profiles = getUserSelectableProfilesForProvider(
25
+ const profiles = getConversationProfilesForProvider(
24
26
  llm.profiles,
25
27
  llm.defaultProvider ?? null,
26
28
  );
@@ -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 = {
@@ -1,7 +1,9 @@
1
1
  import { PLATFORM_PROVIDER_META } from "./platform-proxy/constants.js";
2
2
 
3
3
  export type LongContextMode =
4
- "native-model" | "provider-request-option" | "unsupported";
4
+ | "native-model"
5
+ | "provider-request-option"
6
+ | "unsupported";
5
7
 
6
8
  export interface CatalogModelPricingTier {
7
9
  /**
@@ -243,6 +245,24 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
243
245
  cacheReadPer1mTokens: 1,
244
246
  },
245
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
+ },
246
266
  {
247
267
  id: "claude-opus-5",
248
268
  displayName: "Claude Opus 5",
@@ -454,6 +474,66 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
454
474
  ],
455
475
  },
456
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
+ },
457
537
  // GPT-5.6 family (Sol / Terra / Luna). cacheRead is the 90% cached-read
458
538
  // discount; cacheWrite is the 1.25x-input rate GPT-5.6+ bills for
459
539
  // prompt tokens written to the cache (reported as `cache_write_tokens`
@@ -1207,6 +1287,24 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
1207
1287
  cacheReadPer1mTokens: 1,
1208
1288
  },
1209
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
+ },
1210
1308
  {
1211
1309
  id: "anthropic/claude-opus-5",
1212
1310
  displayName: "Claude Opus 5",
@@ -1429,6 +1527,126 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
1429
1527
  ],
1430
1528
  },
1431
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
+ },
1432
1650
  // GPT-5.6 family (Sol / Terra / Luna). The `*-pro` slugs are the same
1433
1651
  // underlying models served with `reasoning.mode: pro` at identical
1434
1652
  // rates. cacheWrite is the 1.25x-input rate GPT-5.6+ bills for prompt
@@ -2140,6 +2358,24 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
2140
2358
  cacheReadPer1mTokens: 1,
2141
2359
  },
2142
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
+ },
2143
2379
  {
2144
2380
  id: "anthropic/claude-opus-5",
2145
2381
  displayName: "Claude Opus 5",
@@ -2602,6 +2838,16 @@ export const PROVIDER_CATALOG: ProviderCatalogEntry[] =
2602
2838
  supportsPlatformAuth: PLATFORM_PROVIDER_META[entry.id]?.managed === true,
2603
2839
  }));
2604
2840
 
2841
+ /**
2842
+ * Providers that are routing identities rather than catalog owners. Mirrors
2843
+ * `ROUTING_IDENTITY_PROVIDERS` in `inference/auth.ts`, which cannot be
2844
+ * imported here without a cycle.
2845
+ */
2846
+ const ROUTING_IDENTITY_PROVIDER_IDS: ReadonlySet<string> = new Set([
2847
+ "vellum",
2848
+ "chatgpt",
2849
+ ]);
2850
+
2605
2851
  /**
2606
2852
  * Whether a catalog model produces free-form chat text. Unlisted providers
2607
2853
  * and model ids default to true so custom endpoints and unknown snapshots
@@ -2614,9 +2860,16 @@ export function catalogModelSupportsText(
2614
2860
  if (typeof provider !== "string" || typeof modelId !== "string") {
2615
2861
  return true;
2616
2862
  }
2617
- const model = PROVIDER_CATALOG.find((p) => p.id === provider)?.models.find(
2618
- (m) => m.id === modelId,
2619
- );
2863
+ // A routing identity ("vellum", "chatgpt") owns no catalog models of the
2864
+ // upstream it dispatches to, so judge the model by its catalog owner: a
2865
+ // managed Jev profile is `vellum` + `jev-latest`, and `jev-latest` opts out
2866
+ // of text under `typesafe`.
2867
+ const catalogProvider = ROUTING_IDENTITY_PROVIDER_IDS.has(provider)
2868
+ ? (getCatalogProviderForModel(modelId) ?? provider)
2869
+ : provider;
2870
+ const model = PROVIDER_CATALOG.find(
2871
+ (p) => p.id === catalogProvider,
2872
+ )?.models.find((m) => m.id === modelId);
2620
2873
  return model?.supportsText !== false;
2621
2874
  }
2622
2875
 
@@ -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",