@shareai-lab/kode-sdk 2.7.2 → 2.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/README.zh-CN.md +15 -1
- package/dist/core/agent.d.ts +7 -0
- package/dist/core/agent.js +111 -8
- package/dist/core/skills/management-manager.d.ts +93 -60
- package/dist/core/skills/management-manager.js +661 -391
- package/dist/core/skills/manager.d.ts +4 -0
- package/dist/core/skills/manager.js +39 -16
- package/dist/core/skills/types.d.ts +12 -0
- package/dist/core/types.d.ts +9 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -2
- package/dist/infra/opensandbox/index.d.ts +3 -0
- package/dist/infra/opensandbox/index.js +7 -0
- package/dist/infra/opensandbox/opensandbox-fs.d.ts +27 -0
- package/dist/infra/opensandbox/opensandbox-fs.js +113 -0
- package/dist/infra/opensandbox/opensandbox-sandbox.d.ts +35 -0
- package/dist/infra/opensandbox/opensandbox-sandbox.js +325 -0
- package/dist/infra/opensandbox/types.d.ts +30 -0
- package/dist/infra/opensandbox/types.js +2 -0
- package/dist/infra/providers/anthropic.js +16 -0
- package/dist/infra/providers/gemini.js +32 -4
- package/dist/infra/providers/openai.js +27 -3
- package/dist/infra/providers/types.d.ts +35 -1
- package/dist/infra/providers/utils.d.ts +19 -1
- package/dist/infra/providers/utils.js +83 -3
- package/dist/infra/sandbox-factory.js +5 -0
- package/dist/infra/sandbox.d.ts +1 -1
- package/dist/infra/store/json-store.js +2 -1
- package/dist/tools/skills.js +2 -2
- package/dist/tools/type-inference.d.ts +1 -1
- package/package.json +6 -3
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OpenSandbox = void 0;
|
|
4
|
+
const opensandbox_1 = require("@alibaba-group/opensandbox");
|
|
5
|
+
const logger_1 = require("../../utils/logger");
|
|
6
|
+
const opensandbox_fs_1 = require("./opensandbox-fs");
|
|
7
|
+
class OpenSandbox {
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.kind = 'opensandbox';
|
|
10
|
+
this.sandbox = null;
|
|
11
|
+
this.watchers = new Map();
|
|
12
|
+
this.options = { ...options };
|
|
13
|
+
this.workDir = options.workDir || '/workspace';
|
|
14
|
+
this.fs = new opensandbox_fs_1.OpenSandboxFS(this);
|
|
15
|
+
this.watchMode = options.watch?.mode || 'polling';
|
|
16
|
+
this.pollIntervalMs = Math.max(100, options.watch?.pollIntervalMs ?? 1000);
|
|
17
|
+
this.disposeAction = options.lifecycle?.disposeAction || 'kill';
|
|
18
|
+
}
|
|
19
|
+
async init() {
|
|
20
|
+
if (this.sandbox)
|
|
21
|
+
return;
|
|
22
|
+
const connectionConfig = this.buildConnectionConfig();
|
|
23
|
+
if (this.options.sandboxId) {
|
|
24
|
+
const connectOptions = {
|
|
25
|
+
sandboxId: this.options.sandboxId,
|
|
26
|
+
connectionConfig,
|
|
27
|
+
skipHealthCheck: this.options.skipHealthCheck,
|
|
28
|
+
readyTimeoutSeconds: this.options.readyTimeoutSeconds,
|
|
29
|
+
healthCheckPollingInterval: this.options.healthCheckPollingInterval,
|
|
30
|
+
};
|
|
31
|
+
this.sandbox = await opensandbox_1.Sandbox.connect(connectOptions);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
const createOptions = {
|
|
35
|
+
connectionConfig,
|
|
36
|
+
image: this.options.image || this.options.template || 'ubuntu',
|
|
37
|
+
timeoutSeconds: Math.max(1, Math.ceil((this.options.timeoutMs ?? 10 * 60 * 1000) / 1000)),
|
|
38
|
+
env: this.options.env,
|
|
39
|
+
metadata: this.options.metadata,
|
|
40
|
+
resource: this.options.resource,
|
|
41
|
+
networkPolicy: this.options.networkPolicy,
|
|
42
|
+
skipHealthCheck: this.options.skipHealthCheck,
|
|
43
|
+
readyTimeoutSeconds: this.options.readyTimeoutSeconds,
|
|
44
|
+
healthCheckPollingInterval: this.options.healthCheckPollingInterval,
|
|
45
|
+
};
|
|
46
|
+
this.sandbox = await opensandbox_1.Sandbox.create(createOptions);
|
|
47
|
+
}
|
|
48
|
+
// Persist resolved sandbox id for Agent resume metadata.
|
|
49
|
+
this.options.sandboxId = this.sandbox.id;
|
|
50
|
+
// Best-effort workdir bootstrap.
|
|
51
|
+
if (this.workDir && this.workDir !== '/') {
|
|
52
|
+
await this.sandbox.commands
|
|
53
|
+
.run(`mkdir -p ${quoteShell(this.workDir)}`, {
|
|
54
|
+
workingDirectory: '/',
|
|
55
|
+
timeoutSeconds: 10,
|
|
56
|
+
})
|
|
57
|
+
.catch(() => undefined);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
getOpenSandbox() {
|
|
61
|
+
if (!this.sandbox) {
|
|
62
|
+
throw new Error('OpenSandbox not initialized. Call init() first.');
|
|
63
|
+
}
|
|
64
|
+
return this.sandbox;
|
|
65
|
+
}
|
|
66
|
+
getSandboxId() {
|
|
67
|
+
return this.sandbox?.id || this.options.sandboxId;
|
|
68
|
+
}
|
|
69
|
+
async isRunning() {
|
|
70
|
+
try {
|
|
71
|
+
const info = await this.getOpenSandbox().getInfo();
|
|
72
|
+
const state = String(info?.status?.state || '').toLowerCase();
|
|
73
|
+
return state === 'running';
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async exec(cmd, opts) {
|
|
80
|
+
const sandbox = this.getOpenSandbox();
|
|
81
|
+
const timeoutMs = this.resolveExecTimeoutMs(opts);
|
|
82
|
+
const timeoutSeconds = Math.max(1, Math.ceil(timeoutMs / 1000));
|
|
83
|
+
try {
|
|
84
|
+
const execution = await sandbox.commands.run(cmd, {
|
|
85
|
+
workingDirectory: this.workDir,
|
|
86
|
+
timeoutSeconds,
|
|
87
|
+
});
|
|
88
|
+
const stdout = execution.logs.stdout.map((m) => m.text).join('');
|
|
89
|
+
let stderr = execution.logs.stderr.map((m) => m.text).join('');
|
|
90
|
+
let code = execution.error ? 1 : 0;
|
|
91
|
+
if (execution.id) {
|
|
92
|
+
try {
|
|
93
|
+
const status = await sandbox.commands.getCommandStatus(execution.id);
|
|
94
|
+
if (typeof status.exitCode === 'number') {
|
|
95
|
+
code = status.exitCode;
|
|
96
|
+
}
|
|
97
|
+
else if (status.running === false && status.error) {
|
|
98
|
+
code = 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// keep fallback code when status API is unavailable
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (execution.error && !stderr) {
|
|
106
|
+
const traces = Array.isArray(execution.error.traceback) ? execution.error.traceback.join('\n') : '';
|
|
107
|
+
stderr = [execution.error.name, execution.error.value, traces].filter(Boolean).join('\n');
|
|
108
|
+
}
|
|
109
|
+
return { code, stdout, stderr };
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
return {
|
|
113
|
+
code: 1,
|
|
114
|
+
stdout: '',
|
|
115
|
+
stderr: error?.message || String(error),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async watchFiles(paths, listener) {
|
|
120
|
+
if (this.watchMode === 'off') {
|
|
121
|
+
return `watch-disabled-${Date.now()}`;
|
|
122
|
+
}
|
|
123
|
+
const id = `opensandbox-watch-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
124
|
+
const resolved = Array.from(new Set(paths.map((p) => this.fs.resolve(p))));
|
|
125
|
+
if (this.watchMode === 'native') {
|
|
126
|
+
const nativeStarted = await this.startNativeWatcher(id, resolved, listener);
|
|
127
|
+
if (nativeStarted) {
|
|
128
|
+
return id;
|
|
129
|
+
}
|
|
130
|
+
logger_1.logger.warn('[OpenSandbox] native watch unavailable, falling back to polling mode.');
|
|
131
|
+
}
|
|
132
|
+
await this.startPollingWatcher(id, resolved, listener);
|
|
133
|
+
return id;
|
|
134
|
+
}
|
|
135
|
+
unwatchFiles(id) {
|
|
136
|
+
const watcher = this.watchers.get(id);
|
|
137
|
+
if (!watcher)
|
|
138
|
+
return;
|
|
139
|
+
if (watcher.kind === 'polling') {
|
|
140
|
+
clearInterval(watcher.timer);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
watcher.abortController.abort();
|
|
144
|
+
}
|
|
145
|
+
this.watchers.delete(id);
|
|
146
|
+
}
|
|
147
|
+
async dispose() {
|
|
148
|
+
for (const id of Array.from(this.watchers.keys())) {
|
|
149
|
+
this.unwatchFiles(id);
|
|
150
|
+
}
|
|
151
|
+
if (!this.sandbox)
|
|
152
|
+
return;
|
|
153
|
+
let disposeError;
|
|
154
|
+
const sandbox = this.sandbox;
|
|
155
|
+
this.sandbox = null;
|
|
156
|
+
if (this.disposeAction === 'kill') {
|
|
157
|
+
try {
|
|
158
|
+
await sandbox.kill();
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
disposeError = error;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
await sandbox.close();
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
disposeError = disposeError || error;
|
|
169
|
+
}
|
|
170
|
+
if (disposeError) {
|
|
171
|
+
throw disposeError;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
buildConnectionConfig() {
|
|
175
|
+
const config = {
|
|
176
|
+
apiKey: this.options.apiKey,
|
|
177
|
+
domain: this.options.endpoint || this.options.domain,
|
|
178
|
+
protocol: this.options.protocol,
|
|
179
|
+
requestTimeoutSeconds: this.options.requestTimeoutSeconds,
|
|
180
|
+
useServerProxy: this.options.useServerProxy ?? false,
|
|
181
|
+
};
|
|
182
|
+
return new opensandbox_1.ConnectionConfig(config);
|
|
183
|
+
}
|
|
184
|
+
async pollWatcher(id, listener) {
|
|
185
|
+
const watcher = this.watchers.get(id);
|
|
186
|
+
if (!watcher || watcher.kind !== 'polling' || watcher.polling)
|
|
187
|
+
return;
|
|
188
|
+
watcher.polling = true;
|
|
189
|
+
try {
|
|
190
|
+
do {
|
|
191
|
+
watcher.pending = false;
|
|
192
|
+
for (const path of watcher.paths) {
|
|
193
|
+
if (!this.watchers.has(id)) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const current = await this.safeMtime(path);
|
|
197
|
+
const previous = watcher.lastMtimes.get(path);
|
|
198
|
+
watcher.lastMtimes.set(path, current);
|
|
199
|
+
if (previous === undefined && current === undefined)
|
|
200
|
+
continue;
|
|
201
|
+
if (previous === current)
|
|
202
|
+
continue;
|
|
203
|
+
listener({ path, mtimeMs: current ?? Date.now() });
|
|
204
|
+
}
|
|
205
|
+
} while (this.watchers.has(id) && watcher.pending);
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
if (this.watchers.get(id) === watcher) {
|
|
209
|
+
watcher.polling = false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async safeMtime(path) {
|
|
214
|
+
try {
|
|
215
|
+
const stat = await this.fs.stat(path);
|
|
216
|
+
return stat.mtimeMs;
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
resolveExecTimeoutMs(opts) {
|
|
223
|
+
return opts?.timeoutMs ?? this.options.execTimeoutMs ?? this.options.timeoutMs ?? 120000;
|
|
224
|
+
}
|
|
225
|
+
async startPollingWatcher(id, paths, listener) {
|
|
226
|
+
const lastMtimes = new Map();
|
|
227
|
+
for (const p of paths) {
|
|
228
|
+
lastMtimes.set(p, await this.safeMtime(p));
|
|
229
|
+
}
|
|
230
|
+
const watcher = {
|
|
231
|
+
kind: 'polling',
|
|
232
|
+
timer: setInterval(() => {
|
|
233
|
+
const current = this.watchers.get(id);
|
|
234
|
+
if (!current || current.kind !== 'polling')
|
|
235
|
+
return;
|
|
236
|
+
current.pending = true;
|
|
237
|
+
if (!current.polling) {
|
|
238
|
+
void this.pollWatcher(id, listener);
|
|
239
|
+
}
|
|
240
|
+
}, this.pollIntervalMs),
|
|
241
|
+
paths,
|
|
242
|
+
lastMtimes,
|
|
243
|
+
polling: false,
|
|
244
|
+
pending: true,
|
|
245
|
+
};
|
|
246
|
+
this.watchers.set(id, watcher);
|
|
247
|
+
void this.pollWatcher(id, listener);
|
|
248
|
+
}
|
|
249
|
+
async startNativeWatcher(id, paths, listener) {
|
|
250
|
+
const probe = await this.exec('command -v inotifywait >/dev/null 2>&1 && echo __KODE_INOTIFY_READY__', {
|
|
251
|
+
timeoutMs: 5000,
|
|
252
|
+
});
|
|
253
|
+
if (probe.code !== 0 || !probe.stdout.includes('__KODE_INOTIFY_READY__')) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
const sandbox = this.getOpenSandbox();
|
|
257
|
+
const abortController = new AbortController();
|
|
258
|
+
const nativeWatchCommand = buildNativeWatchCommand(paths);
|
|
259
|
+
let stdoutBuffer = '';
|
|
260
|
+
const streamTask = (async () => {
|
|
261
|
+
try {
|
|
262
|
+
for await (const event of sandbox.commands.runStream(nativeWatchCommand, { workingDirectory: this.workDir }, abortController.signal)) {
|
|
263
|
+
if (abortController.signal.aborted) {
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
if (event.type !== 'stdout' || typeof event.text !== 'string') {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
stdoutBuffer += event.text;
|
|
270
|
+
let lineBreak = stdoutBuffer.indexOf('\n');
|
|
271
|
+
while (lineBreak >= 0) {
|
|
272
|
+
const line = stdoutBuffer.slice(0, lineBreak).trim();
|
|
273
|
+
stdoutBuffer = stdoutBuffer.slice(lineBreak + 1);
|
|
274
|
+
if (line) {
|
|
275
|
+
listener({ path: line, mtimeMs: Date.now() });
|
|
276
|
+
}
|
|
277
|
+
lineBreak = stdoutBuffer.indexOf('\n');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const tail = stdoutBuffer.trim();
|
|
281
|
+
if (tail) {
|
|
282
|
+
listener({ path: tail, mtimeMs: Date.now() });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
if (!abortController.signal.aborted) {
|
|
287
|
+
logger_1.logger.warn('[OpenSandbox] native watch stream failed, fallback to polling.', error);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
const current = this.watchers.get(id);
|
|
292
|
+
if (!current || current.kind !== 'native' || current.abortController !== abortController) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
this.watchers.delete(id);
|
|
296
|
+
if (!abortController.signal.aborted) {
|
|
297
|
+
try {
|
|
298
|
+
await this.startPollingWatcher(id, paths, listener);
|
|
299
|
+
logger_1.logger.warn('[OpenSandbox] native watch stopped, switched to polling mode.');
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
logger_1.logger.warn('[OpenSandbox] failed to start polling fallback after native watch exit.', error);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
})();
|
|
307
|
+
const watcher = {
|
|
308
|
+
kind: 'native',
|
|
309
|
+
paths,
|
|
310
|
+
abortController,
|
|
311
|
+
streamTask,
|
|
312
|
+
};
|
|
313
|
+
this.watchers.set(id, watcher);
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
exports.OpenSandbox = OpenSandbox;
|
|
318
|
+
function quoteShell(value) {
|
|
319
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
320
|
+
}
|
|
321
|
+
function buildNativeWatchCommand(paths) {
|
|
322
|
+
const targets = paths.map((p) => quoteShell(p)).join(' ');
|
|
323
|
+
const script = `exec inotifywait -m -e modify,create,delete,move --format '%w%f' -- ${targets}`;
|
|
324
|
+
return `sh -lc ${quoteShell(script)}`;
|
|
325
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type OpenSandboxWatchMode = 'native' | 'polling' | 'off';
|
|
2
|
+
export interface OpenSandboxOptions {
|
|
3
|
+
kind: 'opensandbox';
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
endpoint?: string;
|
|
6
|
+
domain?: string;
|
|
7
|
+
protocol?: 'http' | 'https';
|
|
8
|
+
sandboxId?: string;
|
|
9
|
+
image?: string;
|
|
10
|
+
template?: string;
|
|
11
|
+
workDir?: string;
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
execTimeoutMs?: number;
|
|
14
|
+
requestTimeoutSeconds?: number;
|
|
15
|
+
useServerProxy?: boolean;
|
|
16
|
+
env?: Record<string, string>;
|
|
17
|
+
metadata?: Record<string, string>;
|
|
18
|
+
resource?: Record<string, string>;
|
|
19
|
+
networkPolicy?: Record<string, any>;
|
|
20
|
+
skipHealthCheck?: boolean;
|
|
21
|
+
readyTimeoutSeconds?: number;
|
|
22
|
+
healthCheckPollingInterval?: number;
|
|
23
|
+
watch?: {
|
|
24
|
+
mode?: OpenSandboxWatchMode;
|
|
25
|
+
pollIntervalMs?: number;
|
|
26
|
+
};
|
|
27
|
+
lifecycle?: {
|
|
28
|
+
disposeAction?: 'close' | 'kill';
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -207,6 +207,10 @@ class AnthropicProvider {
|
|
|
207
207
|
degraded = true;
|
|
208
208
|
return { type: 'text', text: utils_1.AUDIO_UNSUPPORTED_TEXT };
|
|
209
209
|
}
|
|
210
|
+
if (block.type === 'video') {
|
|
211
|
+
degraded = true;
|
|
212
|
+
return { type: 'text', text: utils_1.VIDEO_UNSUPPORTED_TEXT };
|
|
213
|
+
}
|
|
210
214
|
if (block.type === 'file') {
|
|
211
215
|
if (block.file_id) {
|
|
212
216
|
return {
|
|
@@ -214,6 +218,18 @@ class AnthropicProvider {
|
|
|
214
218
|
source: { type: 'file', file_id: block.file_id },
|
|
215
219
|
};
|
|
216
220
|
}
|
|
221
|
+
if (block.base64 && block.mime_type) {
|
|
222
|
+
return {
|
|
223
|
+
type: 'document',
|
|
224
|
+
source: { type: 'base64', media_type: block.mime_type, data: block.base64 },
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if (block.url) {
|
|
228
|
+
return {
|
|
229
|
+
type: 'document',
|
|
230
|
+
source: { type: 'url', url: block.url },
|
|
231
|
+
};
|
|
232
|
+
}
|
|
217
233
|
degraded = true;
|
|
218
234
|
return { type: 'text', text: utils_1.FILE_UNSUPPORTED_TEXT };
|
|
219
235
|
}
|
|
@@ -29,14 +29,26 @@ class GeminiProvider {
|
|
|
29
29
|
this.thinking = options?.thinking;
|
|
30
30
|
}
|
|
31
31
|
async uploadFile(input) {
|
|
32
|
-
|
|
32
|
+
// Gemini supports uploading audio, video, and file types
|
|
33
|
+
if (input.kind !== 'file' && input.kind !== 'audio' && input.kind !== 'video') {
|
|
33
34
|
return null;
|
|
34
35
|
}
|
|
35
36
|
const url = new URL(`${this.baseUrl}/files`);
|
|
36
37
|
url.searchParams.set('key', this.apiKey);
|
|
38
|
+
// Determine display name based on kind
|
|
39
|
+
let defaultFilename;
|
|
40
|
+
if (input.kind === 'audio') {
|
|
41
|
+
defaultFilename = 'audio.wav';
|
|
42
|
+
}
|
|
43
|
+
else if (input.kind === 'video') {
|
|
44
|
+
defaultFilename = 'video.mp4';
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
defaultFilename = 'file.pdf';
|
|
48
|
+
}
|
|
37
49
|
const body = {
|
|
38
50
|
file: {
|
|
39
|
-
display_name: input.filename ||
|
|
51
|
+
display_name: input.filename || defaultFilename,
|
|
40
52
|
mime_type: input.mimeType,
|
|
41
53
|
},
|
|
42
54
|
content: input.data.toString('base64'),
|
|
@@ -372,8 +384,24 @@ class GeminiProvider {
|
|
|
372
384
|
}
|
|
373
385
|
}
|
|
374
386
|
else if (block.type === 'audio') {
|
|
375
|
-
|
|
376
|
-
|
|
387
|
+
const audioPart = (0, utils_1.buildGeminiAudioPart)(block);
|
|
388
|
+
if (audioPart) {
|
|
389
|
+
parts.push(audioPart);
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
degraded = true;
|
|
393
|
+
parts.push({ text: utils_1.AUDIO_UNSUPPORTED_TEXT });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
else if (block.type === 'video') {
|
|
397
|
+
const videoPart = (0, utils_1.buildGeminiVideoPart)(block);
|
|
398
|
+
if (videoPart) {
|
|
399
|
+
parts.push(videoPart);
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
degraded = true;
|
|
403
|
+
parts.push({ text: utils_1.VIDEO_UNSUPPORTED_TEXT });
|
|
404
|
+
}
|
|
377
405
|
}
|
|
378
406
|
else if (block.type === 'file') {
|
|
379
407
|
const filePart = (0, utils_1.buildGeminiFilePart)(block);
|
|
@@ -458,7 +458,7 @@ class OpenAIProvider {
|
|
|
458
458
|
buildOpenAIMessages(messages, system, reasoningTransport = 'text') {
|
|
459
459
|
const output = [];
|
|
460
460
|
const toolCallNames = new Map();
|
|
461
|
-
const useStructuredContent = messages.some((msg) => (0, utils_1.getMessageBlocks)(msg).some((block) => block.type === 'image' || block.type === 'audio' || block.type === 'file'));
|
|
461
|
+
const useStructuredContent = messages.some((msg) => (0, utils_1.getMessageBlocks)(msg).some((block) => block.type === 'image' || block.type === 'audio' || block.type === 'video' || block.type === 'file'));
|
|
462
462
|
for (const msg of messages) {
|
|
463
463
|
for (const block of (0, utils_1.getMessageBlocks)(msg)) {
|
|
464
464
|
if (block.type === 'tool_use') {
|
|
@@ -585,8 +585,21 @@ class OpenAIProvider {
|
|
|
585
585
|
continue;
|
|
586
586
|
}
|
|
587
587
|
if (block.type === 'audio') {
|
|
588
|
+
// OpenAI Chat Completions API supports audio via input_audio (wav/mp3 base64 only)
|
|
589
|
+
const audioPart = (0, utils_1.buildOpenAIAudioPart)(block);
|
|
590
|
+
if (audioPart) {
|
|
591
|
+
contentParts.push(audioPart);
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
degraded = true;
|
|
595
|
+
appendText(utils_1.AUDIO_UNSUPPORTED_TEXT);
|
|
596
|
+
}
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
if (block.type === 'video') {
|
|
600
|
+
// OpenAI does not support video input
|
|
588
601
|
degraded = true;
|
|
589
|
-
appendText(utils_1.
|
|
602
|
+
appendText(utils_1.VIDEO_UNSUPPORTED_TEXT);
|
|
590
603
|
continue;
|
|
591
604
|
}
|
|
592
605
|
if (block.type === 'file') {
|
|
@@ -626,8 +639,19 @@ class OpenAIProvider {
|
|
|
626
639
|
parts.push({ type: textType, text: `<think>${block.reasoning}</think>` });
|
|
627
640
|
}
|
|
628
641
|
else if (block.type === 'audio') {
|
|
642
|
+
const audioPart = (0, utils_1.buildOpenAIAudioPart)(block);
|
|
643
|
+
if (audioPart) {
|
|
644
|
+
parts.push(audioPart);
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
degraded = true;
|
|
648
|
+
parts.push({ type: textType, text: utils_1.AUDIO_UNSUPPORTED_TEXT });
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
else if (block.type === 'video') {
|
|
652
|
+
// OpenAI Responses API does not support video input
|
|
629
653
|
degraded = true;
|
|
630
|
-
parts.push({ type: textType, text: utils_1.
|
|
654
|
+
parts.push({ type: textType, text: utils_1.VIDEO_UNSUPPORTED_TEXT });
|
|
631
655
|
}
|
|
632
656
|
else if (block.type === 'file') {
|
|
633
657
|
if (block.file_id) {
|
|
@@ -60,7 +60,7 @@ export interface UploadFileInput {
|
|
|
60
60
|
data: Buffer;
|
|
61
61
|
mimeType: string;
|
|
62
62
|
filename?: string;
|
|
63
|
-
kind: 'image' | 'file';
|
|
63
|
+
kind: 'image' | 'audio' | 'video' | 'file';
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
66
|
* File upload result.
|
|
@@ -104,6 +104,35 @@ export interface MultimodalOptions {
|
|
|
104
104
|
maxBase64Bytes?: number;
|
|
105
105
|
/** Allowed MIME types */
|
|
106
106
|
allowMimeTypes?: string[];
|
|
107
|
+
/** Audio-specific options */
|
|
108
|
+
audio?: {
|
|
109
|
+
/** Allowed audio MIME types */
|
|
110
|
+
allowMimeTypes?: string[];
|
|
111
|
+
/** Maximum audio duration in seconds */
|
|
112
|
+
maxDurationSec?: number;
|
|
113
|
+
/** Custom transcriber callback for providers without native audio support */
|
|
114
|
+
customTranscriber?: (audio: {
|
|
115
|
+
base64?: string;
|
|
116
|
+
url?: string;
|
|
117
|
+
mimeType?: string;
|
|
118
|
+
}) => Promise<string>;
|
|
119
|
+
};
|
|
120
|
+
/** Video-specific options */
|
|
121
|
+
video?: {
|
|
122
|
+
/** Allowed video MIME types */
|
|
123
|
+
allowMimeTypes?: string[];
|
|
124
|
+
/** Maximum video duration in seconds */
|
|
125
|
+
maxDurationSec?: number;
|
|
126
|
+
/** Custom frame extractor callback for providers without native video support */
|
|
127
|
+
customFrameExtractor?: (video: {
|
|
128
|
+
base64?: string;
|
|
129
|
+
url?: string;
|
|
130
|
+
mimeType?: string;
|
|
131
|
+
}) => Promise<Array<{
|
|
132
|
+
base64: string;
|
|
133
|
+
mimeType: string;
|
|
134
|
+
}>>;
|
|
135
|
+
};
|
|
107
136
|
}
|
|
108
137
|
/**
|
|
109
138
|
* Core model configuration.
|
|
@@ -185,12 +214,17 @@ export interface ProviderCapabilities {
|
|
|
185
214
|
supportsInterleavedThinking: boolean;
|
|
186
215
|
supportsImages: boolean;
|
|
187
216
|
supportsAudio: boolean;
|
|
217
|
+
supportsVideo: boolean;
|
|
218
|
+
supportsAudioOutput: boolean;
|
|
188
219
|
supportsFiles: boolean;
|
|
189
220
|
supportsTools: boolean;
|
|
190
221
|
supportsStreaming: boolean;
|
|
191
222
|
supportsCache: boolean;
|
|
192
223
|
maxContextTokens: number;
|
|
193
224
|
maxOutputTokens: number;
|
|
225
|
+
maxAudioDurationSec?: number;
|
|
226
|
+
maxVideoDurationSec?: number;
|
|
227
|
+
maxInlineDataBytes?: number;
|
|
194
228
|
minCacheableTokens?: number;
|
|
195
229
|
maxCacheBreakpoints?: number;
|
|
196
230
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared utilities for provider implementations.
|
|
3
3
|
*/
|
|
4
|
-
import { ContentBlock, Message, ImageContentBlock, FileContentBlock } from '../../core/types';
|
|
4
|
+
import { ContentBlock, Message, ImageContentBlock, FileContentBlock, AudioContentBlock, VideoContentBlock } from '../../core/types';
|
|
5
5
|
import { ReasoningTransport } from './types';
|
|
6
6
|
export declare function resolveProxyUrl(explicit?: string): string | undefined;
|
|
7
7
|
export declare function getProxyDispatcher(proxyUrl?: string): any | undefined;
|
|
@@ -18,6 +18,7 @@ export declare function safeJsonStringify(value: any): string;
|
|
|
18
18
|
export declare const FILE_UNSUPPORTED_TEXT = "[file unsupported] This model does not support PDF input. Please extract text or images first.";
|
|
19
19
|
export declare const IMAGE_UNSUPPORTED_TEXT = "[image unsupported] This model does not support image URLs; please provide base64 data if supported.";
|
|
20
20
|
export declare const AUDIO_UNSUPPORTED_TEXT = "[audio unsupported] This model does not support audio input; please provide a text transcript instead.";
|
|
21
|
+
export declare const VIDEO_UNSUPPORTED_TEXT = "[video unsupported] This model does not support video input; please provide text description or extracted frames instead.";
|
|
21
22
|
export declare function concatTextWithReasoning(blocks: ContentBlock[], reasoningTransport?: ReasoningTransport): string;
|
|
22
23
|
export declare function joinReasoningBlocks(blocks: ContentBlock[]): string;
|
|
23
24
|
/**
|
|
@@ -31,6 +32,23 @@ export declare function splitThinkText(text: string): ContentBlock[];
|
|
|
31
32
|
export declare function extractReasoningDetails(message: any): ContentBlock[];
|
|
32
33
|
export declare function buildGeminiImagePart(block: ImageContentBlock): any | null;
|
|
33
34
|
export declare function buildGeminiFilePart(block: FileContentBlock): any | null;
|
|
35
|
+
export declare function buildGeminiAudioPart(block: AudioContentBlock): any | null;
|
|
36
|
+
export declare function buildGeminiVideoPart(block: VideoContentBlock): any | null;
|
|
37
|
+
/** Supported OpenAI audio formats */
|
|
38
|
+
export declare const OPENAI_SUPPORTED_AUDIO_FORMATS: readonly ["wav", "mp3"];
|
|
39
|
+
export type OpenAIAudioFormat = (typeof OPENAI_SUPPORTED_AUDIO_FORMATS)[number];
|
|
40
|
+
/**
|
|
41
|
+
* Extract and validate OpenAI audio format from MIME type.
|
|
42
|
+
* OpenAI Chat Completions API only supports wav and mp3.
|
|
43
|
+
* @returns The audio format if supported, null otherwise
|
|
44
|
+
*/
|
|
45
|
+
export declare function extractOpenAIAudioFormat(mimeType?: string): OpenAIAudioFormat | null;
|
|
46
|
+
/**
|
|
47
|
+
* Build OpenAI input_audio content part from AudioContentBlock.
|
|
48
|
+
* OpenAI only supports base64 encoded audio (no URLs).
|
|
49
|
+
* @returns The OpenAI input_audio part or null if not supported
|
|
50
|
+
*/
|
|
51
|
+
export declare function buildOpenAIAudioPart(block: AudioContentBlock): any | null;
|
|
34
52
|
export declare function sanitizeGeminiSchema(schema: any): any;
|
|
35
53
|
export declare function hasAnthropicFileBlocks(messages: Message[]): boolean;
|
|
36
54
|
export declare function mergeAnthropicBetaHeader(existing: string | undefined, entries: string[]): string | undefined;
|