@goodandready/dsh-subscriptions 0.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/LICENSE +21 -0
- package/README.md +102 -0
- package/cordis.patch.yml +8 -0
- package/lib/accounts.js +198 -0
- package/lib/adapter.js +110 -0
- package/lib/blob.js +43 -0
- package/lib/client.js +248 -0
- package/lib/code-assist.js +250 -0
- package/lib/gemini-schema.js +67 -0
- package/lib/google-validation.js +117 -0
- package/lib/http.js +44 -0
- package/lib/index.js +422 -0
- package/lib/jwt.js +28 -0
- package/lib/messages.js +258 -0
- package/lib/oauth.js +68 -0
- package/lib/pkce.js +12 -0
- package/lib/refs.js +59 -0
- package/lib/responses-stream.js +175 -0
- package/lib/rotate.js +31 -0
- package/lib/sse.js +44 -0
- package/lib/stream-rotate.js +41 -0
- package/lib/usage.js +51 -0
- package/lib/vendors/antigravity-oauth.js +10 -0
- package/lib/vendors/antigravity.js +152 -0
- package/lib/vendors/claude.js +119 -0
- package/lib/vendors/codex.js +163 -0
- package/lib/vendors/grok.js +223 -0
- package/lib/vendors/index.js +17 -0
- package/lib/wire.js +200 -0
- package/package.json +63 -0
package/lib/messages.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { geminiFunctionDeclarations } from './gemini-schema.js'
|
|
2
|
+
|
|
3
|
+
function flattenText(content) {
|
|
4
|
+
if (typeof content === 'string') return content
|
|
5
|
+
if (!Array.isArray(content)) return ''
|
|
6
|
+
return content
|
|
7
|
+
.filter((block) => block && (block.type === 'text' || block.type === 'reasoning'))
|
|
8
|
+
.map((block) => block.text || '')
|
|
9
|
+
.join('')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function toolResults(content) {
|
|
13
|
+
return Array.isArray(content) ? content.filter((b) => b && b.type === 'tool-result') : []
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function toolCalls(content) {
|
|
17
|
+
return Array.isArray(content) ? content.filter((b) => b && b.type === 'tool-call') : []
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function openaiMessages(options) {
|
|
21
|
+
const out = []
|
|
22
|
+
if (options.system) out.push({ role: 'system', content: options.system })
|
|
23
|
+
for (const message of options.messages || []) {
|
|
24
|
+
if (message.role === 'system') {
|
|
25
|
+
out.push({ role: 'system', content: flattenText(message.content) })
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
28
|
+
if (message.role === 'assistant') {
|
|
29
|
+
const calls = toolCalls(message.content).map((block) => ({
|
|
30
|
+
id: block.id,
|
|
31
|
+
type: 'function',
|
|
32
|
+
function: { name: block.name, arguments: block.arguments || '{}' },
|
|
33
|
+
}))
|
|
34
|
+
out.push({
|
|
35
|
+
role: 'assistant',
|
|
36
|
+
content: flattenText(message.content) || (calls.length ? null : ''),
|
|
37
|
+
...(calls.length ? { tool_calls: calls } : {}),
|
|
38
|
+
})
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
const results = toolResults(message.content)
|
|
42
|
+
const text = flattenText(message.content)
|
|
43
|
+
if (text || !results.length) out.push({ role: 'user', content: text })
|
|
44
|
+
for (const result of results) {
|
|
45
|
+
out.push({
|
|
46
|
+
role: 'tool',
|
|
47
|
+
tool_call_id: result.toolCallId,
|
|
48
|
+
content: flattenText(result.content) || '(no output)',
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return out
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function openaiTools(options) {
|
|
56
|
+
const tools = options.tools
|
|
57
|
+
if (!Array.isArray(tools) || !tools.length) return undefined
|
|
58
|
+
return tools.map((tool) => ({
|
|
59
|
+
type: 'function',
|
|
60
|
+
function: {
|
|
61
|
+
name: tool.name,
|
|
62
|
+
description: tool.description || '',
|
|
63
|
+
parameters: tool.parameters || { type: 'object' },
|
|
64
|
+
},
|
|
65
|
+
}))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function codexResponsesBody(options, fallbackInstructions) {
|
|
69
|
+
const systemParts = []
|
|
70
|
+
if (options.system) systemParts.push(options.system)
|
|
71
|
+
const input = []
|
|
72
|
+
let pendingRole = null
|
|
73
|
+
let pendingContent = []
|
|
74
|
+
function flush() {
|
|
75
|
+
if (!pendingRole) return
|
|
76
|
+
const type = pendingRole === 'assistant' ? 'output_text' : 'input_text'
|
|
77
|
+
input.push({
|
|
78
|
+
role: pendingRole,
|
|
79
|
+
content: pendingContent.length ? pendingContent : [{ type, text: '' }],
|
|
80
|
+
})
|
|
81
|
+
pendingRole = null
|
|
82
|
+
pendingContent = []
|
|
83
|
+
}
|
|
84
|
+
function addText(role, text) {
|
|
85
|
+
const type = role === 'assistant' ? 'output_text' : 'input_text'
|
|
86
|
+
if (pendingRole && pendingRole !== role) flush()
|
|
87
|
+
pendingRole = role
|
|
88
|
+
if (text) pendingContent.push({ type, text })
|
|
89
|
+
}
|
|
90
|
+
for (const message of options.messages || []) {
|
|
91
|
+
if (message.role === 'system') {
|
|
92
|
+
systemParts.push(flattenText(message.content))
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
if (message.role === 'assistant') {
|
|
96
|
+
const text = flattenText(message.content)
|
|
97
|
+
if (text) addText('assistant', text)
|
|
98
|
+
for (const call of toolCalls(message.content)) {
|
|
99
|
+
flush()
|
|
100
|
+
input.push({
|
|
101
|
+
type: 'function_call',
|
|
102
|
+
call_id: String(call.id || ''),
|
|
103
|
+
name: call.name,
|
|
104
|
+
arguments: typeof call.arguments === 'string' ? call.arguments : JSON.stringify(call.arguments || {}),
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
const text = flattenText(message.content)
|
|
110
|
+
if (text) addText('user', text)
|
|
111
|
+
for (const result of toolResults(message.content)) {
|
|
112
|
+
flush()
|
|
113
|
+
input.push({
|
|
114
|
+
type: 'function_call_output',
|
|
115
|
+
call_id: String(result.toolCallId || ''),
|
|
116
|
+
output: flattenText(result.content) || '',
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
flush()
|
|
121
|
+
if (!input.length) input.push({ role: 'user', content: [{ type: 'input_text', text: '' }] })
|
|
122
|
+
const tools = openaiTools(options)
|
|
123
|
+
const responsesTools = tools && tools.map((tool) => ({
|
|
124
|
+
type: 'function',
|
|
125
|
+
name: tool.function.name,
|
|
126
|
+
description: tool.function.description,
|
|
127
|
+
parameters: tool.function.parameters,
|
|
128
|
+
}))
|
|
129
|
+
const instructions = systemParts.filter(Boolean).join('\n\n') || fallbackInstructions
|
|
130
|
+
return {
|
|
131
|
+
model: options.model,
|
|
132
|
+
stream: true,
|
|
133
|
+
store: false,
|
|
134
|
+
instructions,
|
|
135
|
+
input,
|
|
136
|
+
...(responsesTools && responsesTools.length ? { tools: responsesTools } : {}),
|
|
137
|
+
...(options.maxTokens != null ? { max_output_tokens: options.maxTokens } : {}),
|
|
138
|
+
...(options.temperature != null ? { temperature: options.temperature } : {}),
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function anthropicPayload(options, extraSystem) {
|
|
143
|
+
const systemParts = []
|
|
144
|
+
if (extraSystem) systemParts.push(extraSystem)
|
|
145
|
+
if (options.system) systemParts.push(options.system)
|
|
146
|
+
const messages = []
|
|
147
|
+
for (const message of options.messages || []) {
|
|
148
|
+
if (message.role === 'system') {
|
|
149
|
+
systemParts.push(flattenText(message.content))
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
if (message.role === 'assistant') {
|
|
153
|
+
const blocks = []
|
|
154
|
+
const text = flattenText(message.content)
|
|
155
|
+
if (text) blocks.push({ type: 'text', text })
|
|
156
|
+
for (const call of toolCalls(message.content)) {
|
|
157
|
+
blocks.push({
|
|
158
|
+
type: 'tool_use',
|
|
159
|
+
id: call.id,
|
|
160
|
+
name: call.name,
|
|
161
|
+
input: safeJson(call.arguments),
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
messages.push({ role: 'assistant', content: blocks.length ? blocks : [{ type: 'text', text: '' }] })
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
const blocks = []
|
|
168
|
+
const text = flattenText(message.content)
|
|
169
|
+
if (text) blocks.push({ type: 'text', text })
|
|
170
|
+
for (const result of toolResults(message.content)) {
|
|
171
|
+
blocks.push({
|
|
172
|
+
type: 'tool_result',
|
|
173
|
+
tool_use_id: result.toolCallId,
|
|
174
|
+
content: flattenText(result.content) || '',
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
messages.push({ role: 'user', content: blocks.length ? blocks : [{ type: 'text', text: '' }] })
|
|
178
|
+
}
|
|
179
|
+
const tools = Array.isArray(options.tools)
|
|
180
|
+
? options.tools.map((tool) => ({
|
|
181
|
+
name: tool.name,
|
|
182
|
+
description: tool.description || '',
|
|
183
|
+
input_schema: tool.parameters || { type: 'object' },
|
|
184
|
+
}))
|
|
185
|
+
: undefined
|
|
186
|
+
return {
|
|
187
|
+
model: options.model,
|
|
188
|
+
max_tokens: options.maxTokens || 8192,
|
|
189
|
+
stream: true,
|
|
190
|
+
...(systemParts.length ? { system: systemParts.join('\n\n') } : {}),
|
|
191
|
+
messages,
|
|
192
|
+
...(tools && tools.length ? { tools } : {}),
|
|
193
|
+
...(options.temperature != null ? { temperature: options.temperature } : {}),
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function googleContents(options) {
|
|
198
|
+
const contents = []
|
|
199
|
+
const systemParts = []
|
|
200
|
+
if (options.system) systemParts.push({ text: options.system })
|
|
201
|
+
for (const message of options.messages || []) {
|
|
202
|
+
if (message.role === 'system') {
|
|
203
|
+
systemParts.push({ text: flattenText(message.content) })
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
206
|
+
const role = message.role === 'assistant' ? 'model' : 'user'
|
|
207
|
+
const parts = []
|
|
208
|
+
const text = flattenText(message.content)
|
|
209
|
+
if (text) parts.push({ text })
|
|
210
|
+
for (const call of toolCalls(message.content)) {
|
|
211
|
+
parts.push({ functionCall: { name: call.name, args: safeJson(call.arguments) } })
|
|
212
|
+
}
|
|
213
|
+
for (const result of toolResults(message.content)) {
|
|
214
|
+
parts.push({
|
|
215
|
+
functionResponse: {
|
|
216
|
+
name: result.toolName || result.name || 'tool',
|
|
217
|
+
response: { output: flattenText(result.content) },
|
|
218
|
+
},
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
if (!parts.length) parts.push({ text: '' })
|
|
222
|
+
contents.push({ role, parts })
|
|
223
|
+
}
|
|
224
|
+
const declarations = geminiFunctionDeclarations(options.tools)
|
|
225
|
+
const tools = declarations ? [{ functionDeclarations: declarations }] : undefined
|
|
226
|
+
return {
|
|
227
|
+
contents,
|
|
228
|
+
...(systemParts.length ? { systemInstruction: { parts: systemParts } } : {}),
|
|
229
|
+
...(tools ? { tools } : {}),
|
|
230
|
+
generationConfig: {
|
|
231
|
+
...(options.temperature != null ? { temperature: options.temperature } : {}),
|
|
232
|
+
...(options.maxTokens != null ? { maxOutputTokens: options.maxTokens } : {}),
|
|
233
|
+
},
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function safeJson(text) {
|
|
238
|
+
if (text && typeof text === 'object') return text
|
|
239
|
+
try { return JSON.parse(text || '{}') } catch { return {} }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function modelCatalog(provider, entries) {
|
|
243
|
+
return (entries || []).map((entry) => {
|
|
244
|
+
if (!entry) return null
|
|
245
|
+
if (typeof entry === 'string') return { provider, id: entry, name: entry }
|
|
246
|
+
const id = entry.id || entry.slug || entry.name
|
|
247
|
+
if (!id) return null
|
|
248
|
+
return {
|
|
249
|
+
provider,
|
|
250
|
+
id,
|
|
251
|
+
name: entry.name || entry.display_name || id,
|
|
252
|
+
...(entry.description ? { description: entry.description } : {}),
|
|
253
|
+
...(entry.contextWindow ? { contextWindow: entry.contextWindow } : {}),
|
|
254
|
+
...(entry.inputModalities ? { inputModalities: entry.inputModalities } : {}),
|
|
255
|
+
...(entry.reasoning ? { reasoning: entry.reasoning } : {}),
|
|
256
|
+
}
|
|
257
|
+
}).filter(Boolean)
|
|
258
|
+
}
|
package/lib/oauth.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export function buildAuthorizeUrl({
|
|
2
|
+
authUrl,
|
|
3
|
+
clientId,
|
|
4
|
+
redirectUri,
|
|
5
|
+
challenge,
|
|
6
|
+
state,
|
|
7
|
+
extra,
|
|
8
|
+
scope,
|
|
9
|
+
}) {
|
|
10
|
+
const url = new URL(authUrl)
|
|
11
|
+
url.searchParams.set('response_type', 'code')
|
|
12
|
+
url.searchParams.set('client_id', clientId)
|
|
13
|
+
url.searchParams.set('redirect_uri', redirectUri)
|
|
14
|
+
url.searchParams.set('code_challenge', challenge)
|
|
15
|
+
url.searchParams.set('code_challenge_method', 'S256')
|
|
16
|
+
url.searchParams.set('state', state)
|
|
17
|
+
if (scope) url.searchParams.set('scope', scope)
|
|
18
|
+
if (extra && typeof extra === 'object') {
|
|
19
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
20
|
+
if (value == null || value === '') continue
|
|
21
|
+
url.searchParams.set(key, String(value))
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return url.toString()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function parseCallbackInput(text) {
|
|
28
|
+
const raw = String(text || '').trim()
|
|
29
|
+
if (!raw) return { code: '', state: '' }
|
|
30
|
+
if (!raw.includes('://') && !raw.includes('?')) {
|
|
31
|
+
const parts = raw.split('#')
|
|
32
|
+
return { code: parts[0], state: parts[1] || '' }
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const hashed = raw.replace('#', '?')
|
|
36
|
+
const url = new URL(hashed)
|
|
37
|
+
const code = url.searchParams.get('code') || ''
|
|
38
|
+
const state = url.searchParams.get('state') || ''
|
|
39
|
+
return { code, state }
|
|
40
|
+
} catch {
|
|
41
|
+
const match = /(?:^|[?&#])code=([^&#\s]+)/.exec(raw)
|
|
42
|
+
return { code: match ? decodeURIComponent(match[1]) : raw, state: '' }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function requestOrigin(req) {
|
|
47
|
+
const xfProto = header(req, 'x-forwarded-proto')
|
|
48
|
+
const xfHost = header(req, 'x-forwarded-host')
|
|
49
|
+
const host = xfHost || header(req, 'host') || 'localhost'
|
|
50
|
+
let proto = xfProto || 'http'
|
|
51
|
+
if (header(req, 'origin')) {
|
|
52
|
+
try { return new URL(header(req, 'origin')).origin } catch { /* fall through */ }
|
|
53
|
+
}
|
|
54
|
+
if (header(req, 'referer')) {
|
|
55
|
+
try { return new URL(header(req, 'referer')).origin } catch { /* fall through */ }
|
|
56
|
+
}
|
|
57
|
+
return `${proto}://${host}`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function header(req, name) {
|
|
61
|
+
const value = req && req.headers && req.headers[name]
|
|
62
|
+
if (Array.isArray(value)) return value[0] || ''
|
|
63
|
+
return value ? String(value) : ''
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function webCallbackUri(origin) {
|
|
67
|
+
return `${String(origin || '').replace(/\/$/, '')}/dsh-subscriptions/oauth/callback`
|
|
68
|
+
}
|
package/lib/pkce.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
function b64url(bytes) {
|
|
4
|
+
return Buffer.from(bytes).toString('base64url')
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function createPkce() {
|
|
8
|
+
const verifier = b64url(randomBytes(32))
|
|
9
|
+
const challenge = b64url(createHash('sha256').update(verifier).digest())
|
|
10
|
+
const state = b64url(randomBytes(16))
|
|
11
|
+
return { verifier, challenge, state }
|
|
12
|
+
}
|
package/lib/refs.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export const PROVIDERS = Object.freeze([
|
|
2
|
+
'codex',
|
|
3
|
+
'claude',
|
|
4
|
+
'grok',
|
|
5
|
+
'antigravity',
|
|
6
|
+
])
|
|
7
|
+
|
|
8
|
+
const KNOWN = new Set(PROVIDERS)
|
|
9
|
+
|
|
10
|
+
const DISPLAY = {
|
|
11
|
+
codex: 'ChatGPT Codex',
|
|
12
|
+
claude: 'Claude',
|
|
13
|
+
grok: 'Grok',
|
|
14
|
+
antigravity: 'Antigravity',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isProvider(value) {
|
|
18
|
+
return KNOWN.has(value)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function displayName(provider) {
|
|
22
|
+
return DISPLAY[provider] || provider
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function oauthRef(provider, index) {
|
|
26
|
+
if (!KNOWN.has(provider)) throw new Error(`unknown provider: ${provider}`)
|
|
27
|
+
const n = Number(index)
|
|
28
|
+
if (!Number.isInteger(n) || n < 1) throw new Error('account index must be an integer >= 1')
|
|
29
|
+
return `${provider.toUpperCase()}_OAUTH_${n}`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function parseOauthRef(ref) {
|
|
33
|
+
if (typeof ref !== 'string') return null
|
|
34
|
+
const match = /^(CODEX|CLAUDE|GROK|ANTIGRAVITY)_OAUTH_([1-9][0-9]*)$/.exec(ref)
|
|
35
|
+
if (!match) return null
|
|
36
|
+
return { provider: match[1].toLowerCase(), index: Number(match[2]) }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function droppedCredentialRefs(previousSlots, nextSlots) {
|
|
40
|
+
const keep = new Set()
|
|
41
|
+
for (const slot of nextSlots || []) {
|
|
42
|
+
if (!isProvider(slot.provider)) continue
|
|
43
|
+
const index = Number(slot.index)
|
|
44
|
+
if (!Number.isInteger(index) || index < 1) continue
|
|
45
|
+
keep.add(oauthRef(slot.provider, index))
|
|
46
|
+
}
|
|
47
|
+
const out = []
|
|
48
|
+
const seen = new Set()
|
|
49
|
+
for (const slot of previousSlots || []) {
|
|
50
|
+
if (!isProvider(slot.provider)) continue
|
|
51
|
+
const index = Number(slot.index)
|
|
52
|
+
if (!Number.isInteger(index) || index < 1) continue
|
|
53
|
+
const ref = oauthRef(slot.provider, index)
|
|
54
|
+
if (seen.has(ref) || keep.has(ref)) continue
|
|
55
|
+
seen.add(ref)
|
|
56
|
+
out.push(ref)
|
|
57
|
+
}
|
|
58
|
+
return out
|
|
59
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { iterateSse, jsonSse } from './sse.js'
|
|
2
|
+
import { httpError } from './wire.js'
|
|
3
|
+
|
|
4
|
+
function responsesFailure(code, message) {
|
|
5
|
+
const text = String(message || code || 'responses failed')
|
|
6
|
+
throw httpError(400, text, /quota|usage.?limit/i.test(text) ? 'QUOTA' : 'VENDOR')
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class ResponsesStreamTranslator {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.blocks = new Map()
|
|
12
|
+
this.order = []
|
|
13
|
+
this.nextIndex = 0
|
|
14
|
+
this.sawToolCall = false
|
|
15
|
+
this.terminated = false
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
open(key, kind, callId = '', name) {
|
|
19
|
+
const block = {
|
|
20
|
+
index: this.nextIndex++,
|
|
21
|
+
kind,
|
|
22
|
+
text: '',
|
|
23
|
+
callId,
|
|
24
|
+
name,
|
|
25
|
+
}
|
|
26
|
+
this.blocks.set(key, block)
|
|
27
|
+
this.order.push(block)
|
|
28
|
+
return [{ type: 'block-start', index: block.index, blockType: kind }]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
closeItem(itemId) {
|
|
32
|
+
for (const key of [...this.blocks.keys()]) {
|
|
33
|
+
if (key.startsWith(`${itemId}:`)) this.blocks.delete(key)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
push(event) {
|
|
38
|
+
if (this.terminated) return []
|
|
39
|
+
const chunks = []
|
|
40
|
+
switch (event.type) {
|
|
41
|
+
case 'response.output_item.added': {
|
|
42
|
+
const item = event.item
|
|
43
|
+
if (item?.type === 'function_call' && item.id !== undefined) {
|
|
44
|
+
this.sawToolCall = true
|
|
45
|
+
const callId = item.call_id ?? ''
|
|
46
|
+
const start = this.open(`${item.id}:call`, 'tool-call', callId, item.name)
|
|
47
|
+
chunks.push(...start)
|
|
48
|
+
const block = this.blocks.get(`${item.id}:call`)
|
|
49
|
+
chunks.push({
|
|
50
|
+
type: 'tool-call-delta',
|
|
51
|
+
index: block.index,
|
|
52
|
+
id: callId,
|
|
53
|
+
...(item.name === undefined ? {} : { name: item.name }),
|
|
54
|
+
argumentsDelta: '',
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
return chunks
|
|
58
|
+
}
|
|
59
|
+
case 'response.output_text.delta': {
|
|
60
|
+
const key = `${event.item_id ?? ''}:text:${String(event.content_index ?? 0)}`
|
|
61
|
+
if (!this.blocks.has(key)) chunks.push(...this.open(key, 'text'))
|
|
62
|
+
const block = this.blocks.get(key)
|
|
63
|
+
const delta = event.delta ?? ''
|
|
64
|
+
block.text += delta
|
|
65
|
+
chunks.push({ type: 'text-delta', index: block.index, text: delta })
|
|
66
|
+
return chunks
|
|
67
|
+
}
|
|
68
|
+
case 'response.reasoning_summary_text.delta':
|
|
69
|
+
case 'response.reasoning_text.delta': {
|
|
70
|
+
const sub = event.summary_index ?? event.content_index ?? 0
|
|
71
|
+
const key = `${event.item_id ?? ''}:reason:${String(sub)}`
|
|
72
|
+
if (!this.blocks.has(key)) chunks.push(...this.open(key, 'reasoning'))
|
|
73
|
+
const block = this.blocks.get(key)
|
|
74
|
+
const delta = event.delta ?? ''
|
|
75
|
+
block.text += delta
|
|
76
|
+
chunks.push({ type: 'reasoning-delta', index: block.index, text: delta })
|
|
77
|
+
return chunks
|
|
78
|
+
}
|
|
79
|
+
case 'response.function_call_arguments.delta': {
|
|
80
|
+
const key = `${event.item_id ?? ''}:call`
|
|
81
|
+
if (!this.blocks.has(key)) {
|
|
82
|
+
this.sawToolCall = true
|
|
83
|
+
chunks.push(...this.open(key, 'tool-call'))
|
|
84
|
+
}
|
|
85
|
+
const block = this.blocks.get(key)
|
|
86
|
+
const delta = event.delta ?? ''
|
|
87
|
+
block.text += delta
|
|
88
|
+
chunks.push({
|
|
89
|
+
type: 'tool-call-delta',
|
|
90
|
+
index: block.index,
|
|
91
|
+
id: block.callId,
|
|
92
|
+
...(block.name === undefined ? {} : { name: block.name }),
|
|
93
|
+
argumentsDelta: delta,
|
|
94
|
+
})
|
|
95
|
+
return chunks
|
|
96
|
+
}
|
|
97
|
+
case 'response.output_item.done': {
|
|
98
|
+
const item = event.item
|
|
99
|
+
if (item === undefined || item.id === undefined) return chunks
|
|
100
|
+
if (item.type === 'function_call') {
|
|
101
|
+
const key = `${item.id}:call`
|
|
102
|
+
const block = this.blocks.get(key)
|
|
103
|
+
if (block !== undefined && block.text.length === 0 && item.arguments !== undefined) {
|
|
104
|
+
block.text = item.arguments
|
|
105
|
+
chunks.push({
|
|
106
|
+
type: 'tool-call-delta',
|
|
107
|
+
index: block.index,
|
|
108
|
+
id: block.callId,
|
|
109
|
+
...(block.name === undefined ? {} : { name: block.name }),
|
|
110
|
+
argumentsDelta: item.arguments,
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
this.blocks.delete(key)
|
|
114
|
+
} else if (item.type === 'message') {
|
|
115
|
+
const hasText = [...this.blocks.keys()].some((key) => key.startsWith(`${item.id}:text:`))
|
|
116
|
+
if (!hasText) {
|
|
117
|
+
for (const [partIndex, part] of (item.content ?? []).entries()) {
|
|
118
|
+
if (part?.type !== 'output_text' || typeof part.text !== 'string' || part.text.length === 0) continue
|
|
119
|
+
const key = `${item.id}:text:${partIndex}`
|
|
120
|
+
chunks.push(...this.open(key, 'text'))
|
|
121
|
+
const block = this.blocks.get(key)
|
|
122
|
+
block.text = part.text
|
|
123
|
+
chunks.push({ type: 'text-delta', index: block.index, text: part.text })
|
|
124
|
+
this.blocks.delete(key)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
this.closeItem(item.id)
|
|
128
|
+
} else {
|
|
129
|
+
this.closeItem(item.id)
|
|
130
|
+
}
|
|
131
|
+
return chunks
|
|
132
|
+
}
|
|
133
|
+
case 'response.completed': {
|
|
134
|
+
this.terminated = true
|
|
135
|
+
const usage = event.response?.usage
|
|
136
|
+
if (usage !== undefined) chunks.push({ type: 'usage', usage })
|
|
137
|
+
chunks.push({ type: 'finish', reason: { kind: this.sawToolCall ? 'tool-calls' : 'stop' } })
|
|
138
|
+
return chunks
|
|
139
|
+
}
|
|
140
|
+
case 'response.failed':
|
|
141
|
+
responsesFailure(event.response?.error?.code, event.response?.error?.message)
|
|
142
|
+
break
|
|
143
|
+
case 'response.incomplete':
|
|
144
|
+
responsesFailure(event.response?.incomplete_details?.reason, event.response?.error?.message
|
|
145
|
+
|| `incomplete response (${event.response?.incomplete_details?.reason ?? 'unknown'})`)
|
|
146
|
+
break
|
|
147
|
+
case 'error':
|
|
148
|
+
responsesFailure(event.code, event.message)
|
|
149
|
+
break
|
|
150
|
+
default:
|
|
151
|
+
return chunks
|
|
152
|
+
}
|
|
153
|
+
return chunks
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function* streamResponses(body) {
|
|
158
|
+
const translator = new ResponsesStreamTranslator()
|
|
159
|
+
for await (const data of iterateSse(body)) {
|
|
160
|
+
const json = jsonSse(data)
|
|
161
|
+
if (!json) continue
|
|
162
|
+
const type = json.type || ''
|
|
163
|
+
if (type === 'response.failed' || type === 'error') {
|
|
164
|
+
const msg = (json.response && json.response.error && json.response.error.message)
|
|
165
|
+
|| json.message || json.error || 'responses failed'
|
|
166
|
+
const text = typeof msg === 'string' ? msg : JSON.stringify(msg)
|
|
167
|
+
throw httpError(json.status || 400, text, /quota|usage.?limit/i.test(text) ? 'QUOTA' : 'VENDOR')
|
|
168
|
+
}
|
|
169
|
+
for (const chunk of translator.push(json)) yield chunk
|
|
170
|
+
if (translator.terminated) return
|
|
171
|
+
}
|
|
172
|
+
if (!translator.terminated) {
|
|
173
|
+
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
174
|
+
}
|
|
175
|
+
}
|
package/lib/rotate.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const SWITCH_CODES = new Set([
|
|
2
|
+
'RATE_LIMIT',
|
|
3
|
+
'QUOTA',
|
|
4
|
+
'QUOTA_EXCEEDED',
|
|
5
|
+
])
|
|
6
|
+
|
|
7
|
+
export function isSwitchableError(err) {
|
|
8
|
+
if (!err || typeof err !== 'object') return false
|
|
9
|
+
const code = err.code || (err.failure && err.failure.code)
|
|
10
|
+
if (SWITCH_CODES.has(code)) return true
|
|
11
|
+
const status = err.status || err.statusCode
|
|
12
|
+
return status === 429
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function pickAccount(accounts, nowMs) {
|
|
16
|
+
const list = Array.isArray(accounts) ? accounts : []
|
|
17
|
+
const now = Number(nowMs) || 0
|
|
18
|
+
for (const account of list) {
|
|
19
|
+
if (!account || !account.hasToken) continue
|
|
20
|
+
if (account.usagePercent != null && Number(account.usagePercent) >= 100) continue
|
|
21
|
+
if (account.cooldownUntil && Number(account.cooldownUntil) > now) continue
|
|
22
|
+
return account
|
|
23
|
+
}
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function markCooldown(account, nowMs, cooldownMs) {
|
|
28
|
+
const wait = Number(cooldownMs)
|
|
29
|
+
const ms = Number.isFinite(wait) && wait > 0 ? wait : 30 * 60 * 1000
|
|
30
|
+
return { ...account, cooldownUntil: (Number(nowMs) || 0) + ms }
|
|
31
|
+
}
|
package/lib/sse.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export async function* iterateSse(body) {
|
|
2
|
+
const reader = body && typeof body.getReader === 'function' ? body.getReader() : null
|
|
3
|
+
const decoder = new TextDecoder()
|
|
4
|
+
let buffer = ''
|
|
5
|
+
async function* fromText(chunk) {
|
|
6
|
+
buffer += chunk
|
|
7
|
+
let sep
|
|
8
|
+
while ((sep = buffer.search(/\r?\n\r?\n/)) >= 0) {
|
|
9
|
+
const raw = buffer.slice(0, sep)
|
|
10
|
+
buffer = buffer.slice(sep).replace(/^\r?\n\r?\n/, '')
|
|
11
|
+
const dataLines = []
|
|
12
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
13
|
+
if (line.startsWith('data:')) dataLines.push(line.slice(5).trimStart())
|
|
14
|
+
}
|
|
15
|
+
if (!dataLines.length) continue
|
|
16
|
+
const data = dataLines.join('\n')
|
|
17
|
+
if (data === '[DONE]') return
|
|
18
|
+
yield data
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (reader) {
|
|
22
|
+
while (true) {
|
|
23
|
+
const { done, value } = await reader.read()
|
|
24
|
+
if (done) break
|
|
25
|
+
yield* fromText(decoder.decode(value, { stream: true }))
|
|
26
|
+
}
|
|
27
|
+
yield* fromText(decoder.decode())
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
if (body && typeof body[Symbol.asyncIterator] === 'function') {
|
|
31
|
+
for await (const chunk of body) {
|
|
32
|
+
const text = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true })
|
|
33
|
+
yield* fromText(text)
|
|
34
|
+
}
|
|
35
|
+
yield* fromText(decoder.decode())
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
const text = typeof body === 'string' ? body : await new Response(body).text()
|
|
39
|
+
yield* fromText(text)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function jsonSse(data) {
|
|
43
|
+
try { return JSON.parse(data) } catch { return null }
|
|
44
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { pickAccount, markCooldown, isSwitchableError } from './rotate.js'
|
|
2
|
+
|
|
3
|
+
export async function* streamWithRotation({
|
|
4
|
+
accounts,
|
|
5
|
+
nowMs,
|
|
6
|
+
cooldownMs,
|
|
7
|
+
streamOnce,
|
|
8
|
+
options,
|
|
9
|
+
onCooldown,
|
|
10
|
+
}) {
|
|
11
|
+
const pool = (accounts || []).map((account) => ({ ...account }))
|
|
12
|
+
let lastError = null
|
|
13
|
+
const tried = new Set()
|
|
14
|
+
while (true) {
|
|
15
|
+
const account = pickAccount(pool, nowMs())
|
|
16
|
+
if (!account) {
|
|
17
|
+
if (lastError) throw lastError
|
|
18
|
+
const err = new Error('no usable subscription account for this provider')
|
|
19
|
+
err.code = 'AUTH'
|
|
20
|
+
throw err
|
|
21
|
+
}
|
|
22
|
+
if (tried.has(account.ref)) {
|
|
23
|
+
if (lastError) throw lastError
|
|
24
|
+
const err = new Error('all subscription accounts failed')
|
|
25
|
+
err.code = 'RATE_LIMIT'
|
|
26
|
+
throw err
|
|
27
|
+
}
|
|
28
|
+
tried.add(account.ref)
|
|
29
|
+
try {
|
|
30
|
+
yield* streamOnce(account, options)
|
|
31
|
+
return
|
|
32
|
+
} catch (err) {
|
|
33
|
+
lastError = err
|
|
34
|
+
if (!isSwitchableError(err)) throw err
|
|
35
|
+
const cooled = markCooldown(account, nowMs(), cooldownMs)
|
|
36
|
+
account.cooldownUntil = cooled.cooldownUntil
|
|
37
|
+
if (onCooldown) onCooldown(account)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|