free-coding-models 0.2.17 → 0.3.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.
@@ -13,8 +13,9 @@
13
13
  * - Emoji-aware padding via padEndDisplay for aligned verdict/status cells
14
14
  * - Viewport clipping with above/below indicators
15
15
  * - Smart badges (mode, tier filter, origin filter, profile)
16
- * - Proxy status line integrated in footer
16
+ * - Footer J badge: green "Proxy On" / red "Proxy Off" indicator with direct overlay access
17
17
  * - Install-endpoints shortcut surfaced directly in the footer hints
18
+ * - Full-width red outdated-version banner when a newer npm release is known
18
19
  * - Distinct auth-failure vs missing-key health labels so configured providers stay honest
19
20
  *
20
21
  * → Functions:
@@ -40,7 +41,7 @@ import { TIER_COLOR } from './tier-colors.js'
40
41
  import { getAvg, getVerdict, getUptime, getStabilityScore, getVersionStatusInfo } from './utils.js'
41
42
  import { usagePlaceholderForProvider } from './ping.js'
42
43
  import { formatTokenTotalCompact } from './token-usage-reader.js'
43
- import { calculateViewport, sortResultsWithPinnedFavorites, renderProxyStatusLine, padEndDisplay } from './render-helpers.js'
44
+ import { calculateViewport, sortResultsWithPinnedFavorites, padEndDisplay, displayWidth } from './render-helpers.js'
44
45
  import { getToolMeta } from './tool-metadata.js'
45
46
 
46
47
  const ACTIVE_FILTER_BG_BY_TIER = {
@@ -92,7 +93,7 @@ export function setActiveProxy(proxyInstance) {
92
93
  }
93
94
 
94
95
  // ─── renderTable: mode param controls footer hint text (opencode vs openclaw) ─────────
95
- export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, activeProfile = null, profileSaveMode = false, profileSaveBuffer = '', proxyStartupStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, proxyEnabled = false, isOutdated = false, latestVersion = null) {
96
+ export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, activeProfile = null, profileSaveMode = false, profileSaveBuffer = '', proxyStartupStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, widthWarningShowCount = 0, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, proxyEnabled = false, startupLatestVersion = null, versionAlertsEnabled = true) {
96
97
  // 📖 Filter out hidden models for display
97
98
  const visibleResults = results.filter(r => !r.hidden)
98
99
 
@@ -140,7 +141,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
140
141
  : chalk.bold.rgb(0, 200, 255)
141
142
  const modeBadge = toolBadgeColor(' [ ') + chalk.yellow.bold('Z') + toolBadgeColor(` Tool : ${toolMeta.label} ]`)
142
143
  const activeHeaderBadge = (text, bg = [57, 255, 20], fg = [0, 0, 0]) => chalk.bgRgb(...bg).rgb(...fg).bold(` ${text} `)
143
- const versionStatus = getVersionStatusInfo(settingsUpdateState, settingsUpdateLatestVersion)
144
+ const versionStatus = getVersionStatusInfo(settingsUpdateState, settingsUpdateLatestVersion, startupLatestVersion, versionAlertsEnabled)
144
145
 
145
146
  // 📖 Tier filter badge shown when filtering is active (shows exact tier name)
146
147
  const TIER_CYCLE_NAMES = [null, 'S+', 'S', 'A+', 'A', 'A-', 'B+', 'B', 'C']
@@ -193,23 +194,25 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
193
194
  const W_TOKENS = 7
194
195
  const W_USAGE = 7
195
196
  const MIN_TABLE_WIDTH = 166
196
- const warningDurationMs = 5_000
197
+ const warningDurationMs = 4_000
197
198
  const elapsed = widthWarningStartedAt ? Math.max(0, Date.now() - widthWarningStartedAt) : warningDurationMs
198
199
  const remainingMs = Math.max(0, warningDurationMs - elapsed)
199
- const showWidthWarning = terminalCols > 0 && terminalCols < MIN_TABLE_WIDTH && !widthWarningDismissed && remainingMs > 0
200
+ const showWidthWarning = terminalCols > 0 && terminalCols < MIN_TABLE_WIDTH && !widthWarningDismissed && widthWarningShowCount < 2 && remainingMs > 0
200
201
 
201
202
  if (showWidthWarning) {
202
203
  const lines = []
203
- const blankLines = Math.max(0, Math.floor(((terminalRows || 24) - 5) / 2))
204
- const warning = 'Please maximize your terminal for optimal use.'
205
- const warning2 = 'The current terminal is too small.'
206
- const warning3 = 'Reduce font size or maximize width of terminal.'
204
+ const blankLines = Math.max(0, Math.floor(((terminalRows || 24) - 7) / 2))
205
+ const warning = '🖥️ Please maximize your terminal for optimal use.'
206
+ const warning2 = '⚠️ The current terminal is too small.'
207
+ const warning3 = '📏 Reduce font size or maximize width of terminal.'
207
208
  const padLeft = Math.max(0, Math.floor((terminalCols - warning.length) / 2))
208
209
  const padLeft2 = Math.max(0, Math.floor((terminalCols - warning2.length) / 2))
209
210
  const padLeft3 = Math.max(0, Math.floor((terminalCols - warning3.length) / 2))
210
211
  for (let i = 0; i < blankLines; i++) lines.push('')
211
212
  lines.push(' '.repeat(padLeft) + chalk.red.bold(warning))
213
+ lines.push('')
212
214
  lines.push(' '.repeat(padLeft2) + chalk.red(warning2))
215
+ lines.push('')
213
216
  lines.push(' '.repeat(padLeft3) + chalk.red(warning3))
214
217
  lines.push('')
215
218
  lines.push(' '.repeat(Math.max(0, Math.floor((terminalCols - 34) / 2))) + chalk.yellow(`this message will hide in ${(remainingMs / 1000).toFixed(1)}s`))
@@ -334,7 +337,8 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
334
337
  }
335
338
 
336
339
  // 📖 Viewport clipping: only render models that fit on screen
337
- const vp = calculateViewport(terminalRows, scrollOffset, sorted.length)
340
+ const extraFooterLines = versionStatus.isOutdated ? 1 : 0
341
+ const vp = calculateViewport(terminalRows, scrollOffset, sorted.length, extraFooterLines)
338
342
 
339
343
  if (vp.hasAbove) {
340
344
  lines.push(chalk.dim(` ... ${vp.startIdx} more above ...`))
@@ -620,7 +624,8 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
620
624
  const activeHotkey = (keyLabel, text, bg = [57, 255, 20], fg = [0, 0, 0]) => chalk.bgRgb(...bg).rgb(...fg)(` ${keyLabel}${text} `)
621
625
  // 📖 Line 1: core navigation + filtering shortcuts
622
626
  lines.push(
623
- chalk.dim(` ↑↓ Navigate • `) +
627
+ (proxyEnabled ? activeHotkey('J', ' 📡 FCM Proxy V2 On') : activeHotkey('J', ' 📡 FCM Proxy V2 Off', [180, 30, 30], [255, 255, 255])) +
628
+ chalk.dim(` • `) +
624
629
  hotkey('F', ' Toggle Favorite') +
625
630
  chalk.dim(` • `) +
626
631
  (tierFilterMode > 0
@@ -631,7 +636,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
631
636
  ? activeHotkey('D', ` Provider (${activeOriginLabel})`, [0, 0, 0], PROVIDER_COLOR[[null, ...Object.keys(sources)][originFilterMode]] || [255, 255, 255])
632
637
  : hotkey('D', ' Provider')) +
633
638
  chalk.dim(` • `) +
634
- (hideUnconfiguredModels ? activeHotkey('E', ' Configured Only') : hotkey('E', ' Configured Only')) +
639
+ (hideUnconfiguredModels ? activeHotkey('E', ' Configured Models Only') : hotkey('E', ' Configured Models Only')) +
635
640
  chalk.dim(` • `) +
636
641
  hotkey('X', ' Token Logs') +
637
642
  chalk.dim(` • `) +
@@ -639,44 +644,46 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
639
644
  chalk.dim(` • `) +
640
645
  hotkey('K', ' Help')
641
646
  )
642
- // 📖 Line 2: profiles, install flow, recommend, feature request, bug report, and extended hints.
643
- lines.push(chalk.dim(` `) + hotkey('⇧P', ' Cycle profile') + chalk.dim(` • `) + hotkey('⇧S', ' Save profile') + chalk.dim(` • `) + hotkey('Y', ' Install endpoints') + chalk.dim(` • `) + hotkey('Q', ' Smart Recommend') + chalk.dim(` • `) + hotkey('J', ' Request feature') + chalk.dim(` • `) + hotkey('I', ' Report bug'))
644
- // 📖 Proxy status line — always rendered with explicit state (starting/running/failed/stopped)
645
- lines.push(renderProxyStatusLine(proxyStartupStatus, activeProxyRef, proxyEnabled))
646
- if (versionStatus.isOutdated) {
647
- const outdatedBadge = chalk.bgRed.bold.yellow(' This version is outdated . ')
648
- const latestLabel = chalk.redBright(` local v${LOCAL_VERSION} · latest v${versionStatus.latestVersion}`)
649
- lines.push(` ${outdatedBadge}${latestLabel}`)
650
- }
647
+ // 📖 Line 2: profiles, install flow, recommend, proxy shortcut, feedback, and extended hints.
648
+ lines.push(
649
+ chalk.dim(` `) +
650
+ hotkey('⇧P', ' Cycle profile') + chalk.dim(` • `) +
651
+ hotkey('⇧S', ' Save profile') + chalk.dim(` • `) +
652
+ hotkey('Y', ' Install endpoints') + chalk.dim(` • `) +
653
+ hotkey('Q', ' Smart Recommend') + chalk.dim(` • `) +
654
+ hotkey('I', ' Feedback, bugs & requests')
655
+ )
656
+ // 📖 Proxy status is now shown via the J badge in line 2 above — no need for a dedicated line
657
+ const footerLine =
658
+ chalk.rgb(255, 150, 200)(' Made with 💖 & ☕ by \x1b]8;;https://github.com/vava-nessa\x1b\\vava-nessa\x1b]8;;\x1b\\') +
659
+ chalk.dim(' • ') +
660
+ '⭐ ' +
661
+ chalk.yellow('\x1b]8;;https://github.com/vava-nessa/free-coding-models\x1b\\Star on GitHub\x1b]8;;\x1b\\') +
662
+ chalk.dim(' • ') +
663
+ '🤝 ' +
664
+ chalk.rgb(255, 165, 0)('\x1b]8;;https://github.com/vava-nessa/free-coding-models/graphs/contributors\x1b\\Contributors\x1b]8;;\x1b\\') +
665
+ chalk.dim(' • ') +
666
+ '☕ ' +
667
+ chalk.rgb(255, 200, 100)('\x1b]8;;https://buymeacoffee.com/vavanessadev\x1b\\Buy me a coffee\x1b]8;;\x1b\\') +
668
+ chalk.dim(' • ') +
669
+ '💬 ' +
670
+ chalk.rgb(200, 150, 255)('\x1b]8;;https://discord.gg/ZTNFHvvCkU\x1b\\Discord\x1b]8;;\x1b\\') +
671
+ chalk.dim(' → ') +
672
+ chalk.rgb(200, 150, 255)('https://discord.gg/ZTNFHvvCkU') +
673
+ chalk.dim(' • ') +
674
+ chalk.yellow('N') + chalk.dim(' Changelog') +
675
+ chalk.dim(' • ') +
676
+ chalk.dim('Ctrl+C Exit')
677
+ lines.push(footerLine)
651
678
 
652
- // 📖 Build footer line, with OUTDATED warning if isOutdated is true
653
- let footerLine = ''
654
- if (isOutdated) {
655
- // 📖 Show OUTDATED in red background, high contrast warning
656
- footerLine = chalk.bgRed.bold.white(' ⚠ OUTDATED version, please update with "npm i -g free-coding-models@latest" ')
657
- } else {
658
- footerLine =
659
- chalk.rgb(255, 150, 200)(' Made with 💖 & ☕ by \x1b]8;;https://github.com/vava-nessa\x1b\\vava-nessa\x1b]8;;\x1b\\') +
660
- chalk.dim(' • ') +
661
- '⭐ ' +
662
- chalk.yellow('\x1b]8;;https://github.com/vava-nessa/free-coding-models\x1b\\Star on GitHub\x1b]8;;\x1b\\') +
663
- chalk.dim(' • ') +
664
- '🤝 ' +
665
- chalk.rgb(255, 165, 0)('\x1b]8;;https://github.com/vava-nessa/free-coding-models/graphs/contributors\x1b\\Contributors\x1b]8;;\x1b\\') +
666
- chalk.dim(' • ') +
667
- '☕ ' +
668
- chalk.rgb(255, 200, 100)('\x1b]8;;https://buymeacoffee.com/vavanessadev\x1b\\Buy me a coffee\x1b]8;;\x1b\\') +
669
- chalk.dim(' • ') +
670
- '💬 ' +
671
- chalk.rgb(200, 150, 255)('\x1b]8;;https://discord.gg/ZTNFHvvCkU\x1b\\Discord\x1b]8;;\x1b\\') +
672
- chalk.dim(' → ') +
673
- chalk.rgb(200, 150, 255)('https://discord.gg/ZTNFHvvCkU') +
674
- chalk.dim(' • ') +
675
- chalk.yellow('N') + chalk.dim(' Changelog') +
676
- chalk.dim(' • ') +
677
- chalk.dim('Ctrl+C Exit')
679
+ if (versionStatus.isOutdated) {
680
+ const outdatedMessage = ` ⚠ Update available: v${LOCAL_VERSION} -> v${versionStatus.latestVersion}. If auto-update did not complete, run: npm install -g free-coding-models@latest`
681
+ const paddedBanner = terminalCols > 0
682
+ ? outdatedMessage + ' '.repeat(Math.max(0, terminalCols - displayWidth(outdatedMessage)))
683
+ : outdatedMessage
684
+ // 📖 Reserve a dedicated full-width red row so the warning cannot blend into the footer links.
685
+ lines.push(chalk.bgRed.white.bold(paddedBanner))
678
686
  }
679
- lines.push(footerLine)
680
687
 
681
688
  // 📖 Append \x1b[K (erase to EOL) to each line so leftover chars from previous
682
689
  // 📖 frames are cleared. Then pad with blank cleared lines to fill the terminal,
@@ -0,0 +1,423 @@
1
+ /**
2
+ * @file src/responses-translator.js
3
+ * @description Bidirectional translation between the OpenAI Responses API wire format
4
+ * and the older OpenAI Chat Completions wire used by the upstream free providers.
5
+ *
6
+ * @details
7
+ * 📖 Codex CLI can speak either `responses` or `chat` depending on provider config.
8
+ * 📖 Our upstream accounts still expose `/chat/completions`, so this module converts:
9
+ * - Responses requests → Chat Completions requests
10
+ * - Chat Completions JSON/SSE responses → Responses JSON/SSE responses
11
+ *
12
+ * 📖 The implementation focuses on the items Codex actually uses:
13
+ * - `instructions` / `input` message history
14
+ * - function tools + function-call outputs
15
+ * - assistant text deltas
16
+ * - function call argument deltas
17
+ * - final `response.completed` payload with usage
18
+ *
19
+ * @functions
20
+ * → `translateResponsesToOpenAI` — convert a Responses request body to chat completions
21
+ * → `translateOpenAIToResponses` — convert a chat completions JSON response to Responses JSON
22
+ * → `createResponsesSSETransformer` — convert chat-completions SSE chunks to Responses SSE
23
+ *
24
+ * @exports translateResponsesToOpenAI, translateOpenAIToResponses, createResponsesSSETransformer
25
+ * @see src/proxy-server.js
26
+ */
27
+
28
+ import { randomUUID } from 'node:crypto'
29
+ import { Transform } from 'node:stream'
30
+
31
+ const MAX_SSE_BUFFER = 1 * 1024 * 1024
32
+
33
+ function serializeJsonish(value) {
34
+ if (typeof value === 'string') return value
35
+ try {
36
+ return JSON.stringify(value ?? '')
37
+ } catch {
38
+ return String(value ?? '')
39
+ }
40
+ }
41
+
42
+ function normalizeResponseContent(content) {
43
+ if (typeof content === 'string') return [{ type: 'input_text', text: content }]
44
+ if (!Array.isArray(content)) return []
45
+ return content
46
+ }
47
+
48
+ function contentPartToText(part) {
49
+ if (!part || typeof part !== 'object') return ''
50
+ if (typeof part.text === 'string') return part.text
51
+ if (part.type === 'reasoning' && typeof part.summary === 'string') return part.summary
52
+ return ''
53
+ }
54
+
55
+ function pushTextMessage(messages, role, textParts) {
56
+ const text = textParts.join('\n').trim()
57
+ if (!text && role !== 'assistant') return
58
+ messages.push({ role, content: text || '' })
59
+ }
60
+
61
+ function makeFunctionToolCall(entry = {}) {
62
+ const callId = entry.call_id || entry.id || `call_${randomUUID().replace(/-/g, '')}`
63
+ return {
64
+ id: callId,
65
+ type: 'function',
66
+ function: {
67
+ name: entry.name || entry.function?.name || '',
68
+ arguments: typeof entry.arguments === 'string'
69
+ ? entry.arguments
70
+ : serializeJsonish(entry.arguments || entry.function?.arguments || {}),
71
+ },
72
+ }
73
+ }
74
+
75
+ export function translateResponsesToOpenAI(body) {
76
+ if (!body || typeof body !== 'object') return { model: '', messages: [], stream: false }
77
+
78
+ const messages = []
79
+
80
+ if (typeof body.instructions === 'string' && body.instructions.trim()) {
81
+ messages.push({ role: 'system', content: body.instructions.trim() })
82
+ }
83
+
84
+ const inputItems = Array.isArray(body.input)
85
+ ? body.input
86
+ : body.input != null
87
+ ? [body.input]
88
+ : []
89
+
90
+ for (const item of inputItems) {
91
+ if (typeof item === 'string') {
92
+ messages.push({ role: 'user', content: item })
93
+ continue
94
+ }
95
+ if (!item || typeof item !== 'object') continue
96
+
97
+ if (item.type === 'function_call') {
98
+ messages.push({ role: 'assistant', content: null, tool_calls: [makeFunctionToolCall(item)] })
99
+ continue
100
+ }
101
+
102
+ if (item.type === 'function_call_output') {
103
+ messages.push({
104
+ role: 'tool',
105
+ tool_call_id: item.call_id || item.id || '',
106
+ content: serializeJsonish(item.output),
107
+ })
108
+ continue
109
+ }
110
+
111
+ if (item.type === 'input_text' && typeof item.text === 'string') {
112
+ messages.push({ role: 'user', content: item.text })
113
+ continue
114
+ }
115
+
116
+ if (item.type !== 'message') continue
117
+
118
+ const role = item.role === 'assistant'
119
+ ? 'assistant'
120
+ : (item.role === 'developer' || item.role === 'system')
121
+ ? 'system'
122
+ : 'user'
123
+
124
+ const textParts = []
125
+ const toolCalls = []
126
+ for (const part of normalizeResponseContent(item.content)) {
127
+ if (part.type === 'function_call') {
128
+ toolCalls.push(makeFunctionToolCall(part))
129
+ continue
130
+ }
131
+ if (part.type === 'function_call_output') {
132
+ messages.push({
133
+ role: 'tool',
134
+ tool_call_id: part.call_id || part.id || '',
135
+ content: serializeJsonish(part.output),
136
+ })
137
+ continue
138
+ }
139
+ const text = contentPartToText(part)
140
+ if (text) textParts.push(text)
141
+ }
142
+
143
+ if (toolCalls.length > 0) {
144
+ messages.push({
145
+ role: 'assistant',
146
+ content: textParts.length > 0 ? textParts.join('\n') : null,
147
+ tool_calls: toolCalls,
148
+ })
149
+ continue
150
+ }
151
+
152
+ pushTextMessage(messages, role, textParts)
153
+ }
154
+
155
+ const result = {
156
+ model: body.model,
157
+ messages,
158
+ stream: body.stream === true,
159
+ }
160
+
161
+ if (body.max_output_tokens != null) result.max_tokens = body.max_output_tokens
162
+ if (body.temperature != null) result.temperature = body.temperature
163
+ if (body.top_p != null) result.top_p = body.top_p
164
+
165
+ if (Array.isArray(body.tools) && body.tools.length > 0) {
166
+ result.tools = body.tools
167
+ .filter(tool => tool && typeof tool === 'object' && (tool.type === 'function' || typeof tool.name === 'string'))
168
+ .map(tool => ({
169
+ type: 'function',
170
+ function: {
171
+ name: tool.name || tool.function?.name || '',
172
+ description: tool.description || tool.function?.description || '',
173
+ parameters: tool.parameters || tool.input_schema || tool.function?.parameters || {},
174
+ },
175
+ }))
176
+ }
177
+
178
+ return result
179
+ }
180
+
181
+ function buildResponsesOutput(message = {}) {
182
+ const output = []
183
+ const text = typeof message.content === 'string' ? message.content : ''
184
+ if (text || !Array.isArray(message.tool_calls) || message.tool_calls.length === 0) {
185
+ output.push({
186
+ id: `msg_${randomUUID().replace(/-/g, '')}`,
187
+ type: 'message',
188
+ status: 'completed',
189
+ role: 'assistant',
190
+ content: [{ type: 'output_text', text: text || '', annotations: [] }],
191
+ })
192
+ }
193
+
194
+ if (Array.isArray(message.tool_calls)) {
195
+ for (const toolCall of message.tool_calls) {
196
+ const callId = toolCall?.id || `call_${randomUUID().replace(/-/g, '')}`
197
+ output.push({
198
+ id: callId,
199
+ type: 'function_call',
200
+ status: 'completed',
201
+ call_id: callId,
202
+ name: toolCall?.function?.name || '',
203
+ arguments: toolCall?.function?.arguments || '{}',
204
+ })
205
+ }
206
+ }
207
+
208
+ return output
209
+ }
210
+
211
+ export function translateOpenAIToResponses(openaiResponse, requestModel) {
212
+ const choice = openaiResponse?.choices?.[0] || {}
213
+ const message = choice?.message || {}
214
+ const inputTokens = openaiResponse?.usage?.prompt_tokens || 0
215
+ const outputTokens = openaiResponse?.usage?.completion_tokens || 0
216
+
217
+ return {
218
+ id: openaiResponse?.id || `resp_${randomUUID().replace(/-/g, '')}`,
219
+ object: 'response',
220
+ created_at: Math.floor(Date.now() / 1000),
221
+ status: 'completed',
222
+ model: requestModel || openaiResponse?.model || '',
223
+ output: buildResponsesOutput(message),
224
+ usage: {
225
+ input_tokens: inputTokens,
226
+ output_tokens: outputTokens,
227
+ total_tokens: inputTokens + outputTokens,
228
+ },
229
+ }
230
+ }
231
+
232
+ function createResponseSseEvent(type, payload) {
233
+ return `event: ${type}\ndata: ${JSON.stringify({ type, ...payload })}\n\n`
234
+ }
235
+
236
+ export function createResponsesSSETransformer(requestModel) {
237
+ let buffer = ''
238
+ let responseId = `resp_${randomUUID().replace(/-/g, '')}`
239
+ let messageItemId = `msg_${randomUUID().replace(/-/g, '')}`
240
+ let createdAt = Math.floor(Date.now() / 1000)
241
+ let createdSent = false
242
+ let messageAdded = false
243
+ let messageText = ''
244
+ let promptTokens = 0
245
+ let completionTokens = 0
246
+ const functionCalls = new Map()
247
+
248
+ const ensureStarted = (stream) => {
249
+ if (createdSent) return
250
+ createdSent = true
251
+ stream.push(createResponseSseEvent('response.created', {
252
+ response: {
253
+ id: responseId,
254
+ object: 'response',
255
+ created_at: createdAt,
256
+ status: 'in_progress',
257
+ model: requestModel || '',
258
+ output: [],
259
+ },
260
+ }))
261
+ }
262
+
263
+ const ensureMessageItem = (stream) => {
264
+ if (messageAdded) return
265
+ messageAdded = true
266
+ stream.push(createResponseSseEvent('response.output_item.added', {
267
+ output_index: 0,
268
+ item: {
269
+ id: messageItemId,
270
+ type: 'message',
271
+ status: 'in_progress',
272
+ role: 'assistant',
273
+ content: [{ type: 'output_text', text: '', annotations: [] }],
274
+ },
275
+ }))
276
+ }
277
+
278
+ const transform = new Transform({
279
+ transform(chunk, _encoding, callback) {
280
+ buffer += chunk.toString()
281
+ if (buffer.length > MAX_SSE_BUFFER) {
282
+ buffer = ''
283
+ return callback(new Error('Responses SSE buffer overflow'))
284
+ }
285
+
286
+ const lines = buffer.split('\n')
287
+ buffer = lines.pop() || ''
288
+
289
+ for (const line of lines) {
290
+ if (!line.startsWith('data: ')) continue
291
+ const payload = line.slice(6).trim()
292
+
293
+ if (payload === '[DONE]') {
294
+ ensureStarted(this)
295
+ ensureMessageItem(this)
296
+
297
+ const output = [{
298
+ id: messageItemId,
299
+ type: 'message',
300
+ status: 'completed',
301
+ role: 'assistant',
302
+ content: [{ type: 'output_text', text: messageText, annotations: [] }],
303
+ }]
304
+ this.push(createResponseSseEvent('response.output_item.done', {
305
+ output_index: 0,
306
+ item: output[0],
307
+ }))
308
+
309
+ const sortedCalls = [...functionCalls.entries()].sort((a, b) => a[0] - b[0])
310
+ for (const [index, call] of sortedCalls) {
311
+ const item = {
312
+ id: call.id,
313
+ type: 'function_call',
314
+ status: 'completed',
315
+ call_id: call.id,
316
+ name: call.name,
317
+ arguments: call.arguments,
318
+ }
319
+ output.push(item)
320
+ this.push(createResponseSseEvent('response.output_item.done', {
321
+ output_index: index + 1,
322
+ item,
323
+ }))
324
+ }
325
+
326
+ this.push(createResponseSseEvent('response.completed', {
327
+ response: {
328
+ id: responseId,
329
+ object: 'response',
330
+ created_at: createdAt,
331
+ status: 'completed',
332
+ model: requestModel || '',
333
+ output,
334
+ usage: {
335
+ input_tokens: promptTokens,
336
+ output_tokens: completionTokens,
337
+ total_tokens: promptTokens + completionTokens,
338
+ },
339
+ },
340
+ }))
341
+ continue
342
+ }
343
+
344
+ let parsed
345
+ try {
346
+ parsed = JSON.parse(payload)
347
+ } catch {
348
+ continue
349
+ }
350
+
351
+ if (typeof parsed.id === 'string' && parsed.id.length > 0) responseId = parsed.id
352
+ if (typeof parsed.model === 'string' && parsed.model.length > 0 && !requestModel) {
353
+ requestModel = parsed.model
354
+ }
355
+ if (parsed.usage) {
356
+ promptTokens = parsed.usage.prompt_tokens || promptTokens
357
+ completionTokens = parsed.usage.completion_tokens || completionTokens
358
+ }
359
+
360
+ ensureStarted(this)
361
+ const choice = parsed.choices?.[0]
362
+ if (!choice) continue
363
+ const delta = choice.delta || {}
364
+
365
+ if (typeof delta.content === 'string' && delta.content.length > 0) {
366
+ ensureMessageItem(this)
367
+ messageText += delta.content
368
+ this.push(createResponseSseEvent('response.output_text.delta', {
369
+ output_index: 0,
370
+ item_id: messageItemId,
371
+ content_index: 0,
372
+ delta: delta.content,
373
+ }))
374
+ }
375
+
376
+ if (Array.isArray(delta.tool_calls)) {
377
+ for (const toolCallDelta of delta.tool_calls) {
378
+ const callIndex = Number.isInteger(toolCallDelta.index) ? toolCallDelta.index : functionCalls.size
379
+ const existing = functionCalls.get(callIndex) || {
380
+ id: toolCallDelta.id || `call_${randomUUID().replace(/-/g, '')}`,
381
+ name: '',
382
+ arguments: '',
383
+ added: false,
384
+ }
385
+ if (typeof toolCallDelta.id === 'string' && toolCallDelta.id.length > 0) {
386
+ existing.id = toolCallDelta.id
387
+ }
388
+ if (typeof toolCallDelta.function?.name === 'string' && toolCallDelta.function.name.length > 0) {
389
+ existing.name = toolCallDelta.function.name
390
+ }
391
+ if (!existing.added) {
392
+ existing.added = true
393
+ this.push(createResponseSseEvent('response.output_item.added', {
394
+ output_index: callIndex + 1,
395
+ item: {
396
+ id: existing.id,
397
+ type: 'function_call',
398
+ status: 'in_progress',
399
+ call_id: existing.id,
400
+ name: existing.name,
401
+ arguments: existing.arguments,
402
+ },
403
+ }))
404
+ }
405
+ if (typeof toolCallDelta.function?.arguments === 'string' && toolCallDelta.function.arguments.length > 0) {
406
+ existing.arguments += toolCallDelta.function.arguments
407
+ this.push(createResponseSseEvent('response.function_call_arguments.delta', {
408
+ output_index: callIndex + 1,
409
+ item_id: existing.id,
410
+ delta: toolCallDelta.function.arguments,
411
+ }))
412
+ }
413
+ functionCalls.set(callIndex, existing)
414
+ }
415
+ }
416
+ }
417
+
418
+ callback()
419
+ },
420
+ })
421
+
422
+ return { transform }
423
+ }