comfy-qa 2.0.0 → 2.1.0

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": "comfy-qa",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "ComfyUI QA automation CLI",
5
5
  "repository": {
6
6
  "type": "git",
@@ -79,26 +79,14 @@ interface ResearchResults {
79
79
 
80
80
  const ANTHROPIC_KEY = process.env.ANTHROPIC_API_KEY_QA ?? process.env.ANTHROPIC_API_KEY ?? "";
81
81
  const OPENROUTER_KEY = process.env.OPENROUTER_API_KEY ?? "";
82
+ const OPENROUTER_MODEL = process.env.OPENROUTER_MODEL ?? "openai/gpt-4.5";
82
83
 
83
84
  import Anthropic from "@anthropic-ai/sdk";
84
85
 
85
86
  const anthropicClient = ANTHROPIC_KEY ? new Anthropic({ apiKey: ANTHROPIC_KEY, timeout: 60_000 }) : null;
86
87
 
87
88
  async function callLLM(system: string, messages: any[]): Promise<string> {
88
- if (anthropicClient) {
89
- try {
90
- const res = await anthropicClient.messages.create({
91
- model: "claude-sonnet-4-20250514",
92
- max_tokens: 8192,
93
- system,
94
- messages,
95
- });
96
- return res.content?.[0]?.type === "text" ? res.content[0].text : "";
97
- } catch (err: any) {
98
- console.log(` ⚠ Anthropic SDK: ${err.message?.slice(0, 80)}`);
99
- }
100
- }
101
-
89
+ // Prefer OpenRouter
102
90
  if (OPENROUTER_KEY) {
103
91
  try {
104
92
  const controller = new AbortController();
@@ -108,19 +96,35 @@ async function callLLM(system: string, messages: any[]): Promise<string> {
108
96
  signal: controller.signal,
109
97
  headers: { Authorization: `Bearer ${OPENROUTER_KEY}`, "content-type": "application/json" },
110
98
  body: JSON.stringify({
111
- model: "anthropic/claude-sonnet-4-20250514",
99
+ model: OPENROUTER_MODEL,
112
100
  messages: [{ role: "system", content: system }, ...messages],
113
101
  max_tokens: 8192,
114
102
  }),
115
103
  });
116
104
  clearTimeout(timer);
117
105
  const json = (await res.json()) as any;
118
- return json.choices?.[0]?.message?.content ?? "";
106
+ if (json.choices?.[0]?.message?.content) return json.choices[0].message.content;
107
+ console.log(` ⚠ OpenRouter: ${json.error?.message?.slice(0, 80) ?? "empty response"}`);
119
108
  } catch (err: any) {
120
109
  console.log(` ⚠ OpenRouter: ${err.message?.slice(0, 60)}`);
121
110
  }
122
111
  }
123
112
 
113
+ // Fallback: Anthropic SDK
114
+ if (anthropicClient) {
115
+ try {
116
+ const res = await anthropicClient.messages.create({
117
+ model: "claude-sonnet-4-20250514",
118
+ max_tokens: 8192,
119
+ system,
120
+ messages,
121
+ });
122
+ return res.content?.[0]?.type === "text" ? res.content[0].text : "";
123
+ } catch (err: any) {
124
+ console.log(` ⚠ Anthropic SDK: ${err.message?.slice(0, 80)}`);
125
+ }
126
+ }
127
+
124
128
  return "";
125
129
  }
126
130
 
@@ -1,8 +1,27 @@
1
1
  import { $ } from "bun";
2
2
  import type { PRInfo, IssueInfo } from "../utils/github";
3
3
 
4
- /** Call Claude via CLI (already authenticated) or SDK if ANTHROPIC_API_KEY is set */
4
+ const OPENROUTER_KEY = process.env.OPENROUTER_API_KEY ?? "";
5
+ const OPENROUTER_MODEL = process.env.OPENROUTER_MODEL ?? "openai/gpt-4.5";
6
+
5
7
  async function callClaude(prompt: string): Promise<string> {
8
+ // Prefer OpenRouter
9
+ if (OPENROUTER_KEY) {
10
+ const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
11
+ method: "POST",
12
+ headers: { Authorization: `Bearer ${OPENROUTER_KEY}`, "content-type": "application/json" },
13
+ body: JSON.stringify({
14
+ model: OPENROUTER_MODEL,
15
+ messages: [{ role: "user", content: prompt }],
16
+ max_tokens: 4096,
17
+ }),
18
+ });
19
+ const json = (await res.json()) as any;
20
+ if (json.choices?.[0]?.message?.content) return json.choices[0].message.content;
21
+ console.log(` ⚠ OpenRouter: ${json.error?.message?.slice(0, 80) ?? "empty response"}`);
22
+ }
23
+
24
+ // Fallback: Anthropic SDK
6
25
  const apiKey = process.env.ANTHROPIC_API_KEY_QA ?? process.env.ANTHROPIC_API_KEY;
7
26
  if (apiKey) {
8
27
  const Anthropic = (await import("@anthropic-ai/sdk")).default;
@@ -14,7 +33,8 @@ async function callClaude(prompt: string): Promise<string> {
14
33
  });
15
34
  return response.content[0].type === "text" ? response.content[0].text : "";
16
35
  }
17
- // Fallback: pipe through claude CLI via stdin
36
+
37
+ // Last resort: claude CLI
18
38
  const proc = Bun.spawn(["claude", "--print", "--model", "claude-opus-4-6"], {
19
39
  stdin: new TextEncoder().encode(prompt),
20
40
  stdout: "pipe",
@@ -37,46 +37,56 @@ export function parseRef(ref: string): { owner: string; repo: string; number?: n
37
37
  }
38
38
 
39
39
  export async function fetchPR(owner: string, repo: string, number: number): Promise<PRInfo> {
40
- const json = await $`gh pr view ${number} --repo ${owner}/${repo} --json number,title,body,state,headRefName,baseRefName,author,labels,url,files,comments`.text();
41
- const raw = JSON.parse(json);
40
+ const [prJson, filesJson, commentsJson] = await Promise.all([
41
+ $`gh api repos/${owner}/${repo}/pulls/${number}`.text(),
42
+ $`gh api repos/${owner}/${repo}/pulls/${number}/files --paginate`.text(),
43
+ $`gh api repos/${owner}/${repo}/issues/${number}/comments --paginate`.text(),
44
+ ]);
45
+ const pr = JSON.parse(prJson);
46
+ const files = JSON.parse(filesJson);
47
+ const comments = JSON.parse(commentsJson);
42
48
  return {
43
- number: raw.number,
44
- title: raw.title,
45
- body: raw.body || "",
46
- state: raw.state,
47
- headRefName: raw.headRefName,
48
- baseRefName: raw.baseRefName,
49
- author: raw.author?.login || "unknown",
50
- labels: (raw.labels || []).map((l: any) => l.name),
51
- url: raw.url,
52
- files: (raw.files || []).map((f: any) => ({
53
- path: f.path,
49
+ number: pr.number,
50
+ title: pr.title,
51
+ body: pr.body || "",
52
+ state: pr.state,
53
+ headRefName: pr.head.ref,
54
+ baseRefName: pr.base.ref,
55
+ author: pr.user?.login || "unknown",
56
+ labels: (pr.labels || []).map((l: any) => l.name),
57
+ url: pr.html_url,
58
+ files: files.map((f: any) => ({
59
+ path: f.filename,
54
60
  additions: f.additions,
55
61
  deletions: f.deletions,
56
62
  })),
57
- comments: (raw.comments || []).map((c: any) => ({
58
- author: c.author?.login || "unknown",
63
+ comments: comments.map((c: any) => ({
64
+ author: c.user?.login || "unknown",
59
65
  body: c.body || "",
60
- createdAt: c.createdAt,
66
+ createdAt: c.created_at,
61
67
  })),
62
68
  };
63
69
  }
64
70
 
65
71
  export async function fetchIssue(owner: string, repo: string, number: number): Promise<IssueInfo> {
66
- const json = await $`gh issue view ${number} --repo ${owner}/${repo} --json number,title,body,state,author,labels,url,comments`.text();
67
- const raw = JSON.parse(json);
72
+ const [issueJson, commentsJson] = await Promise.all([
73
+ $`gh api repos/${owner}/${repo}/issues/${number}`.text(),
74
+ $`gh api repos/${owner}/${repo}/issues/${number}/comments --paginate`.text(),
75
+ ]);
76
+ const issue = JSON.parse(issueJson);
77
+ const comments = JSON.parse(commentsJson);
68
78
  return {
69
- number: raw.number,
70
- title: raw.title,
71
- body: raw.body || "",
72
- state: raw.state,
73
- author: raw.author?.login || "unknown",
74
- labels: (raw.labels || []).map((l: any) => l.name),
75
- url: raw.url,
76
- comments: (raw.comments || []).map((c: any) => ({
77
- author: c.author?.login || "unknown",
79
+ number: issue.number,
80
+ title: issue.title,
81
+ body: issue.body || "",
82
+ state: issue.state,
83
+ author: issue.user?.login || "unknown",
84
+ labels: (issue.labels || []).map((l: any) => l.name),
85
+ url: issue.html_url,
86
+ comments: comments.map((c: any) => ({
87
+ author: c.user?.login || "unknown",
78
88
  body: c.body || "",
79
- createdAt: c.createdAt,
89
+ createdAt: c.created_at,
80
90
  })),
81
91
  };
82
92
  }