ado-sync 0.1.55 → 0.1.57

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.
Files changed (40) hide show
  1. package/README.md +39 -0
  2. package/dist/__tests__/regressions.test.d.ts +1 -0
  3. package/dist/__tests__/regressions.test.js +140 -0
  4. package/dist/__tests__/regressions.test.js.map +1 -0
  5. package/dist/ai/generate-spec.d.ts +11 -9
  6. package/dist/ai/generate-spec.js +116 -96
  7. package/dist/ai/generate-spec.js.map +1 -1
  8. package/dist/ai/summarizer.d.ts +5 -5
  9. package/dist/ai/summarizer.js +243 -135
  10. package/dist/ai/summarizer.js.map +1 -1
  11. package/dist/azure/test-cases.d.ts +10 -1
  12. package/dist/azure/test-cases.js +61 -25
  13. package/dist/azure/test-cases.js.map +1 -1
  14. package/dist/cli.js +1 -1
  15. package/dist/cli.js.map +1 -1
  16. package/dist/config.js +7 -2
  17. package/dist/config.js.map +1 -1
  18. package/dist/issues/ado-bugs.js +7 -3
  19. package/dist/issues/ado-bugs.js.map +1 -1
  20. package/dist/mcp-server.js +51 -27
  21. package/dist/mcp-server.js.map +1 -1
  22. package/dist/parsers/javascript.js +2 -1
  23. package/dist/parsers/javascript.js.map +1 -1
  24. package/dist/sync/cache.js +3 -1
  25. package/dist/sync/cache.js.map +1 -1
  26. package/dist/sync/engine.d.ts +12 -1
  27. package/dist/sync/engine.js +226 -165
  28. package/dist/sync/engine.js.map +1 -1
  29. package/dist/sync/publish-results.js +64 -21
  30. package/dist/sync/publish-results.js.map +1 -1
  31. package/dist/sync/writeback.js +22 -5
  32. package/dist/sync/writeback.js.map +1 -1
  33. package/dist/types.d.ts +3 -3
  34. package/docs/advanced.md +17 -18
  35. package/docs/cli.md +74 -4
  36. package/docs/publish-test-results.md +3 -3
  37. package/mkdocs.yml +39 -0
  38. package/package.json +23 -14
  39. package/requirements-docs.txt +4 -0
  40. package/scripts/build_site.sh +6 -0
@@ -14,7 +14,7 @@
14
14
  * dependencies, works fully offline.
15
15
  *
16
16
  * ollama — local LLM via Ollama REST API (http://localhost:11434).
17
- * Model suggestion: qwen2.5-coder:7b.
17
+ * Model suggestion: gemma-4-e4b-it.
18
18
  *
19
19
  * openai — OpenAI Chat Completions API (requires API key).
20
20
  *
@@ -23,7 +23,7 @@
23
23
  * Usage (engine.ts / CLI):
24
24
  * const { title, description, steps } = await summarizeTest(test, localType, {
25
25
  * provider: 'local',
26
- * model: '/path/to/qwen2.5-coder-1.5b-instruct-q4_k_m.gguf',
26
+ * model: '/path/to/gemma-4-e4b-it-Q4_K_M.gguf',
27
27
  * });
28
28
  * test.title = title;
29
29
  * test.description = description;
@@ -663,6 +663,9 @@ function parseAiResponse(raw, fallbackTitle) {
663
663
  }
664
664
  return { title, description, steps };
665
665
  }
666
+ // ─── Shared dynamic import helper ────────────────────────────────────────────
667
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
668
+ const esmImport = new Function('m', 'return import(m)');
666
669
  const llamaSessionCache = new Map();
667
670
  async function getLlamaSession(modelPath) {
668
671
  const cached = llamaSessionCache.get(modelPath);
@@ -704,82 +707,65 @@ async function localLlamaSummary(code, fallbackTitle, modelPath, contextContent)
704
707
  }
705
708
  }
706
709
  async function ollamaSummary(code, fallbackTitle, model, baseUrl, contextContent) {
707
- const res = await fetchWithRetry(`${baseUrl}/api/generate`, {
708
- method: 'POST',
709
- headers: { 'Content-Type': 'application/json' },
710
- body: JSON.stringify({ model, prompt: buildPrompt(code, contextContent), stream: false }),
711
- signal: AbortSignal.timeout(60_000),
712
- }, 'ollama');
713
- if (!res.ok)
714
- throw new Error(`Ollama ${res.status}: ${await res.text()}`);
715
- const data = await res.json();
716
- return parseAiResponse(data.response ?? '', fallbackTitle);
717
- }
718
- /**
719
- * Fetch with automatic retry for transient errors:
720
- * 503 — model container cold-starting (Hugging Face serverless inference)
721
- * 429 — rate limit exceeded
722
- * Retries up to `maxRetries` times with exponential backoff starting at `baseDelayMs`.
723
- */
724
- async function fetchWithRetry(url, init, provider, maxRetries = 3, baseDelayMs = 5_000) {
725
- let attempt = 0;
726
- while (true) {
727
- const res = await fetch(url, init);
728
- if (res.status === 503 || res.status === 429) {
729
- if (attempt >= maxRetries)
730
- return res;
731
- const retryAfter = res.headers.get('retry-after');
732
- const delayMs = retryAfter
733
- ? parseInt(retryAfter, 10) * 1_000
734
- : baseDelayMs * Math.pow(2, attempt);
735
- const reason = res.status === 503 ? 'model loading' : 'rate limited';
736
- process.stderr.write(` [ai-summary] ${provider} ${reason} — retrying in ${Math.round(delayMs / 1000)}s (${attempt + 1}/${maxRetries})\n`);
737
- await new Promise(r => setTimeout(r, delayMs));
738
- attempt++;
739
- continue;
740
- }
741
- return res;
710
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
711
+ let Ollama;
712
+ try {
713
+ ({ Ollama } = await esmImport('ollama'));
714
+ }
715
+ catch {
716
+ throw new Error("'ollama' provider requires the ollama package. Install it with: npm install ollama");
742
717
  }
718
+ const client = new Ollama({ host: baseUrl });
719
+ const response = await client.chat({
720
+ model,
721
+ messages: [{ role: 'user', content: buildPrompt(code, contextContent) }],
722
+ });
723
+ return parseAiResponse(response.message?.content ?? '', fallbackTitle);
743
724
  }
744
725
  async function openaiSummary(code, fallbackTitle, model, apiKey, baseUrl, contextContent) {
745
- const res = await fetchWithRetry(`${baseUrl}/chat/completions`, {
746
- method: 'POST',
747
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
748
- body: JSON.stringify({
749
- model,
750
- messages: [{ role: 'user', content: buildPrompt(code, contextContent) }],
751
- temperature: 0,
752
- }),
753
- signal: AbortSignal.timeout(60_000),
754
- }, 'openai');
755
- if (!res.ok)
756
- throw new Error(`openai ${res.status}: ${await res.text()}`);
757
- const data = await res.json();
758
- return parseAiResponse(data.choices?.[0]?.message?.content ?? '', fallbackTitle);
726
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
727
+ let OpenAI;
728
+ try {
729
+ ({ OpenAI } = await esmImport('openai'));
730
+ }
731
+ catch {
732
+ throw new Error("'openai' provider requires the openai package. Install it with: npm install openai");
733
+ }
734
+ const client = new OpenAI({
735
+ apiKey,
736
+ ...(baseUrl ? { baseURL: baseUrl.replace(/\/$/, '') } : {}),
737
+ });
738
+ const msg = await client.chat.completions.create({
739
+ model,
740
+ messages: [{ role: 'user', content: buildPrompt(code, contextContent) }],
741
+ temperature: 0,
742
+ max_tokens: 2048,
743
+ });
744
+ return parseAiResponse(msg.choices?.[0]?.message?.content ?? '', fallbackTitle);
759
745
  }
760
746
  async function anthropicSummary(code, fallbackTitle, model, apiKey, baseUrl, contextContent) {
761
- const url = `${baseUrl.replace(/\/$/, '')}/messages`;
762
- const res = await fetchWithRetry(url, {
763
- method: 'POST',
764
- headers: {
765
- 'Content-Type': 'application/json',
766
- 'x-api-key': apiKey,
767
- 'anthropic-version': '2023-06-01',
768
- },
769
- body: JSON.stringify({
770
- model,
771
- max_tokens: 2048,
772
- messages: [{ role: 'user', content: buildPrompt(code, contextContent) }],
773
- }),
774
- signal: AbortSignal.timeout(60_000),
775
- }, 'anthropic');
776
- if (!res.ok)
777
- throw new Error(`anthropic ${res.status}: ${await res.text()}`);
778
- const data = await res.json();
779
- return parseAiResponse(data.content?.[0]?.text ?? '', fallbackTitle);
747
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
748
+ let Anthropic;
749
+ try {
750
+ ({ default: Anthropic } = await esmImport('@anthropic-ai/sdk'));
751
+ }
752
+ catch {
753
+ throw new Error("'anthropic' provider requires @anthropic-ai/sdk. Install it with: npm install @anthropic-ai/sdk");
754
+ }
755
+ const client = new Anthropic({
756
+ apiKey,
757
+ ...(baseUrl ? { baseURL: baseUrl.replace(/\/$/, '') } : {}),
758
+ });
759
+ const msg = await client.messages.create({
760
+ model,
761
+ max_tokens: 2048,
762
+ messages: [{ role: 'user', content: buildPrompt(code, contextContent) }],
763
+ });
764
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
765
+ return parseAiResponse(msg.content[0]?.text ?? '', fallbackTitle);
780
766
  }
781
767
  async function huggingfaceSummary(code, fallbackTitle, model, apiKey, contextContent) {
782
- // Hugging Face OpenAI-compatible /v1 endpoint
768
+ // Hugging Face exposes an OpenAI-compatible /v1 endpoint — use the openai SDK with a custom baseURL
783
769
  return openaiSummary(code, fallbackTitle, model, apiKey, 'https://api-inference.huggingface.co/v1', contextContent);
784
770
  }
785
771
  // ─── Bedrock helpers (retry + credential pre-flight) ─────────────────────────
@@ -863,26 +849,57 @@ async function bedrockSummary(code, fallbackTitle, model, region, contextContent
863
849
  return parseAiResponse(raw, fallbackTitle);
864
850
  }
865
851
  async function azureaiSummary(code, fallbackTitle, model, apiKey, baseUrl, contextContent) {
866
- // Azure OpenAI uses api-key header; baseUrl is the full endpoint or resource root
867
- let url = baseUrl.replace(/\/$/, '');
868
- if (!url.includes('/chat/completions')) {
869
- url = `${url}/openai/deployments/${model}/chat/completions?api-version=2024-12-01-preview`;
852
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
853
+ let AzureOpenAI;
854
+ try {
855
+ ({ AzureOpenAI } = await esmImport('openai'));
870
856
  }
871
- const res = await fetchWithRetry(url, {
872
- method: 'POST',
873
- headers: { 'Content-Type': 'application/json', 'api-key': apiKey },
874
- body: JSON.stringify({
857
+ catch {
858
+ throw new Error("'azureai' provider requires the openai package. Install it with: npm install openai");
859
+ }
860
+ const client = new AzureOpenAI({
861
+ apiKey,
862
+ endpoint: baseUrl.replace(/\/$/, ''),
863
+ apiVersion: '2024-12-01-preview',
864
+ deployment: model,
865
+ });
866
+ const msg = await client.chat.completions.create({
867
+ model,
868
+ messages: [{ role: 'user', content: buildPrompt(code, contextContent) }],
869
+ temperature: 0,
870
+ max_tokens: 2048,
871
+ });
872
+ return parseAiResponse(msg.choices?.[0]?.message?.content ?? '', fallbackTitle);
873
+ }
874
+ async function githubSummary(code, fallbackTitle, model, apiKey, contextContent) {
875
+ // GitHub Models is OpenAI-compatible — delegate to openaiSummary with GitHub Models endpoint
876
+ return openaiSummary(code, fallbackTitle, model, apiKey, 'https://models.inference.ai.azure.com', contextContent);
877
+ }
878
+ async function azureinferenceSummary(code, fallbackTitle, model, apiKey, endpoint, contextContent) {
879
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
880
+ let ModelClient, AzureKeyCredential;
881
+ try {
882
+ ({ default: ModelClient } = await esmImport('@azure-rest/ai-inference'));
883
+ ({ AzureKeyCredential } = await esmImport('@azure/core-auth'));
884
+ }
885
+ catch {
886
+ throw new Error("'azureinference' provider requires @azure-rest/ai-inference and @azure/core-auth. " +
887
+ 'Install with: npm install @azure-rest/ai-inference @azure/core-auth');
888
+ }
889
+ const client = ModelClient(endpoint.replace(/\/$/, ''), new AzureKeyCredential(apiKey));
890
+ const response = await client.path('/chat/completions').post({
891
+ body: {
892
+ model,
875
893
  messages: [{ role: 'user', content: buildPrompt(code, contextContent) }],
876
894
  temperature: 0,
877
895
  max_tokens: 2048,
878
- }),
879
- signal: AbortSignal.timeout(60_000),
880
- }, 'azureai');
881
- if (!res.ok)
882
- throw new Error(`Azure AI ${res.status}: ${await res.text()}`);
896
+ },
897
+ });
898
+ if (response.status !== '200') {
899
+ throw new Error(`Azure AI Inference ${response.status}: ${JSON.stringify(response.body)}`);
900
+ }
883
901
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
884
- const data = await res.json();
885
- return parseAiResponse(data.choices?.[0]?.message?.content ?? '', fallbackTitle);
902
+ return parseAiResponse(response.body.choices?.[0]?.message?.content ?? '', fallbackTitle);
886
903
  }
887
904
  function resolveEnvVar(value) {
888
905
  if (value.startsWith('$'))
@@ -942,51 +959,86 @@ async function analyzeFailure(testName, errorMessage, stackTrace, opts) {
942
959
  let raw = '';
943
960
  switch (opts.provider) {
944
961
  case 'ollama': {
945
- const res = await fetchWithRetry(`${opts.baseUrl ?? 'http://localhost:11434'}/api/generate`, {
946
- method: 'POST',
947
- headers: { 'Content-Type': 'application/json' },
948
- body: JSON.stringify({ model: opts.model ?? 'qwen2.5-coder:7b', prompt, stream: false }),
949
- signal: AbortSignal.timeout(30_000),
950
- }, 'ollama');
951
- if (!res.ok)
952
- throw new Error(`Ollama ${res.status}`);
953
- raw = (await res.json()).response ?? '';
962
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
963
+ let OllamaFA;
964
+ try {
965
+ ({ Ollama: OllamaFA } = await esmImport('ollama'));
966
+ }
967
+ catch {
968
+ throw new Error("ollama package not installed");
969
+ }
970
+ const ollamaClient = new OllamaFA({ host: opts.baseUrl ?? 'http://localhost:11434' });
971
+ const ollamaResp = await ollamaClient.chat({
972
+ model: opts.model ?? 'gemma-4-e4b-it',
973
+ messages: [{ role: 'user', content: prompt }],
974
+ });
975
+ raw = ollamaResp.message?.content ?? '';
954
976
  break;
955
977
  }
956
978
  case 'openai': {
957
- const res = await fetchWithRetry(`${opts.baseUrl ?? 'https://api.openai.com/v1'}/chat/completions`, {
958
- method: 'POST',
959
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${resolveEnvVar(opts.apiKey ?? '')}` },
960
- body: JSON.stringify({ model: opts.model ?? 'gpt-4o-mini', messages: [{ role: 'user', content: prompt }], temperature: 0, max_tokens: 256 }),
961
- signal: AbortSignal.timeout(30_000),
962
- }, 'openai');
963
- if (!res.ok)
964
- throw new Error(`openai ${res.status}`);
965
- raw = (await res.json()).choices?.[0]?.message?.content ?? '';
979
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
980
+ let OpenAI;
981
+ try {
982
+ ({ OpenAI } = await esmImport('openai'));
983
+ }
984
+ catch {
985
+ throw new Error("openai package not installed");
986
+ }
987
+ const openaiClient = new OpenAI({
988
+ apiKey: resolveEnvVar(opts.apiKey ?? ''),
989
+ ...(opts.baseUrl ? { baseURL: opts.baseUrl.replace(/\/$/, '') } : {}),
990
+ });
991
+ const openaiMsg = await openaiClient.chat.completions.create({
992
+ model: opts.model ?? 'gpt-4o-mini',
993
+ messages: [{ role: 'user', content: prompt }],
994
+ temperature: 0,
995
+ max_tokens: 256,
996
+ });
997
+ raw = openaiMsg.choices?.[0]?.message?.content ?? '';
966
998
  break;
967
999
  }
968
1000
  case 'anthropic': {
969
- const res = await fetchWithRetry(`${(opts.baseUrl ?? 'https://api.anthropic.com/v1').replace(/\/$/, '')}/messages`, {
970
- method: 'POST',
971
- headers: { 'Content-Type': 'application/json', 'x-api-key': resolveEnvVar(opts.apiKey ?? ''), 'anthropic-version': '2023-06-01' },
972
- body: JSON.stringify({ model: opts.model ?? 'claude-haiku-4-5-20251001', max_tokens: 1024, messages: [{ role: 'user', content: prompt }] }),
973
- signal: AbortSignal.timeout(30_000),
974
- }, 'anthropic');
975
- if (!res.ok)
976
- throw new Error(`anthropic ${res.status}`);
977
- raw = (await res.json()).content?.[0]?.text ?? '';
1001
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1002
+ let Anthropic;
1003
+ try {
1004
+ ({ default: Anthropic } = await esmImport('@anthropic-ai/sdk'));
1005
+ }
1006
+ catch {
1007
+ throw new Error("@anthropic-ai/sdk package not installed");
1008
+ }
1009
+ const anthropicClient = new Anthropic({
1010
+ apiKey: resolveEnvVar(opts.apiKey ?? ''),
1011
+ ...(opts.baseUrl ? { baseURL: opts.baseUrl.replace(/\/$/, '') } : {}),
1012
+ });
1013
+ const anthropicMsg = await anthropicClient.messages.create({
1014
+ model: opts.model ?? 'claude-haiku-4-5-20251001',
1015
+ max_tokens: 1024,
1016
+ messages: [{ role: 'user', content: prompt }],
1017
+ });
1018
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1019
+ raw = anthropicMsg.content[0]?.text ?? '';
978
1020
  break;
979
1021
  }
980
1022
  case 'huggingface': {
981
- const res = await fetchWithRetry('https://api-inference.huggingface.co/v1/chat/completions', {
982
- method: 'POST',
983
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${resolveEnvVar(opts.apiKey ?? '')}` },
984
- body: JSON.stringify({ model: opts.model, messages: [{ role: 'user', content: prompt }], max_tokens: 256 }),
985
- signal: AbortSignal.timeout(30_000),
986
- }, 'huggingface');
987
- if (!res.ok)
988
- throw new Error(`huggingface ${res.status}`);
989
- raw = (await res.json()).choices?.[0]?.message?.content ?? '';
1023
+ // Hugging Face OpenAI-compatible endpoint via openai SDK
1024
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1025
+ let OpenAIhf;
1026
+ try {
1027
+ ({ OpenAI: OpenAIhf } = await esmImport('openai'));
1028
+ }
1029
+ catch {
1030
+ throw new Error("openai package not installed");
1031
+ }
1032
+ const hfClient = new OpenAIhf({
1033
+ apiKey: resolveEnvVar(opts.apiKey ?? ''),
1034
+ baseURL: 'https://api-inference.huggingface.co/v1',
1035
+ });
1036
+ const hfMsg = await hfClient.chat.completions.create({
1037
+ model: opts.model,
1038
+ messages: [{ role: 'user', content: prompt }],
1039
+ max_tokens: 256,
1040
+ });
1041
+ raw = hfMsg.choices?.[0]?.message?.content ?? '';
990
1042
  break;
991
1043
  }
992
1044
  case 'bedrock': {
@@ -1014,19 +1066,69 @@ async function analyzeFailure(testName, errorMessage, stackTrace, opts) {
1014
1066
  case 'azureai': {
1015
1067
  if (!opts.baseUrl)
1016
1068
  break;
1017
- let url = opts.baseUrl.replace(/\/$/, '');
1018
- if (!url.includes('/chat/completions')) {
1019
- url = `${url}/openai/deployments/${opts.model ?? 'gpt-4o'}/chat/completions?api-version=2024-12-01-preview`;
1069
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1070
+ let AzureOpenAI;
1071
+ try {
1072
+ ({ AzureOpenAI } = await esmImport('openai'));
1073
+ }
1074
+ catch {
1075
+ throw new Error("openai package not installed");
1076
+ }
1077
+ const azureClient = new AzureOpenAI({
1078
+ apiKey: resolveEnvVar(opts.apiKey ?? ''),
1079
+ endpoint: opts.baseUrl.replace(/\/$/, ''),
1080
+ apiVersion: '2024-12-01-preview',
1081
+ deployment: opts.model ?? 'gpt-4o',
1082
+ });
1083
+ const azureMsg = await azureClient.chat.completions.create({
1084
+ model: opts.model ?? 'gpt-4o',
1085
+ messages: [{ role: 'user', content: prompt }],
1086
+ temperature: 0,
1087
+ max_tokens: 256,
1088
+ });
1089
+ raw = azureMsg.choices?.[0]?.message?.content ?? '';
1090
+ break;
1091
+ }
1092
+ case 'github': {
1093
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1094
+ let OpenAIgh;
1095
+ try {
1096
+ ({ OpenAI: OpenAIgh } = await esmImport('openai'));
1097
+ }
1098
+ catch {
1099
+ throw new Error("openai package not installed");
1100
+ }
1101
+ const ghClient = new OpenAIgh({
1102
+ apiKey: resolveEnvVar(opts.apiKey ?? process.env['GITHUB_TOKEN'] ?? ''),
1103
+ baseURL: 'https://models.inference.ai.azure.com',
1104
+ });
1105
+ const ghMsg = await ghClient.chat.completions.create({
1106
+ model: opts.model ?? 'gpt-4o',
1107
+ messages: [{ role: 'user', content: prompt }],
1108
+ temperature: 0,
1109
+ max_tokens: 256,
1110
+ });
1111
+ raw = ghMsg.choices?.[0]?.message?.content ?? '';
1112
+ break;
1113
+ }
1114
+ case 'azureinference': {
1115
+ if (!opts.baseUrl)
1116
+ break;
1117
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1118
+ let ModelClientFA, AzureKeyCredentialFA;
1119
+ try {
1120
+ ({ default: ModelClientFA } = await esmImport('@azure-rest/ai-inference'));
1121
+ ({ AzureKeyCredential: AzureKeyCredentialFA } = await esmImport('@azure/core-auth'));
1020
1122
  }
1021
- const res = await fetchWithRetry(url, {
1022
- method: 'POST',
1023
- headers: { 'Content-Type': 'application/json', 'api-key': resolveEnvVar(opts.apiKey ?? '') },
1024
- body: JSON.stringify({ messages: [{ role: 'user', content: prompt }], temperature: 0, max_tokens: 256 }),
1025
- signal: AbortSignal.timeout(30_000),
1026
- }, 'azureai');
1027
- if (!res.ok)
1028
- throw new Error(`azureai ${res.status}`);
1029
- raw = (await res.json()).choices?.[0]?.message?.content ?? '';
1123
+ catch {
1124
+ throw new Error("@azure-rest/ai-inference package not installed");
1125
+ }
1126
+ const aiClient = ModelClientFA(opts.baseUrl.replace(/\/$/, ''), new AzureKeyCredentialFA(resolveEnvVar(opts.apiKey ?? '')));
1127
+ const aiResp = await aiClient.path('/chat/completions').post({
1128
+ body: { model: opts.model ?? 'gpt-4o', messages: [{ role: 'user', content: prompt }], temperature: 0, max_tokens: 256 },
1129
+ });
1130
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1131
+ raw = aiResp.body.choices?.[0]?.message?.content ?? '';
1030
1132
  break;
1031
1133
  }
1032
1134
  default:
@@ -1072,7 +1174,7 @@ async function summarizeTest(test, localType, opts) {
1072
1174
  case 'local':
1073
1175
  return await localLlamaSummary(body, fallbackTitle, opts.model, ctx);
1074
1176
  case 'ollama':
1075
- return await ollamaSummary(body, fallbackTitle, opts.model ?? 'qwen2.5-coder:7b', opts.baseUrl ?? 'http://localhost:11434', ctx);
1177
+ return await ollamaSummary(body, fallbackTitle, opts.model ?? 'gemma-4-e4b-it', opts.baseUrl ?? 'http://localhost:11434', ctx);
1076
1178
  case 'openai':
1077
1179
  return await openaiSummary(body, fallbackTitle, opts.model ?? 'gpt-4o-mini', resolveEnvVar(opts.apiKey ?? ''), opts.baseUrl ?? 'https://api.openai.com/v1', ctx);
1078
1180
  case 'anthropic':
@@ -1087,6 +1189,12 @@ async function summarizeTest(test, localType, opts) {
1087
1189
  if (!opts.baseUrl)
1088
1190
  throw new Error('azureai provider requires --ai-url <azure-endpoint>');
1089
1191
  return await azureaiSummary(body, fallbackTitle, opts.model ?? 'gpt-4o', resolveEnvVar(opts.apiKey ?? ''), opts.baseUrl, ctx);
1192
+ case 'github':
1193
+ return await githubSummary(body, fallbackTitle, opts.model ?? 'gpt-4o', resolveEnvVar(opts.apiKey ?? process.env['GITHUB_TOKEN'] ?? ''), ctx);
1194
+ case 'azureinference':
1195
+ if (!opts.baseUrl)
1196
+ throw new Error('azureinference provider requires --ai-url <endpoint>');
1197
+ return await azureinferenceSummary(body, fallbackTitle, opts.model ?? 'gpt-4o', resolveEnvVar(opts.apiKey ?? ''), opts.baseUrl, ctx);
1090
1198
  }
1091
1199
  }
1092
1200
  catch (err) {