@vellumai/assistant 0.12.3 → 0.12.4-dev.202609231620.a15f321

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 (209) hide show
  1. package/Dockerfile +7 -0
  2. package/README.md +12 -12
  3. package/docker/mousepad/90-vellum.gschema.override +4 -0
  4. package/node_modules/@vellumai/avatar-manifest/src/__tests__/notification-avatar.test.ts +48 -4
  5. package/node_modules/@vellumai/avatar-manifest/src/notification-avatar.ts +12 -8
  6. package/openapi.yaml +97 -4
  7. package/package.json +1 -1
  8. package/src/__tests__/approval-routes-http.test.ts +4 -2
  9. package/src/__tests__/call-routes-http.test.ts +4 -2
  10. package/src/__tests__/channel-readiness-discord.test.ts +8 -0
  11. package/src/__tests__/channel-readiness-slack-remote.test.ts +8 -0
  12. package/src/__tests__/config-loader-backfill.test.ts +1 -0
  13. package/src/__tests__/conversation-pairing.test.ts +10 -0
  14. package/src/__tests__/conversation-resurface-done.test.ts +289 -0
  15. package/src/__tests__/default-profile-catalog-fallback.test.ts +1 -0
  16. package/src/__tests__/gemini-provider.test.ts +31 -0
  17. package/src/__tests__/inference-profile-validation-jev.test.ts +17 -0
  18. package/src/__tests__/job-handler-registration-guard.test.ts +1 -0
  19. package/src/__tests__/llm-catalog-parity.test.ts +4 -0
  20. package/src/__tests__/llm-resolver-override-or-default.test.ts +43 -0
  21. package/src/__tests__/openai-responses-prompt-cache.test.ts +3 -1
  22. package/src/__tests__/platform-callback-registration.test.ts +46 -1
  23. package/src/__tests__/plugin-api-model-profiles.test.ts +7 -0
  24. package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
  25. package/src/__tests__/pricing.test.ts +33 -0
  26. package/src/__tests__/provider-error-scenarios.test.ts +46 -0
  27. package/src/__tests__/provider-platform-proxy-integration.test.ts +45 -15
  28. package/src/__tests__/retry-fallback-escalation.test.ts +22 -0
  29. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
  30. package/src/__tests__/scaffold-managed-skill-tool.test.ts +116 -66
  31. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -0
  32. package/src/__tests__/send-endpoint-busy.test.ts +4 -2
  33. package/src/__tests__/skill-load-inline-command.test.ts +35 -0
  34. package/src/__tests__/skill-load-tool.test.ts +42 -11
  35. package/src/__tests__/skill-tool-factory.test.ts +50 -2
  36. package/src/__tests__/steer-on-enqueue-question.test.ts +265 -34
  37. package/src/__tests__/validate-input.test.ts +73 -0
  38. package/src/__tests__/workspace-migration-159-rename-colliding-jev-profile-name.test.ts +197 -0
  39. package/src/api/ask-question-tool.ts +50 -0
  40. package/src/api/events/tool-result.ts +11 -0
  41. package/src/api/index.ts +7 -0
  42. package/src/api/requests/dictation.ts +2 -2
  43. package/src/api/responses/home.ts +27 -0
  44. package/src/avatar/notification-avatar.test.ts +42 -34
  45. package/src/avatar/notification-avatar.ts +1 -0
  46. package/src/calls/__tests__/voice-session-bridge.test.ts +63 -1
  47. package/src/calls/voice-escalation-judge.ts +6 -5
  48. package/src/calls/voice-screen-annotation.ts +44 -0
  49. package/src/calls/voice-session-bridge.ts +22 -0
  50. package/src/cli/commands/__tests__/plugins.test.ts +29 -1
  51. package/src/cli/commands/channel-verification-sessions.help.ts +12 -2
  52. package/src/cli/commands/platform/index.help.ts +2 -1
  53. package/src/cli/commands/platform/index.ts +8 -2
  54. package/src/cli/commands/plugins.ts +16 -0
  55. package/src/cli/lib/__tests__/inspect-plugin.test.ts +29 -0
  56. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +92 -2
  57. package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +15 -0
  58. package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +66 -2
  59. package/src/cli/lib/__tests__/plugin-catalog-resolve.test.ts +21 -0
  60. package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +65 -0
  61. package/src/cli/lib/__tests__/plugin-details.test.ts +43 -0
  62. package/src/cli/lib/__tests__/plugins-install-offline.test.ts +26 -0
  63. package/src/cli/lib/bundled-marketplace.json +4 -4
  64. package/src/cli/lib/inspect-plugin.ts +5 -0
  65. package/src/cli/lib/plugin-catalog-cache.ts +41 -4
  66. package/src/cli/lib/plugin-catalog-local.ts +9 -0
  67. package/src/cli/lib/plugin-catalog-platform.ts +48 -9
  68. package/src/cli/lib/plugin-catalog-visibility.ts +41 -0
  69. package/src/cli/lib/plugin-details.ts +10 -7
  70. package/src/cli/lib/search-plugins.ts +2 -0
  71. package/src/config/__tests__/default-profile-catalog.test.ts +61 -1
  72. package/src/config/__tests__/jev-managed-profile.test.ts +82 -0
  73. package/src/config/__tests__/sync-gated-profiles.test.ts +71 -0
  74. package/src/config/bundled-skills/screen-annotation/SKILL.md +6 -3
  75. package/src/config/call-site-defaults.ts +17 -11
  76. package/src/config/default-profile-catalog.ts +118 -19
  77. package/src/config/default-profile-names.ts +69 -0
  78. package/src/config/feature-flag-registry.json +25 -9
  79. package/src/config/inference-profile-validation.ts +7 -6
  80. package/src/config/llm-resolver.ts +16 -0
  81. package/src/config/profile-text-generation.test.ts +9 -0
  82. package/src/config/schemas/call-site-catalog.ts +8 -1
  83. package/src/config/schemas/llm.ts +70 -9
  84. package/src/config/sidebar-done-gate.ts +27 -0
  85. package/src/config/sync-gated-profiles.ts +96 -44
  86. package/src/daemon/__tests__/auto-profile-router.test.ts +228 -0
  87. package/src/daemon/auto-profile-router.ts +220 -0
  88. package/src/daemon/conversation-agent-loop.ts +77 -4
  89. package/src/daemon/conversation-slash.ts +2 -2
  90. package/src/daemon/conversation-turn-finalize.ts +12 -0
  91. package/src/daemon/dictation-text-processing.ts +4 -4
  92. package/src/daemon/handlers/__tests__/conversation-history-search.test.ts +61 -0
  93. package/src/daemon/handlers/config-channels.ts +3 -1
  94. package/src/daemon/handlers/conversation-history.ts +11 -1
  95. package/src/daemon/handlers/conversations.ts +118 -10
  96. package/src/daemon/pkb-reminder-builder.test.ts +10 -3
  97. package/src/daemon/pkb-reminder-builder.ts +1 -1
  98. package/src/desktop/desktop-browser-endpoint.ts +1 -0
  99. package/src/home/feed-types.ts +2 -0
  100. package/src/inbound/platform-callback-registration.ts +19 -6
  101. package/src/ipc/routes/__tests__/conversation-sync-ipc-routes.test.ts +49 -1
  102. package/src/ipc/routes/conversation-sync-ipc-routes.ts +38 -2
  103. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +27 -0
  104. package/src/live-voice/__tests__/protocol.test.ts +22 -0
  105. package/src/live-voice/continuation-judge.ts +1 -1
  106. package/src/live-voice/live-voice-session.ts +7 -6
  107. package/src/live-voice/protocol.ts +16 -5
  108. package/src/messaging/providers/gmail/client.ts +4 -0
  109. package/src/notifications/README.md +11 -3
  110. package/src/notifications/__tests__/home-feed-side-effect.test.ts +87 -3
  111. package/src/notifications/conversation-pairing.ts +23 -8
  112. package/src/notifications/home-feed-mirror.ts +39 -0
  113. package/src/notifications/home-feed-side-effect.ts +46 -19
  114. package/src/oauth/AGENTS.md +7 -1
  115. package/src/permissions/question-prompter.ts +14 -4
  116. package/src/persistence/conversation-crud.ts +114 -3
  117. package/src/persistence/conversation-queries.ts +48 -3
  118. package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +228 -32
  119. package/src/persistence/embeddings/embedding-backend-shutdown.test.ts +47 -0
  120. package/src/persistence/embeddings/embedding-backend.ts +23 -41
  121. package/src/persistence/embeddings/embedding-gemini.test.ts +17 -0
  122. package/src/persistence/embeddings/embedding-gemini.ts +3 -1
  123. package/src/persistence/embeddings/embedding-local.ts +72 -17
  124. package/src/persistence/embeddings/embedding-types.ts +0 -19
  125. package/src/persistence/jobs-store.ts +229 -0
  126. package/src/persistence/migrations/383-normalize-opencode-host-connections.ts +44 -0
  127. package/src/persistence/migrations/__tests__/383-normalize-opencode-host-connections.test.ts +102 -0
  128. package/src/persistence/steps.ts +12 -0
  129. package/src/platform/sync-avatar.test.ts +29 -0
  130. package/src/plugin-api/model-profiles.ts +5 -3
  131. package/src/plugins/defaults/memory/AGENTS.md +5 -3
  132. package/src/plugins/defaults/memory/__tests__/memory-retrospective-skill-card.test.ts +2 -0
  133. package/src/plugins/defaults/memory/__tests__/skill-update-receipt-boundary-guard.test.ts +45 -0
  134. package/src/plugins/defaults/memory/__tests__/skill-update-receipt-job.test.ts +580 -0
  135. package/src/plugins/defaults/memory/graph/tools.ts +9 -5
  136. package/src/plugins/defaults/memory/hooks/conversation-deleted.ts +4 -0
  137. package/src/plugins/defaults/memory/job-handlers.ts +8 -2
  138. package/src/plugins/defaults/memory/memory-retrospective-skill-card.ts +5 -0
  139. package/src/plugins/defaults/memory/skill-update-receipt-job.ts +334 -0
  140. package/src/plugins/defaults/memory/substrate/skill-content.ts +1 -1
  141. package/src/plugins/defaults/memory/tools.test.ts +17 -0
  142. package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +3 -0
  143. package/src/providers/__tests__/connection-model-compat.test.ts +2 -0
  144. package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
  145. package/src/providers/anthropic/client.ts +2 -2
  146. package/src/providers/gemini/client.ts +17 -1
  147. package/src/providers/inference/auth.ts +25 -0
  148. package/src/providers/jev/ask.test.ts +66 -0
  149. package/src/{calls/typesafe-noul.ts → providers/jev/ask.ts} +75 -36
  150. package/src/providers/malformed-tool-call.ts +55 -0
  151. package/src/providers/model-catalog.ts +257 -4
  152. package/src/providers/openai/__tests__/chat-completions-malformed-tool-call.test.ts +105 -0
  153. package/src/providers/openai/chat-completions-provider.ts +30 -0
  154. package/src/providers/openai/codex-models.ts +2 -0
  155. package/src/providers/platform-proxy/constants.ts +5 -0
  156. package/src/providers/retry.ts +68 -36
  157. package/src/providers/search-provider-catalog.ts +10 -0
  158. package/src/providers/vellum-model-routing.test.ts +1 -0
  159. package/src/runtime/__tests__/agent-wake-archived-gate.test.ts +90 -0
  160. package/src/runtime/__tests__/question-resolution.test.ts +32 -0
  161. package/src/runtime/agent-wake.ts +9 -2
  162. package/src/runtime/auth/__tests__/middleware.test.ts +138 -0
  163. package/src/runtime/auth/middleware.ts +15 -11
  164. package/src/runtime/middleware/auth.ts +7 -1
  165. package/src/runtime/pending-interactions.ts +8 -8
  166. package/src/runtime/question-resolution.ts +54 -8
  167. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +146 -0
  168. package/src/runtime/routes/__tests__/dictation-processing.test.ts +174 -0
  169. package/src/runtime/routes/__tests__/global-search-routes.test.ts +74 -1
  170. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +60 -0
  171. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +16 -1
  172. package/src/runtime/routes/__tests__/plugins-routes.test.ts +8 -0
  173. package/src/runtime/routes/conversation-list-routes.ts +16 -14
  174. package/src/runtime/routes/conversation-query-routes.ts +4 -0
  175. package/src/runtime/routes/conversation-routes.ts +4 -2
  176. package/src/runtime/routes/diagnostics-routes.ts +66 -229
  177. package/src/runtime/routes/global-search-routes.ts +22 -7
  178. package/src/runtime/routes/inference-provider-connection-routes.ts +25 -8
  179. package/src/runtime/routes/llm-call-sites-routes.ts +19 -10
  180. package/src/runtime/routes/platform-routes.ts +6 -1
  181. package/src/runtime/routes/plugins-routes.ts +5 -2
  182. package/src/runtime/sync/documents-sidecar-publish.test.ts +50 -4
  183. package/src/runtime/sync/resource-sync-events.ts +11 -0
  184. package/src/runtime/sync/worker-daemon-notify.test.ts +30 -0
  185. package/src/runtime/sync/worker-daemon-notify.ts +44 -0
  186. package/src/runtime/verification-outbound-actions.ts +2 -2
  187. package/src/skills/validate-input.ts +75 -11
  188. package/src/telegram/__tests__/webhook-health.test.ts +8 -0
  189. package/src/tools/ask-question/ask-question-tool.test.ts +38 -0
  190. package/src/tools/ask-question/ask-question-tool.ts +12 -1
  191. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +28 -1
  192. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +34 -0
  193. package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +35 -1
  194. package/src/tools/network/__tests__/web-search.test.ts +96 -0
  195. package/src/tools/network/web-fetch.ts +30 -9
  196. package/src/tools/network/web-search.ts +235 -4
  197. package/src/tools/skills/execute.ts +1 -1
  198. package/src/tools/skills/format-input-schema.ts +15 -0
  199. package/src/tools/skills/load.ts +15 -43
  200. package/src/tools/skills/scaffold-managed.ts +86 -71
  201. package/src/tools/skills/skill-tool-factory.ts +2 -1
  202. package/src/tools/workflows/manage-workflows.ts +4 -2
  203. package/src/util/worker-pipe.test.ts +146 -0
  204. package/src/util/worker-pipe.ts +49 -0
  205. package/src/workspace/migrations/158-repoint-opencode-host-provider-fragments.ts +163 -0
  206. package/src/workspace/migrations/159-rename-colliding-jev-profile-name.ts +344 -0
  207. package/src/workspace/migrations/__tests__/158-repoint-opencode-host-provider-fragments.test.ts +188 -0
  208. package/src/workspace/migrations/registry.ts +4 -0
  209. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +0 -107
@@ -1,39 +1,53 @@
1
1
  /**
2
- * One bounded yes/no question to TypeSafe's System One model (Jev), shared by
3
- * the voice judges. A judge runs only when its call site resolves to the
4
- * TypeSafe provider; every failure comes back as a non-answer so the caller
5
- * keeps its default behavior.
2
+ * One bounded question set to TypeSafe's System One model (Jev), shared by
3
+ * the voice judges and the automatic profile router. A caller runs its
4
+ * question only when its call site resolves to the TypeSafe provider; every
5
+ * failure comes back as a non-answer so the caller keeps its default
6
+ * behavior.
6
7
  */
7
8
 
8
- import type { LLMCallSite } from "../config/schemas/llm.js";
9
- import { type JevJsonValue, noulFromAnswer } from "../providers/jev/client.js";
10
- import { resolveConfiguredProvider } from "../providers/provider-send-message.js";
11
- import type { Provider, ProviderResponse } from "../providers/types.js";
12
- import { getLogger } from "../util/logger.js";
9
+ import type { LLMCallSite } from "../../config/schemas/llm.js";
10
+ import { getLogger } from "../../util/logger.js";
11
+ import { resolveConfiguredProvider } from "../provider-send-message.js";
12
+ import type { Provider, ProviderResponse } from "../types.js";
13
+ import {
14
+ type JevJsonValue,
15
+ type JevQuestions,
16
+ noulFromAnswer,
17
+ } from "./client.js";
13
18
 
14
- const log = getLogger("typesafe-noul");
19
+ const log = getLogger("typesafe-ask");
15
20
 
16
21
  const TYPESAFE_PROVIDER_NAME = "typesafe";
17
22
 
18
- export type TypesafeNoulOutcome =
19
- | "answered"
20
- | "unavailable"
21
- | "timeout"
22
- | "error";
23
+ export type TypesafeOutcome = "answered" | "unavailable" | "timeout" | "error";
24
+
25
+ export interface TypesafeAskResult {
26
+ outcome: TypesafeOutcome;
27
+ /** One entry per question id, present when the outcome is "answered". */
28
+ answers?: Record<string, unknown>;
29
+ latencyMs: number;
30
+ }
23
31
 
24
32
  export interface TypesafeNoulResult {
25
- outcome: TypesafeNoulOutcome;
33
+ outcome: TypesafeOutcome;
26
34
  /** Calibrated P(yes), present when the outcome is "answered". */
27
35
  noul?: number;
28
36
  latencyMs: number;
29
37
  }
30
38
 
31
- /** The call site's provider when it resolves to TypeSafe, else null. */
39
+ /**
40
+ * The call site's provider when it resolves to TypeSafe, else null. Judged
41
+ * on the dispatched adapter's name rather than the configured provider: a
42
+ * managed profile is configured as `vellum` and only substitutes the
43
+ * TypeSafe upstream at dispatch, so the configured name alone would report
44
+ * the managed Jev profile as unavailable.
45
+ */
32
46
  export async function resolveTypesafeProvider(
33
47
  callSite: LLMCallSite,
34
48
  ): Promise<Provider | null> {
35
49
  const resolved = await resolveConfiguredProvider(callSite);
36
- return resolved?.configuredProviderName === TYPESAFE_PROVIDER_NAME
50
+ return resolved?.provider.name === TYPESAFE_PROVIDER_NAME
37
51
  ? resolved.provider
38
52
  : null;
39
53
  }
@@ -65,21 +79,25 @@ function answersFrom(
65
79
  }
66
80
  }
67
81
 
68
- /**
69
- * Ask one noul question. Never rejects. The timeout is a hard race rather
70
- * than trust in the provider honoring the abort, because callers hold user
71
- * audio or background work on the answer.
72
- */
73
- export async function askTypesafeNoul(args: {
82
+ export interface TypesafeAskArgs {
74
83
  callSite: LLMCallSite;
75
84
  conversationId: string;
76
85
  state: JevJsonValue;
77
- instructions: string;
86
+ questions: JevQuestions;
78
87
  timeoutMs: number;
79
88
  signal?: AbortSignal;
80
89
  /** Defaults to {@link resolveTypesafeProvider} for `callSite`. */
81
90
  resolveProvider?: () => Promise<Provider | null>;
82
- }): Promise<TypesafeNoulResult> {
91
+ }
92
+
93
+ /**
94
+ * Ask one question set. Never rejects. The timeout is a hard race rather
95
+ * than trust in the provider honoring the abort, because callers hold user
96
+ * audio or a turn's first token on the answer.
97
+ */
98
+ export async function askTypesafe(
99
+ args: TypesafeAskArgs,
100
+ ): Promise<TypesafeAskResult> {
83
101
  const startedAt = Date.now();
84
102
  const elapsed = () => Date.now() - startedAt;
85
103
  const controller = new AbortController();
@@ -87,7 +105,7 @@ export async function askTypesafeNoul(args: {
87
105
  ? AbortSignal.any([args.signal, controller.signal])
88
106
  : controller.signal;
89
107
 
90
- const ask = async (): Promise<TypesafeNoulResult> => {
108
+ const ask = async (): Promise<TypesafeAskResult> => {
91
109
  try {
92
110
  const provider = await (
93
111
  args.resolveProvider ?? (() => resolveTypesafeProvider(args.callSite))
@@ -104,9 +122,7 @@ export async function askTypesafeNoul(args: {
104
122
  type: "text",
105
123
  text: JSON.stringify({
106
124
  state: args.state,
107
- questions: {
108
- answer: { type: "noul", instructions: args.instructions },
109
- },
125
+ questions: args.questions,
110
126
  }),
111
127
  },
112
128
  ],
@@ -121,20 +137,20 @@ export async function askTypesafeNoul(args: {
121
137
  signal,
122
138
  },
123
139
  );
124
- const noul = noulFromAnswer(answersFrom(response)?.answer);
125
- if (noul === undefined) {
140
+ const answers = answersFrom(response);
141
+ if (answers === null) {
126
142
  log.warn(
127
143
  { callSite: args.callSite, conversationId: args.conversationId },
128
- "TypeSafe judge returned no usable answer",
144
+ "TypeSafe returned no usable answers",
129
145
  );
130
146
  return { outcome: "error", latencyMs: elapsed() };
131
147
  }
132
- return { outcome: "answered", noul, latencyMs: elapsed() };
148
+ return { outcome: "answered", answers, latencyMs: elapsed() };
133
149
  } catch (err) {
134
150
  if (!signal.aborted) {
135
151
  log.warn(
136
152
  { err, callSite: args.callSite, conversationId: args.conversationId },
137
- "TypeSafe judge failed",
153
+ "TypeSafe question failed",
138
154
  );
139
155
  }
140
156
  return { outcome: "error", latencyMs: elapsed() };
@@ -142,7 +158,7 @@ export async function askTypesafeNoul(args: {
142
158
  };
143
159
 
144
160
  let timer: ReturnType<typeof setTimeout> | undefined;
145
- const timedOut = new Promise<TypesafeNoulResult>((resolve) => {
161
+ const timedOut = new Promise<TypesafeAskResult>((resolve) => {
146
162
  timer = setTimeout(() => {
147
163
  controller.abort();
148
164
  resolve({ outcome: "timeout", latencyMs: elapsed() });
@@ -154,3 +170,26 @@ export async function askTypesafeNoul(args: {
154
170
  clearTimeout(timer);
155
171
  }
156
172
  }
173
+
174
+ /** Ask one yes/no question. Never rejects. */
175
+ export async function askTypesafeNoul(
176
+ args: Omit<TypesafeAskArgs, "questions"> & { instructions: string },
177
+ ): Promise<TypesafeNoulResult> {
178
+ const { instructions, ...rest } = args;
179
+ const result = await askTypesafe({
180
+ ...rest,
181
+ questions: { answer: { type: "noul", instructions } },
182
+ });
183
+ if (result.outcome !== "answered") {
184
+ return { outcome: result.outcome, latencyMs: result.latencyMs };
185
+ }
186
+ const noul = noulFromAnswer(result.answers?.answer);
187
+ if (noul === undefined) {
188
+ log.warn(
189
+ { callSite: args.callSite, conversationId: args.conversationId },
190
+ "TypeSafe judge returned no usable answer",
191
+ );
192
+ return { outcome: "error", latencyMs: result.latencyMs };
193
+ }
194
+ return { outcome: "answered", noul, latencyMs: result.latencyMs };
195
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Upstream finish reasons that mean the model tried to call a tool but the
3
+ * call did not parse.
4
+ *
5
+ * Gemini reports these as `MALFORMED_FUNCTION_CALL` (the call was not valid)
6
+ * and `UNEXPECTED_TOOL_CALL` (a call the request did not allow). The native
7
+ * Gemini API returns them as `candidate.finishReason`; OpenRouter normalizes
8
+ * them away in `finish_reason` and passes the raw value through in
9
+ * `native_finish_reason`. Such responses often carry the tail of the broken
10
+ * call as plain assistant text, so they must never be taken as a final answer.
11
+ * Providers throw a status-less `ProviderError` carrying
12
+ * {@link MALFORMED_TOOL_CALL_MESSAGE}, which the retry layer (`retry.ts`)
13
+ * resends once with a corrective note.
14
+ */
15
+
16
+ import { ProviderError } from "../util/errors.js";
17
+
18
+ /** Message fragment shared by the provider throw sites and the retry classifier. */
19
+ export const MALFORMED_TOOL_CALL_MESSAGE =
20
+ "Model emitted a malformed tool call";
21
+
22
+ const MALFORMED_TOOL_CALL_FINISH_REASONS = new Set([
23
+ "MALFORMED_FUNCTION_CALL",
24
+ "UNEXPECTED_TOOL_CALL",
25
+ ]);
26
+
27
+ /**
28
+ * Whether an upstream finish reason denotes a malformed tool call.
29
+ * Case- and whitespace-insensitive; a `null`/`undefined`/empty reason is false.
30
+ */
31
+ export function isMalformedToolCallFinishReason(
32
+ finishReason: string | null | undefined,
33
+ ): boolean {
34
+ if (!finishReason) {
35
+ return false;
36
+ }
37
+ return MALFORMED_TOOL_CALL_FINISH_REASONS.has(
38
+ finishReason.trim().toUpperCase(),
39
+ );
40
+ }
41
+
42
+ /**
43
+ * Status-less provider error for a response that ended on a malformed tool
44
+ * call. The absent status marks it as a stream-content failure, which the
45
+ * retry layer classifies by message.
46
+ */
47
+ export function malformedToolCallError(
48
+ provider: string,
49
+ finishReason: string,
50
+ ): ProviderError {
51
+ return new ProviderError(
52
+ `${MALFORMED_TOOL_CALL_MESSAGE} (finish reason: ${finishReason})`,
53
+ provider,
54
+ );
55
+ }
@@ -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
 
@@ -0,0 +1,105 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { ProviderError } from "../../../util/errors.js";
4
+ import { MALFORMED_TOOL_CALL_MESSAGE } from "../../malformed-tool-call.js";
5
+ import { OpenAIChatCompletionsProvider } from "../chat-completions-provider.js";
6
+
7
+ type MockChunk = {
8
+ choices: Array<{
9
+ delta: { content?: string | null };
10
+ finish_reason?: string | null;
11
+ native_finish_reason?: string | null;
12
+ }>;
13
+ usage?: { prompt_tokens: number; completion_tokens: number };
14
+ };
15
+
16
+ const MESSAGES = [
17
+ {
18
+ role: "user" as const,
19
+ content: [{ type: "text" as const, text: "upload the cover image" }],
20
+ },
21
+ ];
22
+
23
+ function stubProvider(chunks: MockChunk[]): OpenAIChatCompletionsProvider {
24
+ const provider = new OpenAIChatCompletionsProvider(
25
+ "test-key",
26
+ "google/gemini-3.8-flash",
27
+ );
28
+ (provider as unknown as { client: unknown }).client = {
29
+ baseURL: "https://openrouter.example.com/api/v1",
30
+ chat: {
31
+ completions: {
32
+ create: async () => ({
33
+ async *[Symbol.asyncIterator]() {
34
+ for (const c of chunks) {
35
+ yield c;
36
+ }
37
+ },
38
+ }),
39
+ },
40
+ },
41
+ };
42
+ return provider;
43
+ }
44
+
45
+ function finishChunks(
46
+ text: string,
47
+ finishReason: string,
48
+ nativeFinishReason: string,
49
+ ): MockChunk[] {
50
+ return [
51
+ { choices: [{ delta: { content: text } }] },
52
+ {
53
+ choices: [
54
+ {
55
+ delta: {},
56
+ finish_reason: finishReason,
57
+ native_finish_reason: nativeFinishReason,
58
+ },
59
+ ],
60
+ usage: { prompt_tokens: 1, completion_tokens: 2 },
61
+ },
62
+ ];
63
+ }
64
+
65
+ describe("OpenAIChatCompletionsProvider malformed tool-call finish", () => {
66
+ test("throws a status-less malformed tool-call error on native MALFORMED_FUNCTION_CALL", async () => {
67
+ const provider = stubProvider(
68
+ finishChunks(
69
+ ",featuredImage:{_type:image},patch:true}]})",
70
+ "stop",
71
+ "MALFORMED_FUNCTION_CALL",
72
+ ),
73
+ );
74
+
75
+ const error = await provider.sendMessage(MESSAGES).then(
76
+ () => undefined,
77
+ (e: unknown) => e,
78
+ );
79
+
80
+ expect(error).toBeInstanceOf(ProviderError);
81
+ const providerError = error as ProviderError;
82
+ expect(providerError.statusCode).toBeUndefined();
83
+ expect(providerError.message).toContain(MALFORMED_TOOL_CALL_MESSAGE);
84
+ expect(providerError.message).toContain("MALFORMED_FUNCTION_CALL");
85
+ });
86
+
87
+ test("throws on native UNEXPECTED_TOOL_CALL", async () => {
88
+ const provider = stubProvider(
89
+ finishChunks("{}", "error", "UNEXPECTED_TOOL_CALL"),
90
+ );
91
+
92
+ await expect(provider.sendMessage(MESSAGES)).rejects.toThrow(
93
+ MALFORMED_TOOL_CALL_MESSAGE,
94
+ );
95
+ });
96
+
97
+ test("returns the response normally on native STOP", async () => {
98
+ const provider = stubProvider(finishChunks("All done.", "stop", "STOP"));
99
+
100
+ const response = await provider.sendMessage(MESSAGES);
101
+
102
+ expect(response.stopReason).toBe("stop");
103
+ expect(response.content).toEqual([{ type: "text", text: "All done." }]);
104
+ });
105
+ });