twinclaw 1.4.0 → 1.4.1

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.
@@ -297,6 +297,97 @@ export const STATIC_MODEL_CATALOG = [
297
297
  pricing: 'Free tier available',
298
298
  description: 'Fast inference with Groq'
299
299
  },
300
+ // Alias entries for twinclaw.json model IDs that differ from catalog IDs
301
+ {
302
+ id: 'groq-qwen-qwen3-32b',
303
+ name: 'Qwen 3 32B (Groq)',
304
+ provider: 'groq',
305
+ model: 'qwen/qwen3-32b',
306
+ contextLength: 32768,
307
+ supportsStreaming: true,
308
+ pricing: 'Free tier available',
309
+ description: 'Fast inference with Groq'
310
+ },
311
+ {
312
+ id: 'groq-moonshotai-kimi-k2-instruct-0905',
313
+ name: 'Kimi K2 Instruct (Groq)',
314
+ provider: 'groq',
315
+ model: 'moonshotai/kimi-k2-instruct-0905',
316
+ contextLength: 32768,
317
+ supportsStreaming: true,
318
+ pricing: 'Free tier available',
319
+ description: 'Fast inference with Groq'
320
+ },
321
+ {
322
+ id: 'openrouter-z-ai-glm-4-5-air-free',
323
+ name: 'GLM-4.5 Air (Free)',
324
+ provider: 'openrouter',
325
+ model: 'z-ai/glm-4.5-air:free',
326
+ contextLength: 32000,
327
+ supportsStreaming: true,
328
+ pricing: 'Free',
329
+ description: 'Free model via OpenRouter'
330
+ },
331
+ {
332
+ id: 'openrouter-stepfun-step-3-5-flash-free',
333
+ name: 'StepFun Flash (Free)',
334
+ provider: 'openrouter',
335
+ model: 'stepfun/step-3.5-flash:free',
336
+ contextLength: 32000,
337
+ supportsStreaming: true,
338
+ pricing: 'Free',
339
+ description: 'Free model via OpenRouter'
340
+ },
341
+ {
342
+ id: 'openrouter-qwen-qwen3-coder-free',
343
+ name: 'Qwen3 Coder (Free)',
344
+ provider: 'openrouter',
345
+ model: 'qwen/qwen3-coder:free',
346
+ contextLength: 32000,
347
+ supportsStreaming: true,
348
+ pricing: 'Free',
349
+ description: 'Free model via OpenRouter'
350
+ },
351
+ {
352
+ id: 'openrouter-arcee-ai-trinity-mini-free',
353
+ name: 'Trinity Mini (Free)',
354
+ provider: 'openrouter',
355
+ model: 'arcee-ai/trinity-mini:free',
356
+ contextLength: 32000,
357
+ supportsStreaming: true,
358
+ pricing: 'Free',
359
+ description: 'Free model via OpenRouter'
360
+ },
361
+ {
362
+ id: 'openrouter-arcee-ai-trinity-large-preview-free',
363
+ name: 'Trinity Large Preview (Free)',
364
+ provider: 'openrouter',
365
+ model: 'arcee-ai/trinity-large-preview:free',
366
+ contextLength: 32000,
367
+ supportsStreaming: true,
368
+ pricing: 'Free',
369
+ description: 'Free model via OpenRouter'
370
+ },
371
+ {
372
+ id: 'copilot-gpt-4o',
373
+ name: 'GPT-4o (Copilot)',
374
+ provider: 'copilot',
375
+ model: 'gpt-4o',
376
+ contextLength: 128000,
377
+ supportsStreaming: true,
378
+ pricing: 'Included with Copilot',
379
+ description: 'GitHub Copilot model'
380
+ },
381
+ {
382
+ id: 'copilot-gemini-flash',
383
+ name: 'Gemini Flash (Copilot)',
384
+ provider: 'copilot',
385
+ model: 'gemini-2.0-flash',
386
+ contextLength: 1000000,
387
+ supportsStreaming: true,
388
+ pricing: 'Included with Copilot',
389
+ description: 'GitHub Copilot model'
390
+ },
300
391
  // OpenRouter Free Models
301
392
  {
302
393
  id: 'openrouter-stepfun-flash',
@@ -69,10 +69,14 @@ class LearningSystem {
69
69
  return entry;
70
70
  }
71
71
  #findSimilar(description) {
72
+ if (!description)
73
+ return undefined;
72
74
  const searchTerms = description.toLowerCase().split(/\s+/);
73
75
  let bestMatch;
74
76
  let bestScore = 0;
75
77
  for (const memory of this.#memories.values()) {
78
+ if (!memory.description)
79
+ continue;
76
80
  const memTerms = memory.description.toLowerCase().split(/\s+/);
77
81
  let score = 0;
78
82
  for (const term of searchTerms) {
@@ -868,6 +868,12 @@ export class ModelRouter {
868
868
  return 'copilot';
869
869
  if (url.includes('api.github.com'))
870
870
  return 'github';
871
+ if (url.includes('groq.com'))
872
+ return 'groq';
873
+ if (url.includes('api.openai.com'))
874
+ return 'openai';
875
+ if (url.includes('api.anthropic.com'))
876
+ return 'anthropic';
871
877
  return 'unknown';
872
878
  }
873
879
  getModelCooldownState(modelId) {
@@ -1043,11 +1049,71 @@ export class ModelRouter {
1043
1049
  return 0;
1044
1050
  });
1045
1051
  for (const def of sortedDefinitions) {
1052
+ // Resolve missing model/apiKeyEnvName from the static catalog and provider info
1053
+ let resolvedModel = def.model;
1054
+ let resolvedApiKeyEnvName = def.apiKeyEnvName;
1055
+ if (!resolvedModel || !resolvedApiKeyEnvName) {
1056
+ // Try to find the model in the static catalog by ID
1057
+ const catalogEntry = STATIC_MODEL_CATALOG.find(m => m.id === def.id);
1058
+ if (catalogEntry) {
1059
+ if (!resolvedModel)
1060
+ resolvedModel = catalogEntry.model;
1061
+ if (!resolvedApiKeyEnvName) {
1062
+ const providerInfo = PROVIDER_INFO[catalogEntry.provider];
1063
+ resolvedApiKeyEnvName = providerInfo?.apiKeyEnvName ?? '';
1064
+ }
1065
+ }
1066
+ }
1067
+ // If still missing, infer provider from baseURL and resolve apiKeyEnvName
1068
+ if (!resolvedApiKeyEnvName) {
1069
+ const url = def.baseURL.toLowerCase();
1070
+ if (url.includes('groq.com'))
1071
+ resolvedApiKeyEnvName = 'GROQ_API_KEY';
1072
+ else if (url.includes('openrouter.ai'))
1073
+ resolvedApiKeyEnvName = 'OPENROUTER_API_KEY';
1074
+ else if (url.includes('modal.direct'))
1075
+ resolvedApiKeyEnvName = 'MODAL_API_KEY';
1076
+ else if (url.includes('api.openai.com'))
1077
+ resolvedApiKeyEnvName = 'OPENAI_API_KEY';
1078
+ else if (url.includes('generativelanguage.googleapis.com'))
1079
+ resolvedApiKeyEnvName = 'GEMINI_API_KEY';
1080
+ else if (url.includes('githubcopilot.com'))
1081
+ resolvedApiKeyEnvName = 'GITHUB_TOKEN';
1082
+ else if (url.includes('api.anthropic.com'))
1083
+ resolvedApiKeyEnvName = 'ANTHROPIC_API_KEY';
1084
+ }
1085
+ // If model is still missing, try to derive from the ID (e.g., groq-qwen-qwen3-32b -> qwen/qwen3-32b)
1086
+ if (!resolvedModel) {
1087
+ // Try a fuzzy match against catalog entries by normalizing IDs
1088
+ const normalizedDefId = def.id.toLowerCase();
1089
+ const fuzzyMatch = STATIC_MODEL_CATALOG.find(m => {
1090
+ const normalizedCatalogId = m.id.toLowerCase();
1091
+ return normalizedDefId === normalizedCatalogId ||
1092
+ normalizedDefId.includes(normalizedCatalogId) ||
1093
+ normalizedCatalogId.includes(normalizedDefId);
1094
+ });
1095
+ if (fuzzyMatch) {
1096
+ resolvedModel = fuzzyMatch.model;
1097
+ }
1098
+ else {
1099
+ // Last resort: strip provider prefix and use the rest as model name
1100
+ const parts = def.id.split('-');
1101
+ if (parts.length > 1) {
1102
+ // For IDs like 'groq-qwen-qwen3-32b', try 'qwen/qwen3-32b'
1103
+ // For IDs like 'openrouter-z-ai-glm-4-5-air-free', try 'z-ai/glm-4-5-air:free'
1104
+ resolvedModel = def.id; // Use ID as-is — the API will reject if wrong, triggering fallback
1105
+ }
1106
+ }
1107
+ }
1108
+ if (!resolvedModel) {
1109
+ logThought(`[Router] Skipping definition '${def.id}': unable to resolve model name.`);
1110
+ continue;
1111
+ }
1046
1112
  configModels.push({
1047
1113
  id: def.id,
1048
- model: def.model,
1114
+ model: resolvedModel,
1049
1115
  baseURL: def.baseURL,
1050
- apiKeyEnvName: def.apiKeyEnvName,
1116
+ apiKeyEnvName: resolvedApiKeyEnvName || '',
1051
1117
  });
1052
1118
  }
1053
1119
  // Even with definitions, add fallback providers in case primary fails
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twinclaw",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Eagle-eyed agentic AI gateway with multi-modal hooks and proactive memory.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {