@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/agent",
3
- "version": "0.16.5",
3
+ "version": "0.16.8",
4
4
  "description": "A minimal, public domain AI CLI agent compatible with OpenCode's JSON interface. Bun-only runtime.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -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
- throw new ModelNotFoundError({ providerID, modelID });
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}`;
@@ -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 =
@@ -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
- hint: 'This usually indicates the provider failed to process the request. Check provider status, model availability, and API keys.',
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 }));