|
1 | 1 | import { type UpdateType } from '@patch-pulse/shared'; |
2 | 2 | import { type ReleaseEvidence } from './releaseEvidence'; |
| 3 | +import { getTimeoutMs, withTimeout } from './async'; |
3 | 4 |
|
4 | 5 | const OPENAI_API_URL = 'https://api.openai.com/v1/responses'; |
5 | 6 | const DEFAULT_NANO_MODEL = 'gpt-5-nano'; |
6 | 7 | const DEFAULT_MINI_MODEL = 'gpt-5-mini'; |
| 8 | +const OPENAI_SUMMARY_TIMEOUT_MS = getTimeoutMs( |
| 9 | + 'OPENAI_SUMMARY_TIMEOUT_MS', |
| 10 | + 15_000, |
| 11 | +); |
7 | 12 |
|
8 | 13 | function buildSummaryPrompt(args: { |
9 | 14 | packageName: string; |
@@ -49,44 +54,52 @@ async function callOpenAiSummary( |
49 | 54 | // Caller (summarizeReleaseEvidence) guards that the key exists before calling. |
50 | 55 | const apiKey = process.env.OPENAI_API_KEY!; |
51 | 56 |
|
52 | | - const response = await fetch(OPENAI_API_URL, { |
53 | | - method: 'POST', |
54 | | - headers: { |
55 | | - 'Content-Type': 'application/json', |
56 | | - Authorization: `Bearer ${apiKey}`, |
57 | | - }, |
58 | | - body: JSON.stringify({ |
59 | | - model, |
60 | | - input: [ |
61 | | - { |
62 | | - role: 'system', |
63 | | - content: [ |
| 57 | + return withTimeout( |
| 58 | + (async () => { |
| 59 | + const response = await fetch(OPENAI_API_URL, { |
| 60 | + method: 'POST', |
| 61 | + headers: { |
| 62 | + 'Content-Type': 'application/json', |
| 63 | + Authorization: `Bearer ${apiKey}`, |
| 64 | + }, |
| 65 | + body: JSON.stringify({ |
| 66 | + model, |
| 67 | + input: [ |
| 68 | + { |
| 69 | + role: 'system', |
| 70 | + content: [ |
| 71 | + { |
| 72 | + type: 'input_text', |
| 73 | + text: |
| 74 | + 'You summarize software releases for Slack. Use only the provided evidence. ' + |
| 75 | + 'Do not speculate. Return a single plain-text sentence under 240 characters. ' + |
| 76 | + 'If the evidence is insufficient, return exactly INSUFFICIENT.', |
| 77 | + }, |
| 78 | + ], |
| 79 | + }, |
64 | 80 | { |
65 | | - type: 'input_text', |
66 | | - text: |
67 | | - 'You summarize software releases for Slack. Use only the provided evidence. ' + |
68 | | - 'Do not speculate. Return a single plain-text sentence under 240 characters. ' + |
69 | | - 'If the evidence is insufficient, return exactly INSUFFICIENT.', |
| 81 | + role: 'user', |
| 82 | + content: [{ type: 'input_text', text: prompt }], |
70 | 83 | }, |
71 | 84 | ], |
72 | | - }, |
73 | | - { |
74 | | - role: 'user', |
75 | | - content: [{ type: 'input_text', text: prompt }], |
76 | | - }, |
77 | | - ], |
78 | | - }), |
79 | | - }); |
| 85 | + }), |
| 86 | + }); |
80 | 87 |
|
81 | | - if (!response.ok) { |
82 | | - throw new Error(`OpenAI API error ${response.status}`); |
83 | | - } |
| 88 | + if (!response.ok) { |
| 89 | + throw new Error(`OpenAI API error ${response.status}`); |
| 90 | + } |
84 | 91 |
|
85 | | - const data = (await response.json()) as { |
86 | | - output_text?: string; |
87 | | - }; |
| 92 | + const data = (await response.json()) as { |
| 93 | + output_text?: string; |
| 94 | + }; |
88 | 95 |
|
89 | | - return data.output_text?.trim() || null; |
| 96 | + return data.output_text?.trim() || null; |
| 97 | + })(), |
| 98 | + { |
| 99 | + label: `OpenAI summary request (${model})`, |
| 100 | + timeoutMs: OPENAI_SUMMARY_TIMEOUT_MS, |
| 101 | + }, |
| 102 | + ); |
90 | 103 | } |
91 | 104 |
|
92 | 105 | export async function summarizeReleaseEvidence(args: { |
|
0 commit comments