@goodandready/dsh-subscriptions 0.6.39 → 0.6.41

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.
@@ -79,7 +79,7 @@ export async function* withRunawayGuard(stream, options = {}) {
79
79
  for await (const chunk of stream) {
80
80
  if (chunk && chunk.type === 'finish') {
81
81
  yield chunk
82
- return
82
+ continue
83
83
  }
84
84
 
85
85
  const text = (chunk && typeof chunk === 'object')
@@ -88,6 +88,13 @@ export async function* withRunawayGuard(stream, options = {}) {
88
88
 
89
89
  if (text && (chunk.type === 'text-delta' || chunk.type === 'reasoning-delta' || !chunk.type)) {
90
90
  if (detector.feed(text)) {
91
+ if (options && typeof options === 'object') {
92
+ options.tripped = true
93
+ options.trippedReason = detector.getReason()
94
+ if (typeof options.onRunaway === 'function') {
95
+ options.onRunaway(detector.getReason())
96
+ }
97
+ }
91
98
  yield {
92
99
  type: 'finish',
93
100
  reason: {
package/lib/sse.js CHANGED
@@ -2,7 +2,10 @@ export async function* iterateSse(body, { idleTimeoutMs = 60000, signal } = {})
2
2
  const reader = body && typeof body.getReader === 'function' ? body.getReader() : null
3
3
  const decoder = new TextDecoder()
4
4
  let buffer = ''
5
+ let terminated = false
6
+
5
7
  async function* fromText(chunk) {
8
+ if (terminated) return
6
9
  buffer += chunk
7
10
  let sep
8
11
  while ((sep = buffer.search(/\r?\n\r?\n/)) >= 0) {
@@ -10,12 +13,16 @@ export async function* iterateSse(body, { idleTimeoutMs = 60000, signal } = {})
10
13
  buffer = buffer.slice(sep).replace(/^\r?\n\r?\n/, '')
11
14
  const dataLines = []
12
15
  for (const line of raw.split(/\r?\n/)) {
13
- if (line.startsWith(":")) continue // SSE comments / keep-alive pings
16
+ if (line.startsWith(':')) continue // SSE comments / keep-alive pings
14
17
  if (line.startsWith('data:')) dataLines.push(line.slice(5).trimStart())
15
18
  }
16
19
  if (!dataLines.length) continue
17
20
  const data = dataLines.join('\n')
18
- if (data === '[DONE]') return
21
+ if (data === '[DONE]') {
22
+ terminated = true
23
+ yield '[DONE]'
24
+ return
25
+ }
19
26
  yield data
20
27
  }
21
28
  }
@@ -36,7 +43,7 @@ export async function* iterateSse(body, { idleTimeoutMs = 60000, signal } = {})
36
43
  reject(err)
37
44
  }, idleTimeoutMs)
38
45
  })
39
-
46
+
40
47
  let abortPromise
41
48
  let abortHandler
42
49
  if (signal) {
@@ -64,12 +71,15 @@ export async function* iterateSse(body, { idleTimeoutMs = 60000, signal } = {})
64
71
 
65
72
  if (reader) {
66
73
  try {
67
- while (true) {
74
+ while (!terminated) {
68
75
  const { done, value } = await readWithTimeout(reader)
69
76
  if (done) break
70
77
  yield* fromText(decoder.decode(value, { stream: true }))
78
+ if (terminated) break
79
+ }
80
+ if (!terminated) {
81
+ yield* fromText(decoder.decode())
71
82
  }
72
- yield* fromText(decoder.decode())
73
83
  } finally {
74
84
  if (typeof reader.cancel === 'function') await reader.cancel().catch(() => {})
75
85
  }
@@ -77,11 +87,19 @@ export async function* iterateSse(body, { idleTimeoutMs = 60000, signal } = {})
77
87
  }
78
88
  if (body && typeof body[Symbol.asyncIterator] === 'function') {
79
89
  for await (const chunk of body) {
80
- if (signal && signal.aborted) break
90
+ if (signal && signal.aborted) {
91
+ const err = new Error('stream aborted by client')
92
+ err.name = 'AbortError'
93
+ throw err
94
+ }
95
+ if (terminated) break
81
96
  const text = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true })
82
97
  yield* fromText(text)
98
+ if (terminated) break
99
+ }
100
+ if (!terminated) {
101
+ yield* fromText(decoder.decode())
83
102
  }
84
- yield* fromText(decoder.decode())
85
103
  return
86
104
  }
87
105
  const text = typeof body === 'string' ? body : await new Response(body).text()
@@ -53,7 +53,18 @@ export async function* streamWithRotation({
53
53
 
54
54
  while (true) {
55
55
  try {
56
- const guardedStream = withRunawayGuard(streamOnce(account, options), options && options.runawayGuard)
56
+ const runawayState = options && options._runawayState ? options._runawayState : { tripped: false, reason: null }
57
+ const runawayOpts = {
58
+ ...(options && options.runawayGuard),
59
+ onRunaway: (reason) => {
60
+ runawayState.tripped = true
61
+ runawayState.reason = reason
62
+ },
63
+ }
64
+ const guardedStream = withRunawayGuard(
65
+ streamOnce(account, { ...options, _runawayState: runawayState }),
66
+ runawayOpts
67
+ )
57
68
  for await (const chunk of guardedStream) {
58
69
  firstChunkDelivered = true
59
70
  yield chunk
@@ -1,40 +1,44 @@
1
1
  export function sanitizeToolName(name) {
2
- if (typeof name !== 'string' || !name.trim()) return 'tool_fn'
3
- const sanitized = name.trim().replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64)
4
- return sanitized || 'tool_fn'
2
+ if (typeof name !== "string" || !name.trim()) return "tool_fn"
3
+ const sanitized = name.trim().replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64)
4
+ return sanitized || "tool_fn"
5
5
  }
6
6
 
7
7
  export function sanitizeDescription(desc) {
8
- if (typeof desc === 'string') return desc.trim()
9
- if (desc == null) return ''
8
+ if (typeof desc === "string") return desc.trim()
9
+ if (desc == null) return ""
10
10
  return String(desc).trim()
11
11
  }
12
12
 
13
13
  export function normalizeJsonSchema(schema, depth = 0) {
14
- if (depth > 12) return { type: 'string' }
15
- if (!schema || typeof schema !== 'object') {
16
- return { type: 'object' }
14
+ if (depth > 12) return { type: "string" }
15
+ if (!schema || typeof schema !== "object") {
16
+ return { type: "object" }
17
17
  }
18
18
  if (Array.isArray(schema)) {
19
- return normalizeJsonSchema(schema[0] || { type: 'object' }, depth + 1)
19
+ return normalizeJsonSchema(schema[0] || { type: "object" }, depth + 1)
20
20
  }
21
21
 
22
22
  const out = {}
23
23
 
24
+ if (schema.title) out.title = String(schema.title)
25
+
24
26
  let type = schema.type
25
27
  let nullable = false
26
28
  if (Array.isArray(type)) {
27
- nullable = type.includes('null')
28
- type = type.find((t) => t && t !== 'null') || 'string'
29
+ nullable = type.includes("null")
30
+ type = type.find((t) => t && t !== "null") || "string"
29
31
  }
30
- if (typeof type === 'string') {
32
+ if (typeof type === "string") {
31
33
  out.type = type.toLowerCase()
32
34
  } else if (schema.properties) {
33
- out.type = 'object'
35
+ out.type = "object"
34
36
  } else if (schema.items) {
35
- out.type = 'array'
37
+ out.type = "array"
38
+ } else if (schema.anyOf || schema.oneOf || schema.allOf) {
39
+ // Schema combinator without explicit type
36
40
  } else {
37
- out.type = depth === 0 ? 'object' : 'string'
41
+ out.type = depth === 0 ? "object" : "string"
38
42
  }
39
43
 
40
44
  if (nullable) {
@@ -45,51 +49,74 @@ export function normalizeJsonSchema(schema, depth = 0) {
45
49
  out.description = String(schema.description)
46
50
  }
47
51
 
48
- if (out.type === 'object') {
49
- if (schema.properties && typeof schema.properties === 'object' && !Array.isArray(schema.properties)) {
52
+ if (schema.default !== undefined) {
53
+ out.default = schema.default
54
+ }
55
+
56
+ if (out.type === "object" || (!out.type && (schema.properties || schema.required || schema.additionalProperties !== undefined))) {
57
+ if (schema.properties && typeof schema.properties === "object" && !Array.isArray(schema.properties)) {
50
58
  out.properties = {}
51
59
  for (const [key, val] of Object.entries(schema.properties)) {
52
- if (typeof key === 'string' && key) {
60
+ if (typeof key === "string" && key) {
53
61
  out.properties[key] = normalizeJsonSchema(val, depth + 1)
54
62
  }
55
63
  }
56
64
  }
57
65
 
58
66
  if (Array.isArray(schema.required)) {
59
- const propKeys = new Set(Object.keys(out.properties))
67
+ const propKeys = out.properties ? new Set(Object.keys(out.properties)) : null
60
68
  const reqs = schema.required
61
- .filter((k) => typeof k === 'string' && k.length > 0)
62
- .filter((k) => propKeys.size === 0 || propKeys.has(k))
69
+ .filter((k) => typeof k === "string" && k.length > 0)
70
+ .filter((k) => !propKeys || propKeys.size === 0 || propKeys.has(k))
63
71
  if (reqs.length > 0) {
64
72
  out.required = [...new Set(reqs)]
65
73
  }
66
74
  }
67
75
 
68
- if (typeof schema.additionalProperties === 'boolean') {
76
+ if (typeof schema.additionalProperties === "boolean") {
69
77
  out.additionalProperties = schema.additionalProperties
78
+ } else if (typeof schema.additionalProperties === "object" && schema.additionalProperties !== null) {
79
+ out.additionalProperties = normalizeJsonSchema(schema.additionalProperties, depth + 1)
70
80
  }
71
- } else if (out.type === 'array') {
81
+ } else if (out.type === "array" || (!out.type && schema.items)) {
72
82
  if (schema.items) {
73
83
  out.items = normalizeJsonSchema(
74
84
  Array.isArray(schema.items) ? schema.items[0] : schema.items,
75
85
  depth + 1,
76
86
  )
77
87
  } else {
78
- out.items = { type: 'string' }
88
+ out.items = { type: "string" }
79
89
  }
80
90
  }
81
91
 
92
+ if (Array.isArray(schema.anyOf)) {
93
+ out.anyOf = schema.anyOf.map((s) => normalizeJsonSchema(s, depth + 1))
94
+ }
95
+ if (Array.isArray(schema.oneOf)) {
96
+ out.oneOf = schema.oneOf.map((s) => normalizeJsonSchema(s, depth + 1))
97
+ }
98
+ if (Array.isArray(schema.allOf)) {
99
+ out.allOf = schema.allOf.map((s) => normalizeJsonSchema(s, depth + 1))
100
+ }
101
+
82
102
  if (Array.isArray(schema.enum) && schema.enum.length > 0) {
83
103
  const cleaned = [...new Set(
84
104
  schema.enum
85
- .map((item) => (item != null ? String(item).trim() : ''))
86
- .filter((item) => item.length > 0),
105
+ .filter((v) => v !== undefined && v !== null)
106
+ .map((v) => (typeof v === "string" ? v.trim() : v))
107
+ .filter((v) => (typeof v === "string" ? v.length > 0 : true))
87
108
  )]
88
109
  if (cleaned.length > 0) {
89
110
  out.enum = cleaned
90
111
  }
91
112
  }
92
113
 
114
+ if (typeof schema.pattern === "string") out.pattern = schema.pattern
115
+ if (typeof schema.minimum === "number") out.minimum = schema.minimum
116
+ if (typeof schema.maximum === "number") out.maximum = schema.maximum
117
+ if (typeof schema.minLength === "number") out.minLength = schema.minLength
118
+ if (typeof schema.maxLength === "number") out.maxLength = schema.maxLength
119
+
93
120
  return out
94
121
  }
95
122
 
@@ -98,7 +125,7 @@ export function normalizeTools(tools) {
98
125
  const validTools = []
99
126
 
100
127
  for (const tool of tools) {
101
- if (!tool || typeof tool !== 'object') continue
128
+ if (!tool || typeof tool !== "object") continue
102
129
  const rawName = tool.name || (tool.function && tool.function.name)
103
130
  if (!rawName) continue
104
131
 
@@ -13,8 +13,8 @@ import { codexResponsesBody, modelCatalog } from "./messages.js"
13
13
  import { formTokenRequest, jsonTokenRequest, codexResponsesStream, readJson, tokenBlobFromOAuth, httpError } from "./wire.js"
14
14
  import { emailFromToken } from "./jwt.js"
15
15
 
16
- function defaultModels(models) {
17
- return modelCatalog("custom", (models || []).map((row) => (typeof row === "string" ? { id: row, name: row } : row)))
16
+ function defaultModels(providerId, models) {
17
+ return modelCatalog(providerId, (models || []).map((row) => (typeof row === "string" ? { id: row, name: row } : row)))
18
18
  }
19
19
 
20
20
  /**
@@ -84,7 +84,7 @@ export function createVendorFromProfile(profile) {
84
84
  return tokenBlobFromOAuth(json, { label: blob.label, email: blob.email })
85
85
  },
86
86
  async listModels(blob, cfg, fetchImpl) {
87
- const catalog = defaultModels(cfg.models || profile.models || [])
87
+ const catalog = defaultModels(id, cfg.models || profile.models || [])
88
88
  if (!profile.modelsPath) return catalog
89
89
  try {
90
90
  const res = await fetchWithTimeout(fetchImpl || fetch, `${base}${profile.modelsPath}`, {
@@ -1,7 +1,6 @@
1
1
  import { LlmError } from '@deepseek-ai/dsh-llm'
2
2
  import { openaiMessages } from '../messages.js'
3
3
  import { openaiChatStream, httpError } from '../wire.js'
4
- import { asUsageSnapshot } from '../usage.js'
5
4
 
6
5
  export const id = 'cody'
7
6
  const CODY_API_BASE = 'https://sourcegraph.com/.api'
@@ -42,9 +41,7 @@ export async function listModels() {
42
41
 
43
42
  export async function usage(blob) {
44
43
  if (blob && (blob.accessToken || blob.token)) {
45
- const snap = asUsageSnapshot(15)
46
- if (snap) snap.plan = 'Cody Pro'
47
- return snap
44
+ return { usedPercent: null, plan: 'Cody Pro', timestamp: Date.now() }
48
45
  }
49
46
  return null
50
47
  }
@@ -2,7 +2,6 @@ import { LlmError } from "@deepseek-ai/dsh-llm"
2
2
  import { openaiMessages, openaiTools, modelCatalog } from "../messages.js"
3
3
  import { fetchWithTimeout } from "../http.js"
4
4
  import { openaiChatStream, readJson, httpError } from "../wire.js"
5
- import { asUsageSnapshot } from "../usage.js"
6
5
 
7
6
  export const id = "copilot"
8
7
 
@@ -268,9 +267,14 @@ export async function usage(blob, config, fetchImpl) {
268
267
  })
269
268
  if (!res.ok) return null
270
269
  const user = await readJson(res)
271
- const snap = asUsageSnapshot(10)
272
- if (snap) snap.plan = user && user.plan ? user.plan.name : "Copilot"
273
- return snap
270
+ const usedPercent = user && typeof user.used_percent === "number" && Number.isFinite(user.used_percent)
271
+ ? user.used_percent
272
+ : null
273
+ return {
274
+ usedPercent,
275
+ plan: user && user.plan ? (user.plan.name || String(user.plan)) : "Copilot",
276
+ timestamp: Date.now(),
277
+ }
274
278
  } catch {
275
279
  return null
276
280
  }
@@ -1,7 +1,6 @@
1
1
  import { LlmError } from '@deepseek-ai/dsh-llm'
2
2
  import { openaiMessages } from '../messages.js'
3
3
  import { openaiChatStream, readJson, httpError } from '../wire.js'
4
- import { asUsageSnapshot } from '../usage.js'
5
4
 
6
5
  export const id = 'ernie'
7
6
  export const ERNIE_OAUTH_URL = 'https://aip.baidubce.com/oauth/2.0/token'
@@ -51,10 +50,7 @@ async function refreshErnieToken(apiKey, secretKey, fetchImpl) {
51
50
  return readJson(res)
52
51
  }
53
52
 
54
- export async function usage(blob) {
55
- if (blob && (blob.accessToken || blob.apiKey)) {
56
- return asUsageSnapshot(20)
57
- }
53
+ export async function usage() {
58
54
  return null
59
55
  }
60
56
 
@@ -1,7 +1,6 @@
1
1
  import { LlmError } from '@deepseek-ai/dsh-llm'
2
2
  import { openaiMessages } from '../messages.js'
3
3
  import { openaiChatStream, httpError } from '../wire.js'
4
- import { asUsageSnapshot } from '../usage.js'
5
4
 
6
5
  export const id = 'jetbrains'
7
6
  const JETBRAINS_API_BASE = 'https://api.jetbrains.ai'
@@ -42,10 +41,7 @@ export async function listModels() {
42
41
  return JETBRAINS_MODELS
43
42
  }
44
43
 
45
- export async function usage(blob) {
46
- if (blob && (blob.accessToken || blob.token)) {
47
- return asUsageSnapshot(30)
48
- }
44
+ export async function usage() {
49
45
  return null
50
46
  }
51
47
 
@@ -1,7 +1,6 @@
1
1
  import { LlmError } from '@deepseek-ai/dsh-llm'
2
2
  import { openaiMessages } from '../messages.js'
3
3
  import { openaiChatStream, httpError } from '../wire.js'
4
- import { asUsageSnapshot } from '../usage.js'
5
4
 
6
5
  export const id = 'perplexity'
7
6
  const PERPLEXITY_API_BASE = 'https://api.perplexity.ai'
@@ -44,9 +43,7 @@ export async function listModels() {
44
43
 
45
44
  export async function usage(blob) {
46
45
  if (blob && (blob.accessToken || blob.apiKey)) {
47
- const snap = asUsageSnapshot(18)
48
- if (snap) snap.plan = 'Perplexity Pro'
49
- return snap
46
+ return { usedPercent: null, plan: 'Perplexity Pro', timestamp: Date.now() }
50
47
  }
51
48
  return null
52
49
  }
@@ -1,7 +1,6 @@
1
1
  import { LlmError } from '@deepseek-ai/dsh-llm'
2
2
  import { openaiMessages, openaiTools } from '../messages.js'
3
3
  import { openaiChatStream, httpError } from '../wire.js'
4
- import { asUsageSnapshot } from '../usage.js'
5
4
 
6
5
  export const id = 'qwen'
7
6
  const QWEN_API_BASE = 'https://dashscope.aliyuncs.com/compatible-mode/v1'
@@ -49,10 +48,7 @@ export async function listModels() {
49
48
  return QWEN_MODELS
50
49
  }
51
50
 
52
- export async function usage(blob) {
53
- if (blob && (blob.accessToken || blob.apiKey)) {
54
- return asUsageSnapshot(15)
55
- }
51
+ export async function usage() {
56
52
  return null
57
53
  }
58
54
 
@@ -1,7 +1,6 @@
1
1
  import { LlmError } from '@deepseek-ai/dsh-llm'
2
2
  import { openaiMessages } from '../messages.js'
3
3
  import { openaiChatStream, httpError } from '../wire.js'
4
- import { asUsageSnapshot } from '../usage.js'
5
4
 
6
5
  export const id = 'replit'
7
6
  const REPLIT_API_BASE = 'https://replit.com/api/v1/ai'
@@ -37,9 +36,7 @@ export async function listModels() {
37
36
 
38
37
  export async function usage(blob) {
39
38
  if (blob && (blob.accessToken || blob.token)) {
40
- const snap = asUsageSnapshot(25)
41
- if (snap) snap.plan = 'Replit Core'
42
- return snap
39
+ return { usedPercent: null, plan: 'Replit Core', timestamp: Date.now() }
43
40
  }
44
41
  return null
45
42
  }
@@ -1,7 +1,6 @@
1
1
  import { LlmError } from '@deepseek-ai/dsh-llm'
2
2
  import { openaiMessages } from '../messages.js'
3
3
  import { openaiChatStream, httpError } from '../wire.js'
4
- import { asUsageSnapshot } from '../usage.js'
5
4
 
6
5
  export const id = 'spark'
7
6
  const SPARK_API_BASE = 'https://spark-api-open.xf-yun.com/v1'
@@ -42,10 +41,7 @@ export async function listModels() {
42
41
  return SPARK_MODELS
43
42
  }
44
43
 
45
- export async function usage(blob) {
46
- if (blob && (blob.accessToken || blob.apiKey)) {
47
- return asUsageSnapshot(12)
48
- }
44
+ export async function usage() {
49
45
  return null
50
46
  }
51
47
 
package/lib/wire.js CHANGED
@@ -162,7 +162,12 @@ export async function* openaiChatStream(body) {
162
162
  let textBlock = null
163
163
  const tools = new Map()
164
164
  let finish = { kind: 'stop' }
165
+ let sawTerminal = false
165
166
  for await (const data of iterateSse(body)) {
167
+ if (data === '[DONE]') {
168
+ sawTerminal = true
169
+ break
170
+ }
166
171
  const json = jsonSse(data)
167
172
  if (!json) continue
168
173
  const choice = (json.choices && json.choices[0]) || {}
@@ -196,9 +201,19 @@ export async function* openaiChatStream(body) {
196
201
  }
197
202
  }
198
203
  if (json.usage) yield { type: 'usage', usage: json.usage }
199
- if (choice.finish_reason === 'length') finish = { kind: 'length' }
200
- else if (choice.finish_reason === 'tool_calls') finish = { kind: 'tool' }
204
+ if (choice.finish_reason) {
205
+ sawTerminal = true
206
+ if (choice.finish_reason === 'length') finish = { kind: 'max-tokens' }
207
+ else if (choice.finish_reason === 'tool_calls' || choice.finish_reason === 'function_call') finish = { kind: 'tool-calls' }
208
+ else if (choice.finish_reason === 'content_filter') finish = { kind: 'error', failure: { code: 'CONTENT_FILTER', message: 'content filter triggered' } }
209
+ else if (choice.finish_reason === 'stop') finish = { kind: 'stop' }
210
+ }
211
+ }
212
+ if (!sawTerminal && (textBlock !== null || tools.size > 0)) {
213
+ yield { type: 'finish', reason: { kind: 'error', failure: { code: 'INCOMPLETE', message: 'stream terminated unexpectedly before finish_reason or terminal marker' } } }
214
+ return
201
215
  }
216
+ if (finish.kind === 'stop' && tools.size > 0) finish = { kind: 'tool-calls' }
202
217
  yield { type: 'finish', reason: finish }
203
218
  }
204
219
 
@@ -208,10 +223,20 @@ export async function* anthropicStream(body) {
208
223
  let index = 0
209
224
  let textIndex = 0
210
225
  const toolCallIds = new Map()
226
+ let finish = { kind: 'stop' }
227
+ let sawTerminal = false
228
+ let usageState = null
229
+ let yieldedUsage = false
211
230
  for await (const data of iterateSse(body)) {
231
+ if (data === '[DONE]') {
232
+ sawTerminal = true
233
+ break
234
+ }
212
235
  const json = jsonSse(data)
213
236
  if (!json) continue
214
- if (json.type === 'content_block_start') {
237
+ if (json.type === 'message_start' && json.message && json.message.usage) {
238
+ usageState = { ...(usageState || {}), ...json.message.usage }
239
+ } else if (json.type === 'content_block_start') {
215
240
  const block = json.content_block || {}
216
241
  if (block.type === 'text') {
217
242
  textIndex = json.index ?? index++
@@ -243,17 +268,54 @@ export async function* anthropicStream(body) {
243
268
  argumentsDelta: delta.partial_json,
244
269
  }
245
270
  }
246
- } else if (json.type === 'message_delta' && json.usage) {
247
- yield {
248
- type: 'usage',
249
- usage: {
250
- input: Number(json.usage.input_tokens ?? json.usage.input ?? 0) || 0,
251
- output: Number(json.usage.output_tokens ?? json.usage.output ?? 0) || 0,
252
- },
271
+ } else if (json.type === 'message_delta') {
272
+ sawTerminal = true
273
+ if (json.delta && json.delta.stop_reason) {
274
+ const sr = json.delta.stop_reason
275
+ if (sr === 'tool_use') finish = { kind: 'tool-calls' }
276
+ else if (sr === 'max_tokens') finish = { kind: 'max-tokens' }
277
+ else finish = { kind: 'stop' }
278
+ }
279
+ if (json.usage) {
280
+ usageState = { ...(usageState || {}), ...json.usage }
281
+ }
282
+ if (usageState) {
283
+ const norm = toTokenUsage(usageState)
284
+ yieldedUsage = true
285
+ yield {
286
+ type: 'usage',
287
+ usage: {
288
+ ...usageState,
289
+ ...(norm || {}),
290
+ input: norm ? norm.inputTokens : (Number(usageState.input_tokens ?? usageState.input ?? 0) || 0),
291
+ output: norm ? norm.outputTokens : (Number(usageState.output_tokens ?? usageState.output ?? 0) || 0),
292
+ },
293
+ }
253
294
  }
295
+ } else if (json.type === 'error') {
296
+ sawTerminal = true
297
+ const err = json.error || {}
298
+ finish = { kind: 'error', failure: { code: String(err.type || 'ANTHROPIC_ERROR'), message: String(err.message || 'anthropic error') } }
299
+ }
300
+ }
301
+ if (usageState && !yieldedUsage) {
302
+ const norm = toTokenUsage(usageState)
303
+ yield {
304
+ type: 'usage',
305
+ usage: {
306
+ ...usageState,
307
+ ...(norm || {}),
308
+ input: norm ? norm.inputTokens : (Number(usageState.input_tokens ?? usageState.input ?? 0) || 0),
309
+ output: norm ? norm.outputTokens : (Number(usageState.output_tokens ?? usageState.output ?? 0) || 0),
310
+ },
254
311
  }
255
312
  }
256
- yield { type: 'finish', reason: { kind: 'stop' } }
313
+ if (!sawTerminal && (index > 0 || textIndex > 0 || toolCallIds.size > 0)) {
314
+ yield { type: 'finish', reason: { kind: 'error', failure: { code: 'INCOMPLETE', message: 'stream terminated before message_delta' } } }
315
+ return
316
+ }
317
+ if (finish.kind === 'stop' && toolCallIds.size > 0) finish = { kind: 'tool-calls' }
318
+ yield { type: 'finish', reason: finish }
257
319
  }
258
320
 
259
321
  export async function* googleStream(body) {
@@ -261,11 +323,27 @@ export async function* googleStream(body) {
261
323
  let started = false
262
324
  let callCount = 0
263
325
  const functionCallIndexes = new Map()
326
+ let finish = { kind: 'stop' }
327
+ let sawTerminal = false
264
328
  for await (const data of iterateSse(body)) {
329
+ if (data === '[DONE]') {
330
+ sawTerminal = true
331
+ break
332
+ }
265
333
  const json = jsonSse(data)
266
334
  const wrapped = json && json.response ? json.response : json
267
335
  if (!wrapped) continue
268
336
  const cand = wrapped.candidates && wrapped.candidates[0]
337
+ if (cand && cand.finishReason) {
338
+ sawTerminal = true
339
+ const fr = cand.finishReason
340
+ if (fr === 'MAX_TOKENS') finish = { kind: 'max-tokens' }
341
+ else if (fr === 'SAFETY' || fr === 'RECITATION' || fr === 'BLOCKLIST' || fr === 'PROHIBITED_CONTENT' || fr === 'SPII') {
342
+ finish = { kind: 'error', failure: { code: fr, message: cand.finishMessage || fr } }
343
+ } else if (fr === 'STOP') {
344
+ finish = { kind: 'stop' }
345
+ }
346
+ }
269
347
  const parts = cand && cand.content && Array.isArray(cand.content.parts) ? cand.content.parts : []
270
348
  for (const part of parts) {
271
349
  if (part.text) {
@@ -299,16 +377,27 @@ export async function* googleStream(body) {
299
377
  }
300
378
  }
301
379
  if (wrapped.usageMetadata) {
380
+ const u = wrapped.usageMetadata
381
+ const norm = toTokenUsage(u)
302
382
  yield {
303
383
  type: 'usage',
304
384
  usage: {
305
- input: Number(wrapped.usageMetadata.promptTokenCount ?? 0) || 0,
306
- output: Number(wrapped.usageMetadata.candidatesTokenCount ?? 0) || 0,
385
+ ...u,
386
+ ...(norm || {}),
387
+ input: norm ? norm.inputTokens : (Number(u.promptTokenCount ?? 0) || 0),
388
+ output: norm ? norm.outputTokens : (Number(u.candidatesTokenCount ?? 0) || 0),
307
389
  },
308
390
  }
309
391
  }
310
392
  }
311
- yield { type: 'finish', reason: { kind: 'stop' } }
393
+ if (!sawTerminal && (started || callCount > 0)) {
394
+ yield { type: 'finish', reason: { kind: 'error', failure: { code: 'INCOMPLETE', message: 'stream terminated before finishReason' } } }
395
+ return
396
+ }
397
+ if (finish.kind === 'stop' && (functionCallIndexes.size > 0 || callCount > 0)) {
398
+ finish = { kind: 'tool-calls' }
399
+ }
400
+ yield { type: 'finish', reason: finish }
312
401
  }
313
402
 
314
403
  export async function formTokenRequest(url, params, fetchImpl, headers, { timeoutMs = 25000 } = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.6.39",
3
+ "version": "0.6.41",
4
4
  "description": "Use ChatGPT Codex, Claude, Grok, Antigravity, Kimi, GLM, Cursor, Kiro, Copilot, Qwen, ERNIE, Spark, JetBrains, Perplexity, Replit, and Cody subscriptions as DeepSeek Harness LLM providers via OAuth.",
5
5
  "license": "MIT",
6
6
  "type": "module",