free-coding-models 0.3.11 → 0.3.13
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/CHANGELOG.md +24 -0
- package/README.md +112 -1134
- package/bin/free-coding-models.js +18 -170
- package/package.json +2 -3
- package/src/cli-help.js +0 -18
- package/src/config.js +5 -117
- package/src/endpoint-installer.js +26 -64
- package/src/key-handler.js +90 -443
- package/src/legacy-proxy-cleanup.js +432 -0
- package/src/openclaw.js +69 -108
- package/src/opencode-config.js +48 -0
- package/src/opencode.js +6 -248
- package/src/overlays.js +28 -520
- package/src/product-flags.js +14 -0
- package/src/render-helpers.js +2 -34
- package/src/render-table.js +11 -19
- package/src/testfcm.js +90 -43
- package/src/token-usage-reader.js +9 -38
- package/src/tool-launchers.js +235 -409
- package/src/tool-metadata.js +0 -7
- package/src/utils.js +3 -68
- package/bin/fcm-proxy-daemon.js +0 -242
- package/src/account-manager.js +0 -634
- package/src/anthropic-translator.js +0 -440
- package/src/daemon-manager.js +0 -527
- package/src/error-classifier.js +0 -157
- package/src/log-reader.js +0 -195
- package/src/opencode-sync.js +0 -200
- package/src/proxy-foreground.js +0 -234
- package/src/proxy-server.js +0 -1506
- package/src/proxy-sync.js +0 -591
- package/src/proxy-topology.js +0 -85
- package/src/request-transformer.js +0 -180
- package/src/responses-translator.js +0 -423
- package/src/token-stats.js +0 -320
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* request-transformer.js
|
|
3
|
-
*
|
|
4
|
-
* Utilities for transforming outgoing API request bodies before they are
|
|
5
|
-
* forwarded to a model provider:
|
|
6
|
-
* - applyThinkingBudget — control Anthropic-style "thinking" budget
|
|
7
|
-
* - compressContext — reduce prompt size at increasing compression levels
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
// Internal helpers
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Count the total characters contributed by a single message.
|
|
16
|
-
* Handles both plain-string content and array-of-blocks content.
|
|
17
|
-
*
|
|
18
|
-
* @param {object} msg
|
|
19
|
-
* @returns {number}
|
|
20
|
-
*/
|
|
21
|
-
function messageCharCount(msg) {
|
|
22
|
-
if (typeof msg.content === 'string') return msg.content.length
|
|
23
|
-
if (Array.isArray(msg.content)) {
|
|
24
|
-
return msg.content.reduce((sum, block) => {
|
|
25
|
-
if (typeof block === 'string') return sum + block.length
|
|
26
|
-
if (block.type === 'text') return sum + (block.text?.length || 0)
|
|
27
|
-
if (block.type === 'thinking') return sum + (block.thinking?.length || 0)
|
|
28
|
-
return sum
|
|
29
|
-
}, 0)
|
|
30
|
-
}
|
|
31
|
-
return 0
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
// applyThinkingBudget
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Attach (or omit) an Anthropic-style "thinking" budget to the request body.
|
|
40
|
-
*
|
|
41
|
-
* Modes:
|
|
42
|
-
* 'passthrough' — return a shallow copy of body with no changes
|
|
43
|
-
* 'custom' — add thinking: { budget_tokens: config.budget_tokens }
|
|
44
|
-
* 'auto' — add thinking only when the total prompt is > 2 000 chars;
|
|
45
|
-
* budget is proportional: min(totalChars * 2, 32 000)
|
|
46
|
-
*
|
|
47
|
-
* The original body is NEVER mutated.
|
|
48
|
-
*
|
|
49
|
-
* @param {object} body - The request body (OpenAI-compatible shape)
|
|
50
|
-
* @param {{ mode: string, budget_tokens?: number }} config
|
|
51
|
-
* @returns {object} - A new body object
|
|
52
|
-
*/
|
|
53
|
-
export function applyThinkingBudget(body, config) {
|
|
54
|
-
const { mode } = config
|
|
55
|
-
|
|
56
|
-
if (mode === 'passthrough') {
|
|
57
|
-
return { ...body }
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (mode === 'custom') {
|
|
61
|
-
return { ...body, thinking: { budget_tokens: config.budget_tokens } }
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (mode === 'auto') {
|
|
65
|
-
const messages = Array.isArray(body.messages) ? body.messages : []
|
|
66
|
-
const totalChars = messages.reduce((sum, msg) => {
|
|
67
|
-
if (typeof msg.content === 'string') return sum + msg.content.length
|
|
68
|
-
if (Array.isArray(msg.content)) {
|
|
69
|
-
return sum + msg.content.reduce((s, block) => {
|
|
70
|
-
if (typeof block === 'string') return s + block.length
|
|
71
|
-
return s + (block.text?.length || 0) + (block.thinking?.length || 0)
|
|
72
|
-
}, 0)
|
|
73
|
-
}
|
|
74
|
-
return sum
|
|
75
|
-
}, 0)
|
|
76
|
-
|
|
77
|
-
if (totalChars > 2000) {
|
|
78
|
-
const budget_tokens = Math.min(Math.floor(totalChars * 2), 32000)
|
|
79
|
-
return { ...body, thinking: { budget_tokens } }
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return { ...body }
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Unknown mode — return shallow copy unchanged
|
|
86
|
-
return { ...body }
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// ---------------------------------------------------------------------------
|
|
90
|
-
// compressContext
|
|
91
|
-
// ---------------------------------------------------------------------------
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Reduce the size of the messages array at increasing compression levels.
|
|
95
|
-
*
|
|
96
|
-
* Levels:
|
|
97
|
-
* 0 — no change (shallow copy of array)
|
|
98
|
-
* 1 — truncate tool-result messages whose content exceeds toolResultMaxChars
|
|
99
|
-
* 2 — L1 + truncate thinking blocks in assistant messages
|
|
100
|
-
* 3 — L2 + drop oldest non-system messages when total chars exceed maxTotalChars
|
|
101
|
-
*
|
|
102
|
-
* The original messages array and its objects are NEVER mutated.
|
|
103
|
-
*
|
|
104
|
-
* @param {object[]} messages
|
|
105
|
-
* @param {{
|
|
106
|
-
* level?: number,
|
|
107
|
-
* toolResultMaxChars?: number,
|
|
108
|
-
* thinkingMaxChars?: number,
|
|
109
|
-
* maxTotalChars?: number
|
|
110
|
-
* }} opts
|
|
111
|
-
* @returns {object[]}
|
|
112
|
-
*/
|
|
113
|
-
export function compressContext(messages, opts = {}) {
|
|
114
|
-
const {
|
|
115
|
-
level = 0,
|
|
116
|
-
toolResultMaxChars = 4000,
|
|
117
|
-
thinkingMaxChars = 1000,
|
|
118
|
-
maxTotalChars = 100000,
|
|
119
|
-
} = opts
|
|
120
|
-
|
|
121
|
-
if (level === 0) {
|
|
122
|
-
return [...messages]
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// L1: trim oversized tool results
|
|
126
|
-
let result = messages.map(msg => {
|
|
127
|
-
if (msg.role === 'tool' && typeof msg.content === 'string') {
|
|
128
|
-
if (msg.content.length > toolResultMaxChars) {
|
|
129
|
-
return {
|
|
130
|
-
...msg,
|
|
131
|
-
content: msg.content.slice(0, toolResultMaxChars) + '\n[truncated]',
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return msg
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
if (level === 1) {
|
|
139
|
-
return result
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// L2: trim thinking blocks in assistant messages
|
|
143
|
-
result = result.map(msg => {
|
|
144
|
-
if (msg.role === 'assistant' && Array.isArray(msg.content)) {
|
|
145
|
-
const newContent = msg.content.map(block => {
|
|
146
|
-
if (
|
|
147
|
-
block.type === 'thinking' &&
|
|
148
|
-
typeof block.thinking === 'string' &&
|
|
149
|
-
block.thinking.length > thinkingMaxChars
|
|
150
|
-
) {
|
|
151
|
-
return { ...block, thinking: block.thinking.slice(0, thinkingMaxChars) }
|
|
152
|
-
}
|
|
153
|
-
return block
|
|
154
|
-
})
|
|
155
|
-
// Only create a new message object when something actually changed
|
|
156
|
-
const changed = newContent.some((b, i) => b !== msg.content[i])
|
|
157
|
-
return changed ? { ...msg, content: newContent } : msg
|
|
158
|
-
}
|
|
159
|
-
return msg
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
if (level === 2) {
|
|
163
|
-
return result
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// L3: drop oldest non-system messages until total chars is within budget
|
|
167
|
-
// Always preserve: every 'system' message, and the last message in the array.
|
|
168
|
-
const totalChars = () => result.reduce((sum, msg) => sum + messageCharCount(msg), 0)
|
|
169
|
-
|
|
170
|
-
while (totalChars() > maxTotalChars && result.length > 1) {
|
|
171
|
-
// Find the first droppable message: not 'system', not the last one
|
|
172
|
-
const dropIdx = result.findIndex(
|
|
173
|
-
(msg, idx) => msg.role !== 'system' && idx !== result.length - 1
|
|
174
|
-
)
|
|
175
|
-
if (dropIdx === -1) break // nothing left to drop
|
|
176
|
-
result = [...result.slice(0, dropIdx), ...result.slice(dropIdx + 1)]
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return result
|
|
180
|
-
}
|
|
@@ -1,423 +0,0 @@
|
|
|
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
|
-
}
|