@vellumai/assistant 0.11.4-dev.202608192008.2b8f565 → 0.11.4-dev.202608192109.b91153c
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/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +0 -9
- package/package.json +1 -1
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +52 -42
- package/src/messaging/providers/channel-transport.ts +27 -8
- package/src/messaging/providers/discord/send.ts +16 -0
- package/src/messaging/providers/discord/transport.ts +11 -1
- package/src/messaging/providers/index.ts +54 -10
- package/src/messaging/providers/slack/transport.ts +5 -9
- package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
- package/src/messaging/providers/telegram-bot/transport.ts +3 -6
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +30 -0
- package/src/providers/inference/adapter-factory.ts +6 -0
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +340 -3
- package/src/providers/openai/chat-completions-provider.ts +194 -5
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +30 -10
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +32 -58
|
@@ -131,7 +131,6 @@ function buildManagedOutboundRequestId(
|
|
|
131
131
|
chatId: payload.chatId,
|
|
132
132
|
text: normalizedText,
|
|
133
133
|
assistantId: payload.assistantId ?? null,
|
|
134
|
-
chatAction: payload.chatAction ?? null,
|
|
135
134
|
hasAttachments:
|
|
136
135
|
Array.isArray(payload.attachments) && payload.attachments.length > 0,
|
|
137
136
|
approvalRequestId: payload.approval?.requestId ?? null,
|
|
@@ -212,7 +211,6 @@ async function deliverManagedOutboundReply(
|
|
|
212
211
|
chatId: payload.chatId,
|
|
213
212
|
text: payload.text ?? null,
|
|
214
213
|
assistantId: payload.assistantId ?? null,
|
|
215
|
-
chatAction: payload.chatAction ?? null,
|
|
216
214
|
hasAttachments,
|
|
217
215
|
sourceUpdateId: callback.sourceUpdateId ?? null,
|
|
218
216
|
},
|
|
@@ -325,7 +323,12 @@ export async function deliverChannelReply(
|
|
|
325
323
|
): Promise<ChannelDeliveryResult> {
|
|
326
324
|
const managedCallback = parseManagedOutboundCallback(callbackUrl);
|
|
327
325
|
if (managedCallback) {
|
|
328
|
-
await deliverManagedOutboundReply(
|
|
326
|
+
await deliverManagedOutboundReply(
|
|
327
|
+
managedCallback,
|
|
328
|
+
payload,
|
|
329
|
+
bearerToken,
|
|
330
|
+
log,
|
|
331
|
+
);
|
|
329
332
|
return { ok: true };
|
|
330
333
|
}
|
|
331
334
|
|
|
@@ -385,17 +388,7 @@ export async function deliverChannelReply(
|
|
|
385
388
|
// Response may not be JSON for non-Slack channels; that's fine.
|
|
386
389
|
}
|
|
387
390
|
|
|
388
|
-
|
|
389
|
-
log.debug(
|
|
390
|
-
{ chatId: payload.chatId, callbackUrl, chatAction: payload.chatAction },
|
|
391
|
-
"Channel action delivered",
|
|
392
|
-
);
|
|
393
|
-
} else {
|
|
394
|
-
log.info(
|
|
395
|
-
{ chatId: payload.chatId, callbackUrl },
|
|
396
|
-
"Channel reply delivered",
|
|
397
|
-
);
|
|
398
|
-
}
|
|
391
|
+
log.info({ chatId: payload.chatId, callbackUrl }, "Channel reply delivered");
|
|
399
392
|
|
|
400
393
|
return result;
|
|
401
394
|
}
|
|
@@ -168,7 +168,6 @@ export const ChannelReplyPayloadSchema = z.object({
|
|
|
168
168
|
assistantId: z.string().optional(),
|
|
169
169
|
attachments: z.array(AttachmentMetadataSchema).optional(),
|
|
170
170
|
approval: ApprovalUIMetadataSchema.optional(),
|
|
171
|
-
chatAction: z.literal("typing").optional(),
|
|
172
171
|
/**
|
|
173
172
|
* When true, deliver via `chat.postEphemeral` so only the target `user`
|
|
174
173
|
* sees the message.
|
|
@@ -180,14 +179,6 @@ export const ChannelReplyPayloadSchema = z.object({
|
|
|
180
179
|
messageTs: z.string().optional(),
|
|
181
180
|
/** When true, the daemon generates Block Kit blocks from the text before delivery. */
|
|
182
181
|
useBlocks: z.boolean().optional(),
|
|
183
|
-
/** When provided, add or remove an emoji reaction on a message. */
|
|
184
|
-
reaction: z
|
|
185
|
-
.object({
|
|
186
|
-
action: z.enum(["add", "remove"]),
|
|
187
|
-
name: z.string(),
|
|
188
|
-
messageTs: z.string(),
|
|
189
|
-
})
|
|
190
|
-
.optional(),
|
|
191
182
|
/** When provided, set or clear the Slack Assistants API thread status. */
|
|
192
183
|
assistantThreadStatus: z
|
|
193
184
|
.object({
|
package/package.json
CHANGED
|
@@ -57,8 +57,14 @@ mock.module("../../../util/logger.js", () => ({
|
|
|
57
57
|
getLogger: () => ({ debug() {}, info() {}, warn() {}, error() {} }),
|
|
58
58
|
}));
|
|
59
59
|
|
|
60
|
-
const {
|
|
61
|
-
|
|
60
|
+
const {
|
|
61
|
+
deliverDirect,
|
|
62
|
+
sendChannelReaction,
|
|
63
|
+
sendChannelTyping,
|
|
64
|
+
supportsChannelTyping,
|
|
65
|
+
isDirectDelivery,
|
|
66
|
+
getTransportForCallback,
|
|
67
|
+
} = await import("../index.js");
|
|
62
68
|
|
|
63
69
|
const BASE = "https://gateway.internal";
|
|
64
70
|
|
|
@@ -134,17 +140,14 @@ describe("Slack sub-operation selection", () => {
|
|
|
134
140
|
expect(opts.threadTs).toBe("1700.9");
|
|
135
141
|
});
|
|
136
142
|
|
|
137
|
-
test("
|
|
138
|
-
await
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
},
|
|
146
|
-
}),
|
|
147
|
-
);
|
|
143
|
+
test("sendChannelReaction reaches Slack without touching the text path", async () => {
|
|
144
|
+
await sendChannelReaction(`${BASE}/deliver/slack`, {
|
|
145
|
+
chatId: "C1",
|
|
146
|
+
messageId: "1700.5",
|
|
147
|
+
emoji: "white_check_mark",
|
|
148
|
+
action: "add",
|
|
149
|
+
});
|
|
150
|
+
|
|
148
151
|
expect(slack.sendSlackReaction).toHaveBeenCalledTimes(1);
|
|
149
152
|
expect(slack.sendSlackReply).not.toHaveBeenCalled();
|
|
150
153
|
});
|
|
@@ -164,12 +167,11 @@ describe("Slack sub-operation selection", () => {
|
|
|
164
167
|
expect(slack.sendSlackReply).not.toHaveBeenCalled();
|
|
165
168
|
});
|
|
166
169
|
|
|
167
|
-
test("
|
|
168
|
-
await
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
);
|
|
172
|
-
expect(slack.sendSlackReply).toHaveBeenCalledTimes(1);
|
|
170
|
+
test("sendChannelTyping resolves quietly for a channel with no typing capability", async () => {
|
|
171
|
+
const result = await sendChannelTyping(`${BASE}/deliver/slack`, "C1");
|
|
172
|
+
|
|
173
|
+
expect(result).toEqual({ ok: true });
|
|
174
|
+
expect(slack.sendSlackReply).not.toHaveBeenCalled();
|
|
173
175
|
});
|
|
174
176
|
|
|
175
177
|
test("slackStream routes to sendSlackStreamOp ahead of the text path", async () => {
|
|
@@ -191,22 +193,35 @@ describe("Slack sub-operation selection", () => {
|
|
|
191
193
|
});
|
|
192
194
|
|
|
193
195
|
describe("capability gating across channels", () => {
|
|
194
|
-
test("a
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
196
|
+
test("a channel with no reaction capability resolves quietly", async () => {
|
|
197
|
+
// Slack is the only channel that implements it, because the only producer
|
|
198
|
+
// is Slack's own acknowledgement fallback. A channel without the method
|
|
199
|
+
// is not a failed delivery, so nothing is attempted and nothing throws.
|
|
200
|
+
const target = {
|
|
201
|
+
chatId: "C1",
|
|
202
|
+
messageId: "1",
|
|
203
|
+
emoji: "eyes",
|
|
204
|
+
action: "add",
|
|
205
|
+
} as const;
|
|
206
|
+
expect(
|
|
207
|
+
await sendChannelReaction(`${BASE}/deliver/telegram`, target),
|
|
208
|
+
).toEqual({ ok: true });
|
|
209
|
+
expect(telegram.sendTelegramReply).not.toHaveBeenCalled();
|
|
203
210
|
});
|
|
204
211
|
|
|
205
|
-
test("typing
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
);
|
|
212
|
+
test("the typing capability is read from the transport, not the channel name", () => {
|
|
213
|
+
// The heartbeat gate in background-dispatch asks this rather than testing
|
|
214
|
+
// `sourceChannel === "telegram"`, so a channel that implements the method
|
|
215
|
+
// starts showing an indicator without a caller being changed.
|
|
216
|
+
expect(supportsChannelTyping(`${BASE}/deliver/telegram`)).toBe(true);
|
|
217
|
+
expect(supportsChannelTyping(`${BASE}/deliver/discord`)).toBe(true);
|
|
218
|
+
expect(supportsChannelTyping(`${BASE}/deliver/slack`)).toBe(false);
|
|
219
|
+
expect(supportsChannelTyping(`${BASE}/deliver/whatsapp`)).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("sendChannelTyping reaches Telegram's typing indicator", async () => {
|
|
223
|
+
await sendChannelTyping(`${BASE}/deliver/telegram`, "123");
|
|
224
|
+
|
|
210
225
|
expect(telegram.sendTelegramTypingIndicator).toHaveBeenCalledTimes(1);
|
|
211
226
|
});
|
|
212
227
|
|
|
@@ -244,15 +259,10 @@ describe("capability gating across channels", () => {
|
|
|
244
259
|
});
|
|
245
260
|
});
|
|
246
261
|
|
|
247
|
-
test("
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
await deliverDirect(
|
|
252
|
-
`${BASE}/deliver/discord`,
|
|
253
|
-
payload({ chatAction: "typing", text: "hi" }),
|
|
254
|
-
);
|
|
255
|
-
expect(discord.sendDiscordReply).toHaveBeenCalledTimes(1);
|
|
262
|
+
test("sendChannelTyping reaches Discord's typing indicator", async () => {
|
|
263
|
+
await sendChannelTyping(`${BASE}/deliver/discord`, "999");
|
|
264
|
+
|
|
265
|
+
expect(discord.sendDiscordTypingIndicator).toHaveBeenCalledTimes(1);
|
|
256
266
|
});
|
|
257
267
|
|
|
258
268
|
test("a Slack-only stream payload to Discord falls through to deliver", async () => {
|
|
@@ -15,6 +15,15 @@ export interface CallbackContext {
|
|
|
15
15
|
readonly params: Readonly<Record<string, string>>;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/** The message an emoji reaction lands on, and what to do there. */
|
|
19
|
+
export interface ReactionTarget {
|
|
20
|
+
readonly chatId: string;
|
|
21
|
+
/** The target message, in the channel's own id space. */
|
|
22
|
+
readonly messageId: string;
|
|
23
|
+
readonly emoji: string;
|
|
24
|
+
readonly action: "add" | "remove";
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
/**
|
|
19
28
|
* Direct outbound delivery for one channel, wrapping the channel's provider-API
|
|
20
29
|
* send functions behind a uniform surface. Transports are registered statically
|
|
@@ -35,16 +44,26 @@ export interface ChannelTransport {
|
|
|
35
44
|
payload: ChannelReplyPayload,
|
|
36
45
|
): Promise<ChannelDeliveryResult>;
|
|
37
46
|
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Show that the assistant is working, in whatever form the channel has.
|
|
49
|
+
*
|
|
50
|
+
* Takes the place and nothing else, because that is all it needs. A channel
|
|
51
|
+
* without the affordance omits the method, so implementing it is the whole
|
|
52
|
+
* of declaring the capability.
|
|
53
|
+
*/
|
|
54
|
+
typing?(ctx: CallbackContext, chatId: string): Promise<ChannelDeliveryResult>;
|
|
43
55
|
|
|
44
|
-
/**
|
|
45
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Add or remove one of the assistant's own emoji reactions on a message.
|
|
58
|
+
*
|
|
59
|
+
* `messageId` is the target's id in the channel's own space, the same way
|
|
60
|
+
* `chatId` is: Slack spells it as a timestamp, Discord and Telegram as ids.
|
|
61
|
+
* Nothing outside the channel reads it, so nothing outside needs a shared
|
|
62
|
+
* spelling for it.
|
|
63
|
+
*/
|
|
64
|
+
react?(
|
|
46
65
|
ctx: CallbackContext,
|
|
47
|
-
|
|
66
|
+
target: ReactionTarget,
|
|
48
67
|
): Promise<ChannelDeliveryResult>;
|
|
49
68
|
|
|
50
69
|
/** Update an assistant-thread status surface. Routed when `payload.assistantThreadStatus` is set. */
|
|
@@ -210,3 +210,19 @@ export async function sendDiscordAttachments(
|
|
|
210
210
|
totalCount: attachments.length,
|
|
211
211
|
};
|
|
212
212
|
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Show the Discord typing indicator in a channel.
|
|
216
|
+
*
|
|
217
|
+
* Discord expires it after about ten seconds, so a caller that wants it to
|
|
218
|
+
* persist re-sends on a timer rather than clearing it; there is no stop route.
|
|
219
|
+
*/
|
|
220
|
+
export async function sendDiscordTypingIndicator(
|
|
221
|
+
target: DiscordSendTarget,
|
|
222
|
+
): Promise<void> {
|
|
223
|
+
await callDiscordApi(
|
|
224
|
+
"POST",
|
|
225
|
+
`/channels/${encodeURIComponent(target.channelId)}/typing`,
|
|
226
|
+
{},
|
|
227
|
+
);
|
|
228
|
+
}
|
|
@@ -7,7 +7,11 @@ import type {
|
|
|
7
7
|
} from "../channel-transport.js";
|
|
8
8
|
import { openDiscordDmChannel } from "./api.js";
|
|
9
9
|
import type { DiscordSendTarget } from "./send.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
sendDiscordAttachments,
|
|
12
|
+
sendDiscordReply,
|
|
13
|
+
sendDiscordTypingIndicator,
|
|
14
|
+
} from "./send.js";
|
|
11
15
|
|
|
12
16
|
const log = getLogger("discord-transport");
|
|
13
17
|
|
|
@@ -43,6 +47,12 @@ async function sendTarget(
|
|
|
43
47
|
export const discordTransport: ChannelTransport = {
|
|
44
48
|
channel: "discord",
|
|
45
49
|
|
|
50
|
+
async typing(ctx, chatId) {
|
|
51
|
+
await sendDiscordTypingIndicator(await sendTarget(ctx, chatId));
|
|
52
|
+
log.debug({ chatId }, "Discord typing indicator delivered (direct)");
|
|
53
|
+
return { ok: true };
|
|
54
|
+
},
|
|
55
|
+
|
|
46
56
|
async deliver(ctx, payload) {
|
|
47
57
|
const { chatId, text, attachments, approval } = payload;
|
|
48
58
|
const target = await sendTarget(ctx, chatId);
|
|
@@ -16,7 +16,11 @@ import type {
|
|
|
16
16
|
import { a2aTransport } from "./a2a/transport.js";
|
|
17
17
|
import type { DirectDeliveryChannel } from "./callback-routing.js";
|
|
18
18
|
import { channelForCallback } from "./callback-routing.js";
|
|
19
|
-
import type {
|
|
19
|
+
import type {
|
|
20
|
+
CallbackContext,
|
|
21
|
+
ChannelTransport,
|
|
22
|
+
ReactionTarget,
|
|
23
|
+
} from "./channel-transport.js";
|
|
20
24
|
import { discordTransport } from "./discord/transport.js";
|
|
21
25
|
import { slackTransport } from "./slack/transport.js";
|
|
22
26
|
import { telegramTransport } from "./telegram-bot/transport.js";
|
|
@@ -45,6 +49,52 @@ export function getTransportForCallback(
|
|
|
45
49
|
return channel ? TRANSPORTS[channel] : undefined;
|
|
46
50
|
}
|
|
47
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Whether the channel this callback addresses can show a working indicator.
|
|
54
|
+
*
|
|
55
|
+
* Asks the transport rather than the channel id, so a channel that gains the
|
|
56
|
+
* method starts being asked without a caller being told about it.
|
|
57
|
+
*/
|
|
58
|
+
export function supportsChannelTyping(callbackUrl: string): boolean {
|
|
59
|
+
return getTransportForCallback(callbackUrl)?.typing !== undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Show that the assistant is working on the channel this callback addresses.
|
|
64
|
+
*
|
|
65
|
+
* Resolves to nothing when the channel has no such affordance, which is the
|
|
66
|
+
* ordinary case rather than a failure: the indicator is decoration, and a
|
|
67
|
+
* channel that cannot show one is not degraded by its absence.
|
|
68
|
+
*/
|
|
69
|
+
export async function sendChannelTyping(
|
|
70
|
+
callbackUrl: string,
|
|
71
|
+
chatId: string,
|
|
72
|
+
): Promise<ChannelDeliveryResult> {
|
|
73
|
+
const transport = getTransportForCallback(callbackUrl);
|
|
74
|
+
if (!transport?.typing) {
|
|
75
|
+
return { ok: true };
|
|
76
|
+
}
|
|
77
|
+
return transport.typing(callbackContext(callbackUrl), chatId);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Add or remove one of the assistant's own reactions on a message.
|
|
82
|
+
*
|
|
83
|
+
* Resolves to nothing when the channel has none, the same as typing: a
|
|
84
|
+
* reaction is an acknowledgement, and a channel that cannot show one is not a
|
|
85
|
+
* failed delivery.
|
|
86
|
+
*/
|
|
87
|
+
export async function sendChannelReaction(
|
|
88
|
+
callbackUrl: string,
|
|
89
|
+
target: ReactionTarget,
|
|
90
|
+
): Promise<ChannelDeliveryResult> {
|
|
91
|
+
const transport = getTransportForCallback(callbackUrl);
|
|
92
|
+
if (!transport?.react) {
|
|
93
|
+
return { ok: true };
|
|
94
|
+
}
|
|
95
|
+
return transport.react(callbackContext(callbackUrl), target);
|
|
96
|
+
}
|
|
97
|
+
|
|
48
98
|
function callbackContext(callbackUrl: string): CallbackContext {
|
|
49
99
|
const params: Record<string, string> = {};
|
|
50
100
|
try {
|
|
@@ -74,9 +124,9 @@ export function isDirectDelivery(callbackUrl: string): boolean {
|
|
|
74
124
|
* Deliver a channel reply directly to the provider API, bypassing the gateway
|
|
75
125
|
* HTTP proxy. Callers MUST check `isDirectDelivery()` first.
|
|
76
126
|
*
|
|
77
|
-
* Sub-operations (reaction, thread status
|
|
78
|
-
*
|
|
79
|
-
*
|
|
127
|
+
* Sub-operations (reaction, thread status) route to the transport's optional
|
|
128
|
+
* method when both the payload field and the method are present; otherwise
|
|
129
|
+
* the reply is delivered as text / approval / attachments.
|
|
80
130
|
*/
|
|
81
131
|
export async function deliverDirect(
|
|
82
132
|
callbackUrl: string,
|
|
@@ -93,14 +143,8 @@ export async function deliverDirect(
|
|
|
93
143
|
if (payload.slackStream && transport.streamReply) {
|
|
94
144
|
return transport.streamReply(ctx, payload);
|
|
95
145
|
}
|
|
96
|
-
if (payload.reaction && transport.sendReaction) {
|
|
97
|
-
return transport.sendReaction(ctx, payload);
|
|
98
|
-
}
|
|
99
146
|
if (payload.assistantThreadStatus && transport.setThreadStatus) {
|
|
100
147
|
return transport.setThreadStatus(ctx, payload);
|
|
101
148
|
}
|
|
102
|
-
if (payload.chatAction === "typing" && transport.sendTyping) {
|
|
103
|
-
return transport.sendTyping(ctx, payload);
|
|
104
|
-
}
|
|
105
149
|
return transport.deliver(ctx, payload);
|
|
106
150
|
}
|
|
@@ -54,16 +54,12 @@ export const slackTransport: ChannelTransport = {
|
|
|
54
54
|
return { ok: true, ts: sentTs };
|
|
55
55
|
},
|
|
56
56
|
|
|
57
|
-
async
|
|
58
|
-
const reaction = payload.reaction;
|
|
59
|
-
if (!reaction) {
|
|
60
|
-
return { ok: true };
|
|
61
|
-
}
|
|
57
|
+
async react(_ctx, target) {
|
|
62
58
|
await sendSlackReaction(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
59
|
+
target.chatId,
|
|
60
|
+
target.emoji,
|
|
61
|
+
target.messageId,
|
|
62
|
+
target.action,
|
|
67
63
|
);
|
|
68
64
|
return { ok: true };
|
|
69
65
|
},
|
|
@@ -272,10 +272,7 @@ describe("telegramTransport topic targeting", () => {
|
|
|
272
272
|
});
|
|
273
273
|
|
|
274
274
|
test("typing indicators target the topic", async () => {
|
|
275
|
-
await telegramTransport.
|
|
276
|
-
topicCtx,
|
|
277
|
-
payload({ chatAction: "typing" }),
|
|
278
|
-
);
|
|
275
|
+
await telegramTransport.typing!(topicCtx, "123");
|
|
279
276
|
|
|
280
277
|
expect(callTelegramBotApiMock).toHaveBeenCalledWith("sendChatAction", {
|
|
281
278
|
chat_id: "123",
|
|
@@ -291,10 +288,7 @@ describe("telegramTransport topic targeting", () => {
|
|
|
291
288
|
};
|
|
292
289
|
|
|
293
290
|
await telegramTransport.deliver(bareCtx, payload({ useBlocks: false }));
|
|
294
|
-
await telegramTransport.
|
|
295
|
-
bareCtx,
|
|
296
|
-
payload({ chatAction: "typing" }),
|
|
297
|
-
);
|
|
291
|
+
await telegramTransport.typing!(bareCtx, "123");
|
|
298
292
|
|
|
299
293
|
expect(callTelegramBotApiMock).toHaveBeenCalledWith("sendMessage", {
|
|
300
294
|
chat_id: "123",
|
|
@@ -67,12 +67,9 @@ export const telegramTransport: ChannelTransport = {
|
|
|
67
67
|
return { ok: true };
|
|
68
68
|
},
|
|
69
69
|
|
|
70
|
-
async
|
|
71
|
-
await sendTelegramTypingIndicator(
|
|
72
|
-
log.debug(
|
|
73
|
-
{ chatId: payload.chatId },
|
|
74
|
-
"Telegram typing indicator delivered (direct)",
|
|
75
|
-
);
|
|
70
|
+
async typing(ctx, chatId) {
|
|
71
|
+
await sendTelegramTypingIndicator(chatId, threadOptions(ctx));
|
|
72
|
+
log.debug({ chatId }, "Telegram typing indicator delivered (direct)");
|
|
76
73
|
return { ok: true };
|
|
77
74
|
},
|
|
78
75
|
};
|
|
@@ -74,6 +74,36 @@ describe("adapter factory", () => {
|
|
|
74
74
|
).toBe(true);
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
+
test("openai-compatible round-trips thinking as reasoning_content", () => {
|
|
78
|
+
const adapter = buildProviderAdapter("openai-compatible", {
|
|
79
|
+
apiKey: "test-key",
|
|
80
|
+
model: "deepseek-reasoner",
|
|
81
|
+
streamTimeoutMs: 60_000,
|
|
82
|
+
baseURL: "https://example.com/v1",
|
|
83
|
+
useNativeWebSearch: false,
|
|
84
|
+
});
|
|
85
|
+
expect(adapter).toBeInstanceOf(OpenAIChatCompletionsProvider);
|
|
86
|
+
expect(
|
|
87
|
+
(adapter as unknown as { assistantReasoningField?: string })
|
|
88
|
+
.assistantReasoningField,
|
|
89
|
+
).toBe("reasoning_content");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("litellm round-trips thinking as reasoning_content", () => {
|
|
93
|
+
const adapter = buildProviderAdapter("litellm", {
|
|
94
|
+
apiKey: "test-key",
|
|
95
|
+
model: "deepseek-reasoner",
|
|
96
|
+
streamTimeoutMs: 60_000,
|
|
97
|
+
baseURL: "https://example.com/v1",
|
|
98
|
+
useNativeWebSearch: false,
|
|
99
|
+
});
|
|
100
|
+
expect(adapter).toBeInstanceOf(OpenAIChatCompletionsProvider);
|
|
101
|
+
expect(
|
|
102
|
+
(adapter as unknown as { assistantReasoningField?: string })
|
|
103
|
+
.assistantReasoningField,
|
|
104
|
+
).toBe("reasoning_content");
|
|
105
|
+
});
|
|
106
|
+
|
|
77
107
|
test("backfills placeholder content after an aborted empty assistant turn", async () => {
|
|
78
108
|
const adapter = buildProviderAdapter("openai-compatible", {
|
|
79
109
|
apiKey: "test-key",
|
|
@@ -131,6 +131,9 @@ const ADAPTER_FACTORIES: Record<string, AdapterFactory> = {
|
|
|
131
131
|
providerName: "litellm",
|
|
132
132
|
providerLabel: "LiteLLM",
|
|
133
133
|
streamTimeoutMs,
|
|
134
|
+
// Replay thinking as `reasoning_content` so DeepSeek-compatible
|
|
135
|
+
// thinking-mode upstreams accept follow-up requests that include tools.
|
|
136
|
+
assistantReasoningField: "reasoning_content",
|
|
134
137
|
// Generic OpenAI-compat proxies may front a strict backend (vLLM,
|
|
135
138
|
// DeepSeek, Portkey). Backfill empty assistant turns so the request
|
|
136
139
|
// satisfies `content or tool_calls must be set`.
|
|
@@ -144,6 +147,9 @@ const ADAPTER_FACTORIES: Record<string, AdapterFactory> = {
|
|
|
144
147
|
providerName: "openai-compatible",
|
|
145
148
|
providerLabel: "OpenAI-compatible",
|
|
146
149
|
streamTimeoutMs,
|
|
150
|
+
// Replay thinking as `reasoning_content` so DeepSeek-compatible
|
|
151
|
+
// thinking-mode endpoints accept follow-up requests that include tools.
|
|
152
|
+
assistantReasoningField: "reasoning_content",
|
|
147
153
|
// Custom endpoints (Portkey, vLLM, LM Studio, DeepSeek-compat) often
|
|
148
154
|
// reject `{ role: "assistant", content: null }` after a Stop mid-stream
|
|
149
155
|
// or a reasoning-only turn. Same guard as OpenRouter and Vercel AI
|