@link-assistant/agent 0.16.9 → 0.16.11
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/package.json +1 -1
- package/src/index.js +31 -0
- package/src/provider/provider.ts +133 -5
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -54,6 +54,37 @@ try {
|
|
|
54
54
|
// Track if any errors occurred during execution
|
|
55
55
|
let hasError = false;
|
|
56
56
|
|
|
57
|
+
// Intercept stderr writes to ensure ALL output is JSON (#200)
|
|
58
|
+
// Bun runtime may print errors in plain text format (e.g., stack traces with source code)
|
|
59
|
+
// This interceptor wraps any non-JSON stderr output in a JSON envelope
|
|
60
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
61
|
+
process.stderr.write = function (chunk, encoding, callback) {
|
|
62
|
+
const str = typeof chunk === 'string' ? chunk : chunk.toString();
|
|
63
|
+
const trimmed = str.trim();
|
|
64
|
+
if (!trimmed) {
|
|
65
|
+
return originalStderrWrite(chunk, encoding, callback);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check if it's already valid JSON
|
|
69
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
70
|
+
try {
|
|
71
|
+
JSON.parse(trimmed);
|
|
72
|
+
// Already JSON, pass through
|
|
73
|
+
return originalStderrWrite(chunk, encoding, callback);
|
|
74
|
+
} catch (_e) {
|
|
75
|
+
// Not valid JSON, wrap it
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Wrap non-JSON stderr output in JSON envelope
|
|
80
|
+
const wrapped = `${JSON.stringify({
|
|
81
|
+
type: 'error',
|
|
82
|
+
errorType: 'RuntimeError',
|
|
83
|
+
message: trimmed,
|
|
84
|
+
})}\n`;
|
|
85
|
+
return originalStderrWrite(wrapped, encoding, callback);
|
|
86
|
+
};
|
|
87
|
+
|
|
57
88
|
// Install global error handlers to ensure non-zero exit codes
|
|
58
89
|
// All output is JSON to ensure machine-parsability (#200)
|
|
59
90
|
process.on('uncaughtException', (error) => {
|
package/src/provider/provider.ts
CHANGED
|
@@ -1289,6 +1289,85 @@ export namespace Provider {
|
|
|
1289
1289
|
responseHeaders: Object.fromEntries(response.headers.entries()),
|
|
1290
1290
|
}));
|
|
1291
1291
|
|
|
1292
|
+
// Log response body for debugging provider failures
|
|
1293
|
+
// For streaming responses (SSE/event-stream), tee() the stream so the AI SDK
|
|
1294
|
+
// still receives the full stream while we asynchronously log a preview.
|
|
1295
|
+
// For non-streaming responses, buffer the body and reconstruct the Response.
|
|
1296
|
+
// See: https://github.com/link-assistant/agent/issues/204
|
|
1297
|
+
const responseBodyMaxChars = 4000;
|
|
1298
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
1299
|
+
const isStreaming =
|
|
1300
|
+
contentType.includes('event-stream') ||
|
|
1301
|
+
contentType.includes('octet-stream');
|
|
1302
|
+
|
|
1303
|
+
if (response.body) {
|
|
1304
|
+
if (isStreaming) {
|
|
1305
|
+
// Tee the stream: one copy for AI SDK, one for logging
|
|
1306
|
+
const [sdkStream, logStream] = response.body.tee();
|
|
1307
|
+
|
|
1308
|
+
// Consume log stream asynchronously (does not block SDK)
|
|
1309
|
+
(async () => {
|
|
1310
|
+
try {
|
|
1311
|
+
const reader = logStream.getReader();
|
|
1312
|
+
const decoder = new TextDecoder();
|
|
1313
|
+
let bodyPreview = '';
|
|
1314
|
+
let truncated = false;
|
|
1315
|
+
while (true) {
|
|
1316
|
+
const { done, value } = await reader.read();
|
|
1317
|
+
if (done) break;
|
|
1318
|
+
if (!truncated) {
|
|
1319
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
1320
|
+
bodyPreview += chunk;
|
|
1321
|
+
if (bodyPreview.length > responseBodyMaxChars) {
|
|
1322
|
+
bodyPreview = bodyPreview.slice(
|
|
1323
|
+
0,
|
|
1324
|
+
responseBodyMaxChars
|
|
1325
|
+
);
|
|
1326
|
+
truncated = true;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
log.info(() => ({
|
|
1331
|
+
message: 'HTTP response body (stream)',
|
|
1332
|
+
providerID: provider.id,
|
|
1333
|
+
url,
|
|
1334
|
+
bodyPreview: truncated
|
|
1335
|
+
? bodyPreview + `... [truncated]`
|
|
1336
|
+
: bodyPreview,
|
|
1337
|
+
}));
|
|
1338
|
+
} catch {
|
|
1339
|
+
// Ignore logging errors — do not affect the SDK stream
|
|
1340
|
+
}
|
|
1341
|
+
})();
|
|
1342
|
+
|
|
1343
|
+
// Return response with the SDK's copy of the stream
|
|
1344
|
+
return new Response(sdkStream, {
|
|
1345
|
+
status: response.status,
|
|
1346
|
+
statusText: response.statusText,
|
|
1347
|
+
headers: response.headers,
|
|
1348
|
+
});
|
|
1349
|
+
} else {
|
|
1350
|
+
// Non-streaming: buffer body, log it, reconstruct Response
|
|
1351
|
+
const bodyText = await response.text();
|
|
1352
|
+
const bodyPreview =
|
|
1353
|
+
bodyText.length > responseBodyMaxChars
|
|
1354
|
+
? bodyText.slice(0, responseBodyMaxChars) +
|
|
1355
|
+
`... [truncated, total ${bodyText.length} chars]`
|
|
1356
|
+
: bodyText;
|
|
1357
|
+
log.info(() => ({
|
|
1358
|
+
message: 'HTTP response body',
|
|
1359
|
+
providerID: provider.id,
|
|
1360
|
+
url,
|
|
1361
|
+
bodyPreview,
|
|
1362
|
+
}));
|
|
1363
|
+
return new Response(bodyText, {
|
|
1364
|
+
status: response.status,
|
|
1365
|
+
statusText: response.statusText,
|
|
1366
|
+
headers: response.headers,
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1292
1371
|
return response;
|
|
1293
1372
|
} catch (error) {
|
|
1294
1373
|
const durationMs = Date.now() - startMs;
|
|
@@ -1416,21 +1495,70 @@ export namespace Provider {
|
|
|
1416
1495
|
providerID === 'link-assistant' || providerID === 'link-assistant/cache';
|
|
1417
1496
|
|
|
1418
1497
|
// For synthetic providers, we don't need model info from the database
|
|
1419
|
-
|
|
1498
|
+
let info = isSyntheticProvider ? null : provider.info.models[modelID];
|
|
1420
1499
|
if (!isSyntheticProvider && !info) {
|
|
1500
|
+
// Model not in provider's known list - try refreshing the cache first (#200)
|
|
1501
|
+
// This handles stale bundled data or expired cache (1-hour TTL)
|
|
1502
|
+
log.info(() => ({
|
|
1503
|
+
message: 'model not in catalog - refreshing models cache',
|
|
1504
|
+
providerID,
|
|
1505
|
+
modelID,
|
|
1506
|
+
}));
|
|
1507
|
+
try {
|
|
1508
|
+
await ModelsDev.refresh();
|
|
1509
|
+
const freshDB = await ModelsDev.get();
|
|
1510
|
+
const freshProvider = freshDB[providerID];
|
|
1511
|
+
if (freshProvider?.models[modelID]) {
|
|
1512
|
+
// Model found after refresh - update provider info and use it
|
|
1513
|
+
provider.info.models[modelID] = freshProvider.models[modelID];
|
|
1514
|
+
info = freshProvider.models[modelID];
|
|
1515
|
+
log.info(() => ({
|
|
1516
|
+
message: 'model found after cache refresh',
|
|
1517
|
+
providerID,
|
|
1518
|
+
modelID,
|
|
1519
|
+
}));
|
|
1520
|
+
}
|
|
1521
|
+
} catch (refreshError) {
|
|
1522
|
+
log.warn(() => ({
|
|
1523
|
+
message: 'cache refresh failed',
|
|
1524
|
+
error:
|
|
1525
|
+
refreshError instanceof Error
|
|
1526
|
+
? refreshError.message
|
|
1527
|
+
: String(refreshError),
|
|
1528
|
+
}));
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
if (!isSyntheticProvider && !info) {
|
|
1533
|
+
// Still not found after refresh - create fallback info and try anyway
|
|
1534
|
+
// Provider may support unlisted models
|
|
1421
1535
|
const availableInProvider = Object.keys(provider.info.models).slice(
|
|
1422
1536
|
0,
|
|
1423
1537
|
10
|
|
1424
1538
|
);
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1539
|
+
log.warn(() => ({
|
|
1540
|
+
message:
|
|
1541
|
+
'model not in provider catalog after refresh - attempting anyway (may be unlisted)',
|
|
1428
1542
|
providerID,
|
|
1429
1543
|
modelID,
|
|
1430
1544
|
availableModels: availableInProvider,
|
|
1431
1545
|
totalModels: Object.keys(provider.info.models).length,
|
|
1432
1546
|
}));
|
|
1433
|
-
|
|
1547
|
+
|
|
1548
|
+
// Create a minimal fallback model info so SDK loading can proceed
|
|
1549
|
+
// Use sensible defaults - the provider will reject if the model truly doesn't exist
|
|
1550
|
+
info = {
|
|
1551
|
+
id: modelID,
|
|
1552
|
+
name: modelID,
|
|
1553
|
+
release_date: '',
|
|
1554
|
+
attachment: false,
|
|
1555
|
+
reasoning: false,
|
|
1556
|
+
temperature: true,
|
|
1557
|
+
tool_call: true,
|
|
1558
|
+
cost: { input: 0, output: 0 },
|
|
1559
|
+
limit: { context: 128000, output: 16384 },
|
|
1560
|
+
options: {},
|
|
1561
|
+
} as ModelsDev.Model;
|
|
1434
1562
|
}
|
|
1435
1563
|
|
|
1436
1564
|
try {
|