@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.
@@ -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
+