@vellumai/assistant 0.12.6-staging.3 → 0.12.7-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -0
- package/ARCHITECTURE.md +14 -14
- package/docs/architecture/integrations.md +5 -5
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +2 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +8 -4
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +2 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +8 -4
- package/node_modules/@vellumai/gateway-client/src/__tests__/route-trust-class.test.ts +111 -0
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +5 -2
- package/node_modules/@vellumai/gateway-client/src/index.ts +9 -0
- package/node_modules/@vellumai/gateway-client/src/route-trust-class.ts +94 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +2 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +8 -4
- package/openapi.yaml +153 -5
- package/package.json +1 -1
- package/src/__tests__/channel-policy.test.ts +7 -0
- package/src/__tests__/conversation-routes-disk-view.test.ts +141 -2
- package/src/__tests__/identity-routes.test.ts +1 -0
- package/src/__tests__/list-messages-client-message-id.test.ts +43 -0
- package/src/__tests__/openai-provider.test.ts +27 -0
- package/src/__tests__/skill-execute-input.test.ts +11 -0
- package/src/__tests__/tool-approval-handler.test.ts +4 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +26 -0
- package/src/agent/loop-unparseable-tool-retry.test.ts +333 -0
- package/src/agent/loop.ts +133 -12
- package/src/calls/__tests__/call-controller.test.ts +273 -1
- package/src/calls/__tests__/media-stream-server-integration.test.ts +40 -0
- package/src/calls/__tests__/media-stream-stt-session.test.ts +33 -0
- package/src/calls/call-constants.ts +10 -0
- package/src/calls/call-controller.ts +359 -25
- package/src/calls/media-stream-server.ts +18 -0
- package/src/calls/media-stream-stt-session.ts +30 -9
- package/src/channels/config.ts +8 -0
- package/src/cli/lib/bundled-marketplace.json +39 -0
- package/src/config/composer-preferences.test.ts +97 -0
- package/src/config/composer-preferences.ts +95 -0
- package/src/config/feature-flag-registry.json +16 -8
- package/src/config/schemas/llm.ts +1 -0
- package/src/contacts/types.ts +1 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +18 -2
- package/src/daemon/conversation-agent-loop.ts +3 -0
- package/src/daemon/conversation-messaging.ts +4 -0
- package/src/daemon/conversation-process.ts +15 -0
- package/src/daemon/conversation-queue-manager.ts +2 -0
- package/src/daemon/conversation.ts +2 -0
- package/src/daemon/handlers/shared.ts +13 -2
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/process-message.ts +5 -0
- package/src/i18n/__tests__/i18n.test.ts +25 -0
- package/src/i18n/assistant-text.ts +18 -0
- package/src/i18n/index.ts +1 -0
- package/src/i18n/messages.ts +11 -0
- package/src/ipc/routes/__tests__/route-adapter.test.ts +40 -0
- package/src/ipc/routes/route-adapter.ts +9 -0
- package/src/notifications/README.md +1 -1
- package/src/notifications/signal.ts +4 -0
- package/src/persistence/conversation-key-store.ts +12 -11
- package/src/providers/__tests__/unparseable-tool-args.test.ts +25 -0
- package/src/providers/inference/adapter-factory.ts +6 -0
- package/src/providers/ionet/client.test.ts +143 -0
- package/src/providers/ionet/client.ts +37 -0
- package/src/providers/model-catalog.ts +40 -0
- package/src/providers/openai/chat-completions-provider.ts +33 -2
- package/src/providers/retry.ts +10 -4
- package/src/providers/unparseable-tool-args.ts +29 -5
- package/src/runtime/__tests__/http-router-trust.test.ts +360 -0
- package/src/runtime/__tests__/shared-principal-lookup.test.ts +346 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +51 -1
- package/src/runtime/auth/__tests__/route-policy.test.ts +85 -1
- package/src/runtime/auth/__tests__/scopes.test.ts +36 -0
- package/src/runtime/auth/route-policy.ts +44 -0
- package/src/runtime/auth/scopes.ts +15 -0
- package/src/runtime/auth/types.ts +1 -0
- package/src/runtime/channel-reply-delivery.ts +2 -0
- package/src/runtime/http-router.ts +61 -1
- package/src/runtime/http-types.ts +3 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +35 -10
- package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +48 -0
- package/src/runtime/migrations/vbundle-builder.ts +19 -3
- package/src/runtime/migrations/vbundle-streaming-importer.ts +14 -19
- package/src/runtime/migrations/vbundle-streaming-validator.ts +14 -7
- package/src/runtime/migrations/vbundle-validator.ts +54 -1
- package/src/runtime/routes/composer-settings-routes.test.ts +71 -0
- package/src/runtime/routes/composer-settings-routes.ts +76 -0
- package/src/runtime/routes/conversation-routes.ts +54 -14
- package/src/runtime/routes/identity-routes.ts +2 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -0
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/migration-routes.ts +3 -1
- package/src/runtime/routes/types.ts +4 -1
- package/src/runtime/shared-principal-lookup.ts +235 -0
- package/src/tools/host-terminal/host-shell.ts +4 -2
- package/src/tools/shared/shell-output.test.ts +42 -0
- package/src/tools/shared/shell-output.ts +27 -4
- package/src/tools/skills/execute.ts +10 -7
- package/src/tools/terminal/shell.ts +2 -0
- package/src/tools/tool-approval-handler.ts +9 -1
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Resolve persisted daemon-authored assistant text at a presentation edge. */
|
|
2
|
+
|
|
3
|
+
import { DEFAULT_LOCALE, type SupportedLocale } from "./locales.js";
|
|
4
|
+
import { isMessageKey, t } from "./messages.js";
|
|
5
|
+
|
|
6
|
+
export function resolveStoredMessageText(
|
|
7
|
+
text: string,
|
|
8
|
+
locale: SupportedLocale = DEFAULT_LOCALE,
|
|
9
|
+
): string {
|
|
10
|
+
const trimmed = text.trim();
|
|
11
|
+
if (!isMessageKey(trimmed)) {
|
|
12
|
+
return text;
|
|
13
|
+
}
|
|
14
|
+
const start = text.indexOf(trimmed);
|
|
15
|
+
return `${text.slice(0, start)}${t(trimmed, locale)}${text.slice(
|
|
16
|
+
start + trimmed.length,
|
|
17
|
+
)}`;
|
|
18
|
+
}
|
package/src/i18n/index.ts
CHANGED
package/src/i18n/messages.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { DEFAULT_LOCALE, type SupportedLocale } from "./locales.js";
|
|
|
16
16
|
export const MESSAGE_KEYS = {
|
|
17
17
|
CONVERSATION_TITLE_GENERATING: "conversation.title.generating",
|
|
18
18
|
CONVERSATION_TITLE_UNTITLED: "conversation.title.untitled",
|
|
19
|
+
AGENT_LOOP_UNPARSEABLE_TOOL_RETRY_STOP: "agentLoop.unparseableToolRetryStop",
|
|
19
20
|
PLUGIN_MCP_OAUTH_CREDENTIALS_UNCHECKED:
|
|
20
21
|
"plugin.uninstall.mcp_oauth_credentials_unchecked",
|
|
21
22
|
} as const;
|
|
@@ -27,30 +28,40 @@ export const MESSAGE_CATALOGS: Record<
|
|
|
27
28
|
Record<MessageKey, string>
|
|
28
29
|
> = {
|
|
29
30
|
en: {
|
|
31
|
+
"agentLoop.unparseableToolRetryStop":
|
|
32
|
+
"I stopped after repeated malformed tool calls to keep this conversation from growing indefinitely. Please try again, or switch models if the problem continues.",
|
|
30
33
|
"conversation.title.generating": "Generating title...",
|
|
31
34
|
"conversation.title.untitled": "Untitled Conversation",
|
|
32
35
|
"plugin.uninstall.mcp_oauth_credentials_unchecked":
|
|
33
36
|
"Credential storage is unavailable, so historical plugin MCP OAuth credentials could not be checked.",
|
|
34
37
|
},
|
|
35
38
|
es: {
|
|
39
|
+
"agentLoop.unparseableToolRetryStop":
|
|
40
|
+
"Me detuve después de varias llamadas de herramienta con formato incorrecto para evitar que esta conversación creciera indefinidamente. Inténtalo de nuevo o cambia de modelo si el problema continúa.",
|
|
36
41
|
"conversation.title.generating": "Generando título...",
|
|
37
42
|
"conversation.title.untitled": "Sin título",
|
|
38
43
|
"plugin.uninstall.mcp_oauth_credentials_unchecked":
|
|
39
44
|
"El almacenamiento de credenciales no está disponible, por lo que no se pudieron comprobar las credenciales históricas de OAuth de MCP del plugin.",
|
|
40
45
|
},
|
|
41
46
|
ru: {
|
|
47
|
+
"agentLoop.unparseableToolRetryStop":
|
|
48
|
+
"Выполнение остановлено после нескольких некорректных вызовов инструментов, чтобы этот разговор не рос бесконечно. Попробуйте ещё раз или смените модель, если проблема не исчезнет.",
|
|
42
49
|
"conversation.title.generating": "Создание названия...",
|
|
43
50
|
"conversation.title.untitled": "Без названия",
|
|
44
51
|
"plugin.uninstall.mcp_oauth_credentials_unchecked":
|
|
45
52
|
"Хранилище учетных данных недоступно, поэтому не удалось проверить сохраненные учетные данные OAuth MCP плагина.",
|
|
46
53
|
},
|
|
47
54
|
zh: {
|
|
55
|
+
"agentLoop.unparseableToolRetryStop":
|
|
56
|
+
"在工具调用多次格式错误后,我已停止运行,以免此对话无限增长。请重试;如果问题仍然存在,请切换模型。",
|
|
48
57
|
"conversation.title.generating": "标题生成中...",
|
|
49
58
|
"conversation.title.untitled": "无标题",
|
|
50
59
|
"plugin.uninstall.mcp_oauth_credentials_unchecked":
|
|
51
60
|
"凭据存储不可用,因此无法检查该插件之前的 MCP OAuth 凭据。",
|
|
52
61
|
},
|
|
53
62
|
"zh-TW": {
|
|
63
|
+
"agentLoop.unparseableToolRetryStop":
|
|
64
|
+
"在工具呼叫多次格式錯誤後,我已停止執行,以免此對話無限增長。請重試;如果問題仍然存在,請切換模型。",
|
|
54
65
|
"conversation.title.generating": "標題產生中...",
|
|
55
66
|
"conversation.title.untitled": "未命名",
|
|
56
67
|
"plugin.uninstall.mcp_oauth_credentials_unchecked":
|
|
@@ -41,6 +41,7 @@ interface SchemaEntry {
|
|
|
41
41
|
policy: {
|
|
42
42
|
requiredScopes: string[];
|
|
43
43
|
allowedPrincipalTypes: string[];
|
|
44
|
+
allowedTrustClasses: string[];
|
|
44
45
|
} | null;
|
|
45
46
|
}
|
|
46
47
|
|
|
@@ -130,6 +131,7 @@ describe("routeDefinitionsToIpcMethods — schema shape", () => {
|
|
|
130
131
|
.object({
|
|
131
132
|
requiredScopes: z.array(z.string()),
|
|
132
133
|
allowedPrincipalTypes: z.array(z.string()),
|
|
134
|
+
allowedTrustClasses: z.array(z.string()),
|
|
133
135
|
})
|
|
134
136
|
.nullable(),
|
|
135
137
|
});
|
|
@@ -241,4 +243,42 @@ describe("routeDefinitionsToIpcMethods — policy serialization", () => {
|
|
|
241
243
|
expect(schema[0].policy?.requiredScopes).toEqual(["settings.write"]);
|
|
242
244
|
expect(schema[1].policy?.requiredScopes).toEqual(["settings.write"]);
|
|
243
245
|
});
|
|
246
|
+
|
|
247
|
+
test("a policy declaring no trust classes ships guardian only", async () => {
|
|
248
|
+
// The gateway is the sole policy check on the IPC path, so the
|
|
249
|
+
// effective value must cross the wire rather than be re-derived
|
|
250
|
+
// there from an absent field.
|
|
251
|
+
const routes: RouteDefinition[] = [
|
|
252
|
+
defineRoute({
|
|
253
|
+
operationId: "settings_read",
|
|
254
|
+
endpoint: "settings",
|
|
255
|
+
policy: {
|
|
256
|
+
requiredScopes: ["settings.read"],
|
|
257
|
+
allowedPrincipalTypes: ["actor"],
|
|
258
|
+
},
|
|
259
|
+
}),
|
|
260
|
+
];
|
|
261
|
+
const schema = await getSchema(routes);
|
|
262
|
+
expect(schema[0].policy?.allowedTrustClasses).toEqual(["guardian"]);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("declared trust classes ship verbatim", async () => {
|
|
266
|
+
const routes: RouteDefinition[] = [
|
|
267
|
+
defineRoute({
|
|
268
|
+
operationId: "chat_send",
|
|
269
|
+
endpoint: "chat",
|
|
270
|
+
method: "POST",
|
|
271
|
+
policy: {
|
|
272
|
+
requiredScopes: ["chat.write"],
|
|
273
|
+
allowedPrincipalTypes: ["actor"],
|
|
274
|
+
allowedTrustClasses: ["guardian", "trusted_contact"],
|
|
275
|
+
},
|
|
276
|
+
}),
|
|
277
|
+
];
|
|
278
|
+
const schema = await getSchema(routes);
|
|
279
|
+
expect(schema[0].policy?.allowedTrustClasses).toEqual([
|
|
280
|
+
"guardian",
|
|
281
|
+
"trusted_contact",
|
|
282
|
+
]);
|
|
283
|
+
});
|
|
244
284
|
});
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* RouteDefinition; no lookup or derivation happens here.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { GUARDIAN_ONLY } from "../../runtime/auth/route-policy.js";
|
|
11
12
|
import type { RouteDefinition } from "../../runtime/routes/types.js";
|
|
12
13
|
|
|
13
14
|
function isIpcEligible(r: RouteDefinition): boolean {
|
|
@@ -22,6 +23,10 @@ function isIpcEligible(r: RouteDefinition): boolean {
|
|
|
22
23
|
* unprotected (e.g. health, debug). The gateway respects that and
|
|
23
24
|
* skips enforcement. `policy: { ... }` carries the same scopes /
|
|
24
25
|
* principal types the daemon's HTTP path enforces via `enforcePolicy()`.
|
|
26
|
+
*
|
|
27
|
+
* `allowedTrustClasses` is always present on a non-null policy, resolved
|
|
28
|
+
* to {@link GUARDIAN_ONLY} when the route declares none, so the wire
|
|
29
|
+
* carries the effective value and no default is duplicated gateway-side.
|
|
25
30
|
*/
|
|
26
31
|
interface IpcRouteSchemaEntry {
|
|
27
32
|
operationId: string;
|
|
@@ -30,6 +35,7 @@ interface IpcRouteSchemaEntry {
|
|
|
30
35
|
policy: {
|
|
31
36
|
requiredScopes: string[];
|
|
32
37
|
allowedPrincipalTypes: string[];
|
|
38
|
+
allowedTrustClasses: string[];
|
|
33
39
|
} | null;
|
|
34
40
|
}
|
|
35
41
|
|
|
@@ -44,6 +50,9 @@ function toSchemaEntry(r: RouteDefinition): IpcRouteSchemaEntry {
|
|
|
44
50
|
// shape doesn't carry the `Scope` / `PrincipalType` narrowing.
|
|
45
51
|
requiredScopes: [...r.policy.requiredScopes],
|
|
46
52
|
allowedPrincipalTypes: [...r.policy.allowedPrincipalTypes],
|
|
53
|
+
allowedTrustClasses: [
|
|
54
|
+
...(r.policy.allowedTrustClasses ?? GUARDIAN_ONLY),
|
|
55
|
+
],
|
|
47
56
|
}
|
|
48
57
|
: null,
|
|
49
58
|
};
|
|
@@ -216,7 +216,7 @@ Each policy defines:
|
|
|
216
216
|
| `start_new_conversation` | Pairs the delivery with a new or reused conversation, or appends to the producing one (see Conversation Pairing below). | `vellum` |
|
|
217
217
|
| `continue_existing_conversation` | Resolves the destination chat's home conversation, creating one on first delivery. The delivered post is written there only once the channel acknowledges it. | `telegram`, `slack`, `discord`, and the non-deliverable `whatsapp`, `email`, `a2a`, `plugin` |
|
|
218
218
|
| `push_only` | Pairs nothing. The push deep-links through the vellum delivery's conversation. | `platform` |
|
|
219
|
-
| `not_deliverable` | Channel cannot receive notifications. Pairing returns null IDs. | `phone`
|
|
219
|
+
| `not_deliverable` | Channel cannot receive notifications. Pairing returns null IDs. | `phone`, `vellum-shared` |
|
|
220
220
|
|
|
221
221
|
### Helper Functions
|
|
222
222
|
|
|
@@ -27,6 +27,10 @@ export const NOTIFICATION_SOURCE_CHANNELS = [
|
|
|
27
27
|
{ id: "a2a", description: "Agent-to-agent protocol channel" },
|
|
28
28
|
{ id: "discord", description: "Discord channel" },
|
|
29
29
|
{ id: "plugin", description: "Channel brought by an installed plugin" },
|
|
30
|
+
{
|
|
31
|
+
id: "vellum-shared",
|
|
32
|
+
description: "Conversation a guardian shared with a contact",
|
|
33
|
+
},
|
|
30
34
|
{ id: "scheduler", description: "Scheduled task runner (reminders, cron)" },
|
|
31
35
|
{ id: "watcher", description: "File/event watcher subsystem" },
|
|
32
36
|
] as const;
|
|
@@ -36,6 +36,13 @@ export function _resetFirstConversationSeenForTesting(): void {
|
|
|
36
36
|
firstConversationSeen = false;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export function recordStandardConversationCreated(): void {
|
|
40
|
+
if (firstConversationSeen) {
|
|
41
|
+
cleanupBootstrapFiles("new conversation created after onboarding");
|
|
42
|
+
}
|
|
43
|
+
firstConversationSeen = true;
|
|
44
|
+
}
|
|
45
|
+
|
|
39
46
|
export interface ConversationKeyMapping {
|
|
40
47
|
id: string;
|
|
41
48
|
conversationKey: string;
|
|
@@ -174,6 +181,8 @@ export function getOrCreateConversation(
|
|
|
174
181
|
* what every caller did before this option existed.
|
|
175
182
|
*/
|
|
176
183
|
origin?: ChannelId;
|
|
184
|
+
/** The caller records creation after required setup succeeds. */
|
|
185
|
+
deferBootstrapCleanup?: boolean;
|
|
177
186
|
},
|
|
178
187
|
): {
|
|
179
188
|
conversationId: string;
|
|
@@ -233,17 +242,6 @@ export function getOrCreateConversation(
|
|
|
233
242
|
};
|
|
234
243
|
}
|
|
235
244
|
|
|
236
|
-
// The first standard conversation is the onboarding one, so BOOTSTRAP.md
|
|
237
|
-
// survives its whole duration and any later standard create means
|
|
238
|
-
// onboarding is over. Background rows stay inert: a hidden side thread
|
|
239
|
-
// minted before the user's first visible chat must not consume that slot.
|
|
240
|
-
if (conversationType === "standard") {
|
|
241
|
-
if (firstConversationSeen) {
|
|
242
|
-
cleanupBootstrapFiles("new conversation created after onboarding");
|
|
243
|
-
}
|
|
244
|
-
firstConversationSeen = true;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
245
|
const now = Date.now();
|
|
248
246
|
const conversationId = uuid();
|
|
249
247
|
const customTitle = opts?.title?.trim();
|
|
@@ -300,6 +298,9 @@ export function getOrCreateConversation(
|
|
|
300
298
|
|
|
301
299
|
if (result.created) {
|
|
302
300
|
initConversationDir(result.conversation);
|
|
301
|
+
if (conversationType === "standard" && !opts?.deferBootstrapCleanup) {
|
|
302
|
+
recordStandardConversationCreated();
|
|
303
|
+
}
|
|
303
304
|
// Attribution for every key-materialized conversation: this is the single
|
|
304
305
|
// choke point through which all unseen-key creations flow, and the key
|
|
305
306
|
// shape + caller frames identify the entry point from the log alone. The
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
boundUnparseableToolArgs,
|
|
5
|
+
boundUnparseableToolArgsRaw,
|
|
4
6
|
isUnparseableToolArgs,
|
|
5
7
|
unparseableToolArgsMessage,
|
|
6
8
|
wrapUnparseableToolArgs,
|
|
@@ -31,6 +33,29 @@ describe("unparseable tool args marker", () => {
|
|
|
31
33
|
expect(isUnparseableToolArgs({})).toBe(false);
|
|
32
34
|
});
|
|
33
35
|
|
|
36
|
+
test("bounds long raw args while preserving the marker shape", () => {
|
|
37
|
+
const raw = "a".repeat(1_000);
|
|
38
|
+
const bounded = boundUnparseableToolArgs(wrapUnparseableToolArgs(raw));
|
|
39
|
+
|
|
40
|
+
expect(isUnparseableToolArgs(bounded)).toBe(true);
|
|
41
|
+
expect(bounded).toEqual({ _raw: "a".repeat(200) + "…" });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("bounds raw diagnostic copies with the same preview contract", () => {
|
|
45
|
+
expect(boundUnparseableToolArgsRaw("a".repeat(1_000))).toBe(
|
|
46
|
+
"a".repeat(200) + "…",
|
|
47
|
+
);
|
|
48
|
+
expect(boundUnparseableToolArgsRaw("short")).toBe("short");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("leaves short markers and ordinary input unchanged", () => {
|
|
52
|
+
const marker = wrapUnparseableToolArgs("short");
|
|
53
|
+
const ordinary = { command: "ls" };
|
|
54
|
+
|
|
55
|
+
expect(boundUnparseableToolArgs(marker)).toBe(marker);
|
|
56
|
+
expect(boundUnparseableToolArgs(ordinary)).toBe(ordinary);
|
|
57
|
+
});
|
|
58
|
+
|
|
34
59
|
test("message includes tool name, raw preview, and retry instruction", () => {
|
|
35
60
|
const msg = unparseableToolArgsMessage("ui_show", '{"surface_type": ');
|
|
36
61
|
expect(msg).toContain('"ui_show"');
|
|
@@ -35,6 +35,7 @@ import { AtlasCloudProvider } from "../atlascloud/client.js";
|
|
|
35
35
|
import { BasetenProvider } from "../baseten/client.js";
|
|
36
36
|
import { FireworksProvider } from "../fireworks/client.js";
|
|
37
37
|
import { GeminiProvider } from "../gemini/client.js";
|
|
38
|
+
import { IonetProvider } from "../ionet/client.js";
|
|
38
39
|
import { JevProvider } from "../jev/client.js";
|
|
39
40
|
import { MinimaxProvider } from "../minimax/client.js";
|
|
40
41
|
import { PROVIDER_CATALOG } from "../model-catalog.js";
|
|
@@ -206,6 +207,11 @@ const ADAPTER_FACTORIES: Record<string, AdapterFactory> = {
|
|
|
206
207
|
streamTimeoutMs,
|
|
207
208
|
...(baseURL ? { baseURL } : {}),
|
|
208
209
|
}),
|
|
210
|
+
ionet: ({ apiKey, model, streamTimeoutMs, baseURL }) =>
|
|
211
|
+
new IonetProvider(apiKey, model, {
|
|
212
|
+
streamTimeoutMs,
|
|
213
|
+
...(baseURL ? { baseURL } : {}),
|
|
214
|
+
}),
|
|
209
215
|
typesafe: ({ apiKey, model, streamTimeoutMs, baseURL }) =>
|
|
210
216
|
new JevProvider(apiKey, model, {
|
|
211
217
|
streamTimeoutMs,
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifies the IO Intelligence (io.net) provider wiring: the SDK client must
|
|
3
|
+
* resolve to the documented IO Intelligence chat-completions endpoint unless
|
|
4
|
+
* an explicit override is supplied (custom-connection / test usage).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "bun:test";
|
|
8
|
+
|
|
9
|
+
import { IonetProvider } from "./client.js";
|
|
10
|
+
|
|
11
|
+
type MockChunk = {
|
|
12
|
+
choices: Array<{
|
|
13
|
+
delta: { content?: string | null };
|
|
14
|
+
finish_reason?: string | null;
|
|
15
|
+
}>;
|
|
16
|
+
usage?: { prompt_tokens: number; completion_tokens: number };
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const OK_CHUNKS: MockChunk[] = [
|
|
20
|
+
{
|
|
21
|
+
choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
|
|
22
|
+
usage: { prompt_tokens: 1, completion_tokens: 1 },
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function makeStream(chunks: MockChunk[]): AsyncIterable<MockChunk> {
|
|
27
|
+
return {
|
|
28
|
+
async *[Symbol.asyncIterator]() {
|
|
29
|
+
for (const c of chunks) {
|
|
30
|
+
yield c;
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function clientOf(provider: IonetProvider) {
|
|
37
|
+
return (
|
|
38
|
+
provider as unknown as {
|
|
39
|
+
client: {
|
|
40
|
+
baseURL: string;
|
|
41
|
+
chat: {
|
|
42
|
+
completions: {
|
|
43
|
+
create: (params: unknown) => Promise<AsyncIterable<MockChunk>>;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
).client;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe("IonetProvider", () => {
|
|
52
|
+
test("defaults the SDK client to the IO Intelligence endpoint", async () => {
|
|
53
|
+
const provider = new IonetProvider(
|
|
54
|
+
"test-key",
|
|
55
|
+
"meta-llama/Llama-3.3-70B-Instruct",
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const client = clientOf(provider);
|
|
59
|
+
expect(client.baseURL).toBe("https://api.intelligence.io.solutions/api/v1");
|
|
60
|
+
|
|
61
|
+
// The same base URL must be reported on the response, read off the live
|
|
62
|
+
// client instance rather than a constant.
|
|
63
|
+
client.chat.completions.create = async () => makeStream(OK_CHUNKS);
|
|
64
|
+
const response = await provider.sendMessage([
|
|
65
|
+
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
|
66
|
+
]);
|
|
67
|
+
expect(response.resolvedEndpoint).toBe(
|
|
68
|
+
"https://api.intelligence.io.solutions/api/v1",
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("sends tool_choice auto when tools are offered without an explicit choice", async () => {
|
|
73
|
+
// io.net defaults an unspecified tool_choice to "none" (not OpenAI's
|
|
74
|
+
// "auto"), so the provider must send the explicit default or the model
|
|
75
|
+
// could never invoke an offered tool.
|
|
76
|
+
const provider = new IonetProvider(
|
|
77
|
+
"test-key",
|
|
78
|
+
"meta-llama/Llama-3.3-70B-Instruct",
|
|
79
|
+
);
|
|
80
|
+
const client = clientOf(provider);
|
|
81
|
+
|
|
82
|
+
let seenParams:
|
|
83
|
+
| {
|
|
84
|
+
tools?: unknown;
|
|
85
|
+
tool_choice?: unknown;
|
|
86
|
+
}
|
|
87
|
+
| undefined;
|
|
88
|
+
client.chat.completions.create = async (params) => {
|
|
89
|
+
seenParams = params as { tools?: unknown; tool_choice?: unknown };
|
|
90
|
+
return makeStream(OK_CHUNKS);
|
|
91
|
+
};
|
|
92
|
+
await provider.sendMessage(
|
|
93
|
+
[{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
94
|
+
{
|
|
95
|
+
tools: [
|
|
96
|
+
{
|
|
97
|
+
name: "get_weather",
|
|
98
|
+
description: "Get the weather",
|
|
99
|
+
input_schema: { type: "object", properties: {} },
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
expect(Array.isArray(seenParams?.tools)).toBe(true);
|
|
105
|
+
expect(seenParams?.tool_choice).toBe("auto");
|
|
106
|
+
|
|
107
|
+
// An explicit caller choice still wins.
|
|
108
|
+
let forcedParams: { tool_choice?: unknown } | undefined;
|
|
109
|
+
client.chat.completions.create = async (params) => {
|
|
110
|
+
forcedParams = params as { tool_choice?: unknown };
|
|
111
|
+
return makeStream(OK_CHUNKS);
|
|
112
|
+
};
|
|
113
|
+
await provider.sendMessage(
|
|
114
|
+
[{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
115
|
+
{
|
|
116
|
+
tools: [
|
|
117
|
+
{
|
|
118
|
+
name: "get_weather",
|
|
119
|
+
description: "Get the weather",
|
|
120
|
+
input_schema: { type: "object", properties: {} },
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
config: { tool_choice: { type: "none" } },
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
expect(forcedParams?.tool_choice).toBe("none");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("honours an explicit baseURL override", async () => {
|
|
130
|
+
const provider = new IonetProvider("test-key", "test-model", {
|
|
131
|
+
baseURL: "https://inference.example.test/v1",
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const client = clientOf(provider);
|
|
135
|
+
expect(client.baseURL).toBe("https://inference.example.test/v1");
|
|
136
|
+
|
|
137
|
+
client.chat.completions.create = async () => makeStream(OK_CHUNKS);
|
|
138
|
+
const response = await provider.sendMessage([
|
|
139
|
+
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
|
140
|
+
]);
|
|
141
|
+
expect(response.resolvedEndpoint).toBe("https://inference.example.test/v1");
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { OpenAIChatCompletionsProvider } from "../openai/chat-completions-provider.js";
|
|
2
|
+
|
|
3
|
+
export interface IonetProviderOptions {
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
baseURL?: string;
|
|
6
|
+
streamTimeoutMs?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const DEFAULT_IONET_BASE_URL = "https://api.intelligence.io.solutions/api/v1";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* IO Intelligence (io.net) serves an OpenAI chat-completions API with
|
|
13
|
+
* Hugging Face-style `org/name` model ids, so it rides the shared
|
|
14
|
+
* chat-completions client with its own default base URL.
|
|
15
|
+
*/
|
|
16
|
+
export class IonetProvider extends OpenAIChatCompletionsProvider {
|
|
17
|
+
constructor(
|
|
18
|
+
apiKey: string,
|
|
19
|
+
model: string,
|
|
20
|
+
options: IonetProviderOptions = {},
|
|
21
|
+
) {
|
|
22
|
+
super(apiKey, model, {
|
|
23
|
+
baseURL: options.baseURL?.trim() || DEFAULT_IONET_BASE_URL,
|
|
24
|
+
providerName: "ionet",
|
|
25
|
+
providerLabel: "IO Intelligence",
|
|
26
|
+
streamTimeoutMs: options.streamTimeoutMs,
|
|
27
|
+
// IO Intelligence serves DeepSeek- and GLM-style thinking models whose
|
|
28
|
+
// reasoning arrives as `reasoning_content`; replay it there so
|
|
29
|
+
// follow-up requests that include tools stay acceptable upstream.
|
|
30
|
+
assistantReasoningField: "reasoning_content",
|
|
31
|
+
// io.net defaults an unspecified `tool_choice` to "none" (unlike
|
|
32
|
+
// OpenAI's "auto"), so tools offered without an explicit choice must
|
|
33
|
+
// still send one or the model could never invoke them.
|
|
34
|
+
defaultToolChoiceAuto: true,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -2746,6 +2746,46 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
|
|
|
2746
2746
|
apiKeyUrl: "https://app.baseten.co/settings/api_keys",
|
|
2747
2747
|
apiKeyPlaceholder: "Your Baseten API key",
|
|
2748
2748
|
},
|
|
2749
|
+
{
|
|
2750
|
+
id: "ionet",
|
|
2751
|
+
displayName: "IO Intelligence",
|
|
2752
|
+
subtitle:
|
|
2753
|
+
"Open models served by IO Intelligence, io.net's inference platform (OpenAI-compatible). Requires an IO Intelligence API key.",
|
|
2754
|
+
setupMode: "api-key",
|
|
2755
|
+
setupHint:
|
|
2756
|
+
"Enter your IO Intelligence API key to enable IO Intelligence models.",
|
|
2757
|
+
envVar: "IONET_API_KEY",
|
|
2758
|
+
credentialsGuide: {
|
|
2759
|
+
description: "Sign in to io.net and create an IO Intelligence API key.",
|
|
2760
|
+
url: "https://io.net/docs/guides/intelligence/api-keys-and-secrets",
|
|
2761
|
+
linkLabel: "Open io.net",
|
|
2762
|
+
},
|
|
2763
|
+
// Model ids are Hugging Face-style `org/name`. Context windows and tool
|
|
2764
|
+
// support come from the endpoint's live `GET /models` catalog, and every
|
|
2765
|
+
// entry is accessible at the basic access tier.
|
|
2766
|
+
models: [
|
|
2767
|
+
{
|
|
2768
|
+
id: "meta-llama/Llama-3.3-70B-Instruct",
|
|
2769
|
+
displayName: "Llama 3.3 70B Instruct",
|
|
2770
|
+
contextWindowTokens: 128000,
|
|
2771
|
+
defaultContextWindowTokens: 128000,
|
|
2772
|
+
maxOutputTokens: 4096,
|
|
2773
|
+
supportsToolUse: true,
|
|
2774
|
+
},
|
|
2775
|
+
{
|
|
2776
|
+
id: "openai/gpt-oss-20b",
|
|
2777
|
+
displayName: "GPT-OSS 20B",
|
|
2778
|
+
contextWindowTokens: 64000,
|
|
2779
|
+
defaultContextWindowTokens: 64000,
|
|
2780
|
+
maxOutputTokens: 32768,
|
|
2781
|
+
supportsThinking: true,
|
|
2782
|
+
supportsToolUse: true,
|
|
2783
|
+
},
|
|
2784
|
+
],
|
|
2785
|
+
defaultModel: "meta-llama/Llama-3.3-70B-Instruct",
|
|
2786
|
+
apiKeyUrl: "https://io.net/docs/guides/intelligence/api-keys-and-secrets",
|
|
2787
|
+
apiKeyPlaceholder: "Your IO Intelligence API key",
|
|
2788
|
+
},
|
|
2749
2789
|
{
|
|
2750
2790
|
id: "poolside",
|
|
2751
2791
|
displayName: "Poolside",
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
extractOverflowTokensFromMessage,
|
|
40
40
|
} from "../types.js";
|
|
41
41
|
import {
|
|
42
|
+
boundUnparseableToolArgsRaw,
|
|
42
43
|
isUnparseableToolArgs,
|
|
43
44
|
wrapUnparseableToolArgs,
|
|
44
45
|
} from "../unparseable-tool-args.js";
|
|
@@ -212,6 +213,12 @@ export interface OpenAIChatCompletionsProviderOptions {
|
|
|
212
213
|
* (Fireworks, Together) keep sending `none` / forced choices. Enabled for
|
|
213
214
|
* the generic `openai-compatible` adapter, whose upstream is unknown. */
|
|
214
215
|
omitToolChoiceWhenReasoning?: boolean;
|
|
216
|
+
/** Send `tool_choice: "auto"` whenever tools are offered and the caller
|
|
217
|
+
* left the choice unspecified. Strict OpenAI-compatible upstreams default
|
|
218
|
+
* an unspecified `tool_choice` to "none" (io.net documents exactly that),
|
|
219
|
+
* which would leave the model unable to call any tool; OpenAI's own
|
|
220
|
+
* default is "auto", so this option only exists for upstreams that differ. */
|
|
221
|
+
defaultToolChoiceAuto?: boolean;
|
|
215
222
|
/** Wire field for the output-token limit. OpenAI and OpenAI-compatible
|
|
216
223
|
* backends use `max_completion_tokens`. OpenRouter defaults to
|
|
217
224
|
* `max_tokens` because its parameter router matches that key on
|
|
@@ -864,6 +871,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
864
871
|
private coerceObjectArgsToJsonString: boolean;
|
|
865
872
|
private salvageXmlToolCalls: boolean;
|
|
866
873
|
private omitToolChoiceWhenReasoning: boolean;
|
|
874
|
+
private defaultToolChoiceAuto: boolean;
|
|
867
875
|
private outputTokenLimitField: "max_completion_tokens" | "max_tokens";
|
|
868
876
|
|
|
869
877
|
constructor(
|
|
@@ -896,6 +904,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
896
904
|
options.salvageXmlToolCalls ?? shouldSalvageXmlToolCalls(model);
|
|
897
905
|
this.omitToolChoiceWhenReasoning =
|
|
898
906
|
options.omitToolChoiceWhenReasoning ?? false;
|
|
907
|
+
this.defaultToolChoiceAuto = options.defaultToolChoiceAuto ?? false;
|
|
899
908
|
this.outputTokenLimitField =
|
|
900
909
|
options.outputTokenLimitField ?? "max_completion_tokens";
|
|
901
910
|
}
|
|
@@ -958,6 +967,15 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
958
967
|
maxTokens;
|
|
959
968
|
}
|
|
960
969
|
|
|
970
|
+
// Per-conversation prompt-cache key (OpenAI Chat Completions
|
|
971
|
+
// `prompt_cache_key`). `RetryProvider` stamps it from the durable
|
|
972
|
+
// conversation id for providers in PROMPT_CACHE_KEY_PROVIDERS; omitted
|
|
973
|
+
// from the wire when absent.
|
|
974
|
+
const promptCacheKey = configObj?.promptCacheKey;
|
|
975
|
+
if (typeof promptCacheKey === "string" && promptCacheKey.length > 0) {
|
|
976
|
+
params.prompt_cache_key = promptCacheKey;
|
|
977
|
+
}
|
|
978
|
+
|
|
961
979
|
// Profile-scoped token biasing (e.g. the `suppress-cjk` preset). Resolved
|
|
962
980
|
// and gated to this provider family upstream in `RetryProvider`.
|
|
963
981
|
if (logitBias) {
|
|
@@ -1028,7 +1046,12 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
1028
1046
|
// wire. Catalog providers that honor `none` / forced choices still
|
|
1029
1047
|
// receive them; the generic openai-compatible adapter drops every
|
|
1030
1048
|
// explicit value in thinking mode via `omitToolChoiceWhenReasoning`.
|
|
1031
|
-
const
|
|
1049
|
+
const callerToolChoice = mapNeutralToolChoice(configObj?.tool_choice);
|
|
1050
|
+
const toolChoice =
|
|
1051
|
+
callerToolChoice ??
|
|
1052
|
+
(this.defaultToolChoiceAuto
|
|
1053
|
+
? ("auto" as OpenAI.Chat.Completions.ChatCompletionToolChoiceOption)
|
|
1054
|
+
: undefined);
|
|
1032
1055
|
xmlToolCallSalvageEnabled =
|
|
1033
1056
|
this.salvageXmlToolCalls && toolChoice !== "none";
|
|
1034
1057
|
if (toolChoice !== undefined) {
|
|
@@ -1149,6 +1172,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
1149
1172
|
id: string;
|
|
1150
1173
|
name: string;
|
|
1151
1174
|
args: string;
|
|
1175
|
+
inspectableArgs?: string;
|
|
1152
1176
|
providerMetadata?: ToolUseContent["providerMetadata"];
|
|
1153
1177
|
}
|
|
1154
1178
|
>();
|
|
@@ -1450,6 +1474,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
1450
1474
|
input = JSON.parse(tc.args);
|
|
1451
1475
|
} catch {
|
|
1452
1476
|
input = wrapUnparseableToolArgs(tc.args);
|
|
1477
|
+
tc.inspectableArgs = boundUnparseableToolArgsRaw(tc.args);
|
|
1453
1478
|
}
|
|
1454
1479
|
const objectKeys = coercedObjectKeys.get(tc.name);
|
|
1455
1480
|
if (objectKeys && !isUnparseableToolArgs(input)) {
|
|
@@ -1457,6 +1482,9 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
1457
1482
|
input = decoded.failedKey
|
|
1458
1483
|
? wrapUnparseableToolArgs(tc.args)
|
|
1459
1484
|
: decoded.input;
|
|
1485
|
+
if (decoded.failedKey) {
|
|
1486
|
+
tc.inspectableArgs = boundUnparseableToolArgsRaw(tc.args);
|
|
1487
|
+
}
|
|
1460
1488
|
}
|
|
1461
1489
|
content.push({
|
|
1462
1490
|
type: "tool_use",
|
|
@@ -1484,7 +1512,10 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
1484
1512
|
{
|
|
1485
1513
|
id: tc.id,
|
|
1486
1514
|
type: "function",
|
|
1487
|
-
function: {
|
|
1515
|
+
function: {
|
|
1516
|
+
name: tc.name,
|
|
1517
|
+
arguments: tc.inspectableArgs ?? tc.args,
|
|
1518
|
+
},
|
|
1488
1519
|
},
|
|
1489
1520
|
thoughtSignatureFromToolUseMetadata(
|
|
1490
1521
|
tc.providerMetadata,
|
package/src/providers/retry.ts
CHANGED
|
@@ -78,10 +78,16 @@ const USAGE_ATTRIBUTION_HEADER_NAMES = {
|
|
|
78
78
|
subagentSpawnMode: "X-Vellum-Subagent-Spawn-Mode",
|
|
79
79
|
} as const;
|
|
80
80
|
|
|
81
|
-
/** Providers whose transports consume `promptCacheKey` (OpenAI Responses
|
|
82
|
-
* `prompt_cache_key`); `RetryProvider` derives it from
|
|
83
|
-
* these only.
|
|
84
|
-
|
|
81
|
+
/** Providers whose transports consume `promptCacheKey` (OpenAI Responses /
|
|
82
|
+
* Chat Completions `prompt_cache_key`); `RetryProvider` derives it from
|
|
83
|
+
* `selectionSeed` for these only. `openai-compatible` is included so
|
|
84
|
+
* self-hosted bridges (e.g. a Claude Code shim) receive a stable
|
|
85
|
+
* per-conversation key and can pin server-side sessions / prompt caches to it. */
|
|
86
|
+
const PROMPT_CACHE_KEY_PROVIDERS = new Set([
|
|
87
|
+
"openai",
|
|
88
|
+
"openrouter",
|
|
89
|
+
"openai-compatible",
|
|
90
|
+
]);
|
|
85
91
|
|
|
86
92
|
/** Providers that support the `effort` config (extended thinking / reasoning). */
|
|
87
93
|
const EFFORT_SUPPORTED_PROVIDERS = new Set([
|