@link-assistant/agent 0.16.5 → 0.16.8
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/provider/provider.ts +123 -3
- package/src/session/processor.ts +26 -0
- package/src/session/prompt.ts +18 -4
package/package.json
CHANGED
package/src/provider/provider.ts
CHANGED
|
@@ -687,7 +687,7 @@ export namespace Provider {
|
|
|
687
687
|
},
|
|
688
688
|
};
|
|
689
689
|
|
|
690
|
-
const state = Instance.state(async () => {
|
|
690
|
+
export const state = Instance.state(async () => {
|
|
691
691
|
using _ = log.time('state');
|
|
692
692
|
const config = await Config.get();
|
|
693
693
|
const database = await ModelsDev.get();
|
|
@@ -1201,6 +1201,113 @@ export namespace Provider {
|
|
|
1201
1201
|
sessionID: provider.id,
|
|
1202
1202
|
});
|
|
1203
1203
|
|
|
1204
|
+
// Wrap fetch with verbose HTTP logging when --verbose is enabled
|
|
1205
|
+
// This logs raw HTTP request/response details for debugging provider issues
|
|
1206
|
+
// See: https://github.com/link-assistant/agent/issues/200
|
|
1207
|
+
if (Flag.OPENCODE_VERBOSE) {
|
|
1208
|
+
const innerFetch = options['fetch'];
|
|
1209
|
+
options['fetch'] = async (
|
|
1210
|
+
input: RequestInfo | URL,
|
|
1211
|
+
init?: RequestInit
|
|
1212
|
+
): Promise<Response> => {
|
|
1213
|
+
const url =
|
|
1214
|
+
typeof input === 'string'
|
|
1215
|
+
? input
|
|
1216
|
+
: input instanceof URL
|
|
1217
|
+
? input.toString()
|
|
1218
|
+
: input.url;
|
|
1219
|
+
const method = init?.method ?? 'GET';
|
|
1220
|
+
|
|
1221
|
+
// Sanitize headers - mask authorization values
|
|
1222
|
+
const sanitizedHeaders: Record<string, string> = {};
|
|
1223
|
+
const rawHeaders = init?.headers;
|
|
1224
|
+
if (rawHeaders) {
|
|
1225
|
+
const entries =
|
|
1226
|
+
rawHeaders instanceof Headers
|
|
1227
|
+
? Array.from(rawHeaders.entries())
|
|
1228
|
+
: Array.isArray(rawHeaders)
|
|
1229
|
+
? rawHeaders
|
|
1230
|
+
: Object.entries(rawHeaders);
|
|
1231
|
+
for (const [key, value] of entries) {
|
|
1232
|
+
const lower = key.toLowerCase();
|
|
1233
|
+
if (
|
|
1234
|
+
lower === 'authorization' ||
|
|
1235
|
+
lower === 'x-api-key' ||
|
|
1236
|
+
lower === 'api-key'
|
|
1237
|
+
) {
|
|
1238
|
+
sanitizedHeaders[key] =
|
|
1239
|
+
typeof value === 'string' && value.length > 8
|
|
1240
|
+
? value.slice(0, 4) + '...' + value.slice(-4)
|
|
1241
|
+
: '[REDACTED]';
|
|
1242
|
+
} else {
|
|
1243
|
+
sanitizedHeaders[key] = String(value);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// Log request body preview (truncated)
|
|
1249
|
+
let bodyPreview: string | undefined;
|
|
1250
|
+
if (init?.body) {
|
|
1251
|
+
const bodyStr =
|
|
1252
|
+
typeof init.body === 'string'
|
|
1253
|
+
? init.body
|
|
1254
|
+
: init.body instanceof ArrayBuffer ||
|
|
1255
|
+
init.body instanceof Uint8Array
|
|
1256
|
+
? `[binary ${(init.body as ArrayBuffer).byteLength ?? (init.body as Uint8Array).length} bytes]`
|
|
1257
|
+
: undefined;
|
|
1258
|
+
if (bodyStr && typeof bodyStr === 'string') {
|
|
1259
|
+
bodyPreview =
|
|
1260
|
+
bodyStr.length > 2000
|
|
1261
|
+
? bodyStr.slice(0, 2000) +
|
|
1262
|
+
`... [truncated, total ${bodyStr.length} chars]`
|
|
1263
|
+
: bodyStr;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
log.info(() => ({
|
|
1268
|
+
message: 'HTTP request',
|
|
1269
|
+
providerID: provider.id,
|
|
1270
|
+
method,
|
|
1271
|
+
url,
|
|
1272
|
+
headers: sanitizedHeaders,
|
|
1273
|
+
bodyPreview,
|
|
1274
|
+
}));
|
|
1275
|
+
|
|
1276
|
+
const startMs = Date.now();
|
|
1277
|
+
try {
|
|
1278
|
+
const response = await innerFetch(input, init);
|
|
1279
|
+
const durationMs = Date.now() - startMs;
|
|
1280
|
+
|
|
1281
|
+
log.info(() => ({
|
|
1282
|
+
message: 'HTTP response',
|
|
1283
|
+
providerID: provider.id,
|
|
1284
|
+
method,
|
|
1285
|
+
url,
|
|
1286
|
+
status: response.status,
|
|
1287
|
+
statusText: response.statusText,
|
|
1288
|
+
durationMs,
|
|
1289
|
+
responseHeaders: Object.fromEntries(response.headers.entries()),
|
|
1290
|
+
}));
|
|
1291
|
+
|
|
1292
|
+
return response;
|
|
1293
|
+
} catch (error) {
|
|
1294
|
+
const durationMs = Date.now() - startMs;
|
|
1295
|
+
log.error(() => ({
|
|
1296
|
+
message: 'HTTP request failed',
|
|
1297
|
+
providerID: provider.id,
|
|
1298
|
+
method,
|
|
1299
|
+
url,
|
|
1300
|
+
durationMs,
|
|
1301
|
+
error:
|
|
1302
|
+
error instanceof Error
|
|
1303
|
+
? { name: error.name, message: error.message }
|
|
1304
|
+
: String(error),
|
|
1305
|
+
}));
|
|
1306
|
+
throw error;
|
|
1307
|
+
}
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1204
1311
|
// Check if we have a bundled provider first - this avoids runtime package installation
|
|
1205
1312
|
// which can hang or timeout due to known Bun issues
|
|
1206
1313
|
// @see https://github.com/link-assistant/agent/issues/173
|
|
@@ -1310,8 +1417,21 @@ export namespace Provider {
|
|
|
1310
1417
|
|
|
1311
1418
|
// For synthetic providers, we don't need model info from the database
|
|
1312
1419
|
const info = isSyntheticProvider ? null : provider.info.models[modelID];
|
|
1313
|
-
if (!isSyntheticProvider && !info)
|
|
1314
|
-
|
|
1420
|
+
if (!isSyntheticProvider && !info) {
|
|
1421
|
+
const availableInProvider = Object.keys(provider.info.models).slice(
|
|
1422
|
+
0,
|
|
1423
|
+
10
|
|
1424
|
+
);
|
|
1425
|
+
const suggestion = `Model "${modelID}" not found in provider "${providerID}". Available models: ${availableInProvider.join(', ')}${Object.keys(provider.info.models).length > 10 ? ` (and ${Object.keys(provider.info.models).length - 10} more)` : ''}.`;
|
|
1426
|
+
log.error(() => ({
|
|
1427
|
+
message: 'model not found in provider',
|
|
1428
|
+
providerID,
|
|
1429
|
+
modelID,
|
|
1430
|
+
availableModels: availableInProvider,
|
|
1431
|
+
totalModels: Object.keys(provider.info.models).length,
|
|
1432
|
+
}));
|
|
1433
|
+
throw new ModelNotFoundError({ providerID, modelID, suggestion });
|
|
1434
|
+
}
|
|
1315
1435
|
|
|
1316
1436
|
try {
|
|
1317
1437
|
const keyReal = `${providerID}/${modelID}`;
|
package/src/session/processor.ts
CHANGED
|
@@ -298,6 +298,32 @@ export namespace SessionProcessor {
|
|
|
298
298
|
input.assistantMessage.cost += usage.cost;
|
|
299
299
|
input.assistantMessage.tokens = usage.tokens;
|
|
300
300
|
|
|
301
|
+
// Log warning when provider returns zero tokens (#198)
|
|
302
|
+
if (
|
|
303
|
+
usage.tokens.input === 0 &&
|
|
304
|
+
usage.tokens.output === 0 &&
|
|
305
|
+
usage.tokens.reasoning === 0 &&
|
|
306
|
+
finishReason === 'unknown'
|
|
307
|
+
) {
|
|
308
|
+
log.warn(() => ({
|
|
309
|
+
message:
|
|
310
|
+
'provider returned zero tokens with unknown finish reason at step level',
|
|
311
|
+
providerID: input.providerID,
|
|
312
|
+
requestedModelID: input.model.id,
|
|
313
|
+
respondedModelID:
|
|
314
|
+
(value as any).response?.modelId ?? 'none',
|
|
315
|
+
rawFinishReason: String(
|
|
316
|
+
value.finishReason ?? 'undefined'
|
|
317
|
+
),
|
|
318
|
+
rawUsage: JSON.stringify(value.usage ?? null),
|
|
319
|
+
providerMetadata: JSON.stringify(
|
|
320
|
+
value.providerMetadata ?? null
|
|
321
|
+
),
|
|
322
|
+
issue:
|
|
323
|
+
'https://github.com/link-assistant/agent/issues/198',
|
|
324
|
+
}));
|
|
325
|
+
}
|
|
326
|
+
|
|
301
327
|
// Build model info if --output-response-model flag is enabled
|
|
302
328
|
// @see https://github.com/link-assistant/agent/issues/179
|
|
303
329
|
const modelInfo: MessageV2.ModelInfo | undefined =
|
package/src/session/prompt.ts
CHANGED
|
@@ -297,17 +297,29 @@ export namespace SessionPrompt {
|
|
|
297
297
|
tokens.output === 0 &&
|
|
298
298
|
tokens.reasoning === 0
|
|
299
299
|
) {
|
|
300
|
+
const errorMessage =
|
|
301
|
+
`Provider returned zero tokens with unknown finish reason. ` +
|
|
302
|
+
`Requested model: ${lastAssistant.model?.requestedModelID ?? 'unknown'} ` +
|
|
303
|
+
`(provider: ${lastAssistant.model?.providerID ?? 'unknown'}). ` +
|
|
304
|
+
`Responded model: ${lastAssistant.model?.respondedModelID ?? 'unknown'}. ` +
|
|
305
|
+
`This usually indicates the provider failed to process the request. ` +
|
|
306
|
+
`Check provider status, model availability, and API keys.`;
|
|
300
307
|
log.error(() => ({
|
|
301
|
-
message:
|
|
302
|
-
'provider returned zero tokens with unknown finish reason - possible API failure',
|
|
308
|
+
message: errorMessage,
|
|
303
309
|
sessionID,
|
|
304
310
|
finishReason: lastAssistant.finish,
|
|
305
311
|
tokens,
|
|
306
312
|
cost: lastAssistant.cost,
|
|
307
313
|
model: lastAssistant.model,
|
|
308
|
-
|
|
309
|
-
issue: 'https://github.com/link-assistant/agent/issues/196',
|
|
314
|
+
issue: 'https://github.com/link-assistant/agent/issues/198',
|
|
310
315
|
}));
|
|
316
|
+
// Publish error event so JSON standard output includes it (#198)
|
|
317
|
+
Bus.publish(Session.Event.Error, {
|
|
318
|
+
sessionID,
|
|
319
|
+
error: new NamedError.Unknown({
|
|
320
|
+
message: errorMessage,
|
|
321
|
+
}).toObject(),
|
|
322
|
+
});
|
|
311
323
|
break;
|
|
312
324
|
}
|
|
313
325
|
|
|
@@ -637,6 +649,8 @@ export namespace SessionPrompt {
|
|
|
637
649
|
log.info(() => ({
|
|
638
650
|
message: 'Model',
|
|
639
651
|
model: `${model.providerID}/${model.modelID}`,
|
|
652
|
+
apiModelID: model.info?.id ?? model.modelID,
|
|
653
|
+
npm: model.npm,
|
|
640
654
|
}));
|
|
641
655
|
log.info(() => ({ message: 'Session ID', sessionID }));
|
|
642
656
|
log.info(() => ({ message: 'Agent', agent: agent.name }));
|