@semanticist14/clco 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bun.lock +3 -0
- package/package.json +2 -1
- package/src/responses.ts +95 -11
- package/src/server.ts +4 -0
package/bun.lock
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@types/bun": "latest",
|
|
16
|
+
"@types/turndown": "^5.0.6",
|
|
16
17
|
"typescript": "^5",
|
|
17
18
|
},
|
|
18
19
|
},
|
|
@@ -30,6 +31,8 @@
|
|
|
30
31
|
|
|
31
32
|
"@types/node": ["@types/node@26.5.1", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-CzNm2FezW4VR/LjG6yUdiEgLE/rAQ9Slj5gCu/C2VrdcW7I0ahNZ8DRbHT7zOZ6r3ONgd/bsQIeSaoDGrd1C6g=="],
|
|
32
33
|
|
|
34
|
+
"@types/turndown": ["@types/turndown@5.0.6", "", {}, "sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg=="],
|
|
35
|
+
|
|
33
36
|
"boolbase": ["boolbase@2.0.0", "", {}, "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA=="],
|
|
34
37
|
|
|
35
38
|
"bun-types": ["bun-types@1.4.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-bxV1FgK7yBIzjRe5zBozIM4Bem11ZJcCXSrjWRG3YWLt8yFDePu4cLjpebO8OvPeIE9trbyPF4fuj3Cia4Fj3w=="],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@semanticist14/clco",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Run Claude Code on a GitHub Copilot subscription",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@types/bun": "latest",
|
|
31
|
+
"@types/turndown": "^5.0.6",
|
|
31
32
|
"typescript": "^5"
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|
package/src/responses.ts
CHANGED
|
@@ -196,9 +196,68 @@ function incompleteError(response: Record<string, unknown> | undefined): OpenAIR
|
|
|
196
196
|
export class ResponsesEventAdapter {
|
|
197
197
|
private toolIndexes = new Map<string, number>()
|
|
198
198
|
private argsAccum = new Map<string, string>()
|
|
199
|
+
private argsRescued = new Set<string>()
|
|
199
200
|
private textAccum = new Map<string, string>()
|
|
201
|
+
private textRescued = new Set<string>()
|
|
202
|
+
private lastToolKey: string | null = null
|
|
203
|
+
private lastTextKey: string | null = null
|
|
200
204
|
private nextIndex = 0
|
|
201
205
|
private sawToolCall = false
|
|
206
|
+
private fallbacks = 0
|
|
207
|
+
private drops = 0
|
|
208
|
+
private firstLoss: string | null = null
|
|
209
|
+
|
|
210
|
+
// Some upstreams spell the item_id differently on output_item.added than on
|
|
211
|
+
// the delta/done events that follow (observed with gpt-6-astra). An unknown
|
|
212
|
+
// id falls back to the most recent item of that kind so the event is not
|
|
213
|
+
// silently dropped. Ceilings: interleaved parallel tool calls with distinct
|
|
214
|
+
// mismatched ids cannot be correlated and merge into the latest item, and a
|
|
215
|
+
// text item announced only by added (deltas under another id) is not
|
|
216
|
+
// rescued. Both are visible in lossSummary().
|
|
217
|
+
private toolKeyFor(
|
|
218
|
+
itemId: string,
|
|
219
|
+
event?: Record<string, unknown>,
|
|
220
|
+
): string | null {
|
|
221
|
+
if (this.toolIndexes.has(itemId)) return itemId
|
|
222
|
+
if (this.lastToolKey !== null) {
|
|
223
|
+
this.fallbacks++
|
|
224
|
+
this.noteLoss(
|
|
225
|
+
`arg event item_id=${itemId} matched to most recent tool call ${this.lastToolKey}`,
|
|
226
|
+
event,
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
return this.lastToolKey
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private textKeyFor(
|
|
233
|
+
itemId: string,
|
|
234
|
+
event?: Record<string, unknown>,
|
|
235
|
+
): string {
|
|
236
|
+
if (this.textAccum.has(itemId) || this.lastTextKey === null)
|
|
237
|
+
return itemId
|
|
238
|
+
this.fallbacks++
|
|
239
|
+
this.noteLoss(
|
|
240
|
+
`text event item_id=${itemId} matched to most recent text item ${this.lastTextKey}`,
|
|
241
|
+
event,
|
|
242
|
+
)
|
|
243
|
+
return this.lastTextKey
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private noteLoss(detail: string, event?: Record<string, unknown>): void {
|
|
247
|
+
if (this.firstLoss !== null) return
|
|
248
|
+
const raw = event ? ` event=${JSON.stringify(event).slice(0, 300)}` : ""
|
|
249
|
+
this.firstLoss = detail + raw
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** One line per stream, or null when every event matched an added item. */
|
|
253
|
+
lossSummary(): string | null {
|
|
254
|
+
if (this.fallbacks === 0 && this.drops === 0) return null
|
|
255
|
+
const parts: string[] = []
|
|
256
|
+
if (this.fallbacks > 0)
|
|
257
|
+
parts.push(`${this.fallbacks} event(s) matched by fallback (guessed item)`)
|
|
258
|
+
if (this.drops > 0) parts.push(`${this.drops} event(s) dropped (lost)`)
|
|
259
|
+
return `${parts.join("; ")}; first: ${this.firstLoss}`
|
|
260
|
+
}
|
|
202
261
|
|
|
203
262
|
/** Convert one Responses stream event into OpenAI-chunk shape (or null). */
|
|
204
263
|
pushEvent(event: Record<string, unknown>): OpenAIResponse | null {
|
|
@@ -208,8 +267,9 @@ export class ResponsesEventAdapter {
|
|
|
208
267
|
if (type === "response.output_text.delta") {
|
|
209
268
|
const delta = event.delta as string | undefined
|
|
210
269
|
if (!delta) return null
|
|
211
|
-
const key = String(event.item_id ?? "r")
|
|
270
|
+
const key = this.textKeyFor(String(event.item_id ?? "r"))
|
|
212
271
|
this.textAccum.set(key, (this.textAccum.get(key) ?? "") + delta)
|
|
272
|
+
this.lastTextKey = key
|
|
213
273
|
return {
|
|
214
274
|
id: key,
|
|
215
275
|
model: "",
|
|
@@ -220,9 +280,10 @@ export class ResponsesEventAdapter {
|
|
|
220
280
|
if (type === "response.output_text.done") {
|
|
221
281
|
// Authoritative fallback: if no delta arrived for this item, emit the
|
|
222
282
|
// full text so a lost stream cannot silently become an empty message.
|
|
223
|
-
const key = String(event.item_id ?? "r")
|
|
283
|
+
const key = this.textKeyFor(String(event.item_id ?? "r"), event)
|
|
224
284
|
const full = (event.text as string | undefined) ?? ""
|
|
225
|
-
if (full && !(this.
|
|
285
|
+
if (full && !(this.textRescued.has(key) || this.textAccum.get(key))) {
|
|
286
|
+
this.textRescued.add(key)
|
|
226
287
|
return {
|
|
227
288
|
id: key,
|
|
228
289
|
model: "",
|
|
@@ -236,13 +297,20 @@ export class ResponsesEventAdapter {
|
|
|
236
297
|
const item = event.item as
|
|
237
298
|
| { type?: string; call_id?: string; id?: string; name?: string }
|
|
238
299
|
| undefined
|
|
239
|
-
if (item?.type !== "function_call")
|
|
300
|
+
if (item?.type !== "function_call") {
|
|
301
|
+
// A non-function item (message, reasoning, ...) is a genuinely new
|
|
302
|
+
// text item: stop pointing lastTextKey at the previous one so its
|
|
303
|
+
// deltas are never folded in.
|
|
304
|
+
this.lastTextKey = null
|
|
305
|
+
return null
|
|
306
|
+
}
|
|
240
307
|
this.sawToolCall = true
|
|
241
308
|
const index = this.nextIndex++
|
|
242
309
|
const key = String(item.call_id ?? item.id ?? index)
|
|
243
310
|
this.toolIndexes.set(key, index)
|
|
244
311
|
// Arguments deltas are keyed by item_id; remember that too.
|
|
245
312
|
if (item.id) this.toolIndexes.set(String(item.id), index)
|
|
313
|
+
this.lastToolKey = String(item.id ?? key)
|
|
246
314
|
return {
|
|
247
315
|
id: String(item.call_id ?? item.id ?? "r"),
|
|
248
316
|
model: "",
|
|
@@ -267,9 +335,16 @@ export class ResponsesEventAdapter {
|
|
|
267
335
|
if (type === "response.function_call_arguments.delta") {
|
|
268
336
|
const delta = event.delta as string | undefined
|
|
269
337
|
if (!delta) return null
|
|
270
|
-
const key = String(event.item_id ?? "")
|
|
271
|
-
|
|
272
|
-
|
|
338
|
+
const key = this.toolKeyFor(String(event.item_id ?? ""), event)
|
|
339
|
+
if (key === null || !this.toolIndexes.has(key)) {
|
|
340
|
+
this.drops++
|
|
341
|
+
this.noteLoss(
|
|
342
|
+
`arg delta item_id=${event.item_id} had no tool call to attach to`,
|
|
343
|
+
event,
|
|
344
|
+
)
|
|
345
|
+
return null
|
|
346
|
+
}
|
|
347
|
+
const index = this.toolIndexes.get(key)!
|
|
273
348
|
this.argsAccum.set(key, (this.argsAccum.get(key) ?? "") + delta)
|
|
274
349
|
return {
|
|
275
350
|
id: key,
|
|
@@ -287,11 +362,20 @@ export class ResponsesEventAdapter {
|
|
|
287
362
|
}
|
|
288
363
|
|
|
289
364
|
if (type === "response.function_call_arguments.done") {
|
|
290
|
-
const key = String(event.item_id ?? "")
|
|
291
|
-
|
|
292
|
-
|
|
365
|
+
const key = this.toolKeyFor(String(event.item_id ?? ""), event)
|
|
366
|
+
if (key === null || !this.toolIndexes.has(key)) {
|
|
367
|
+
this.drops++
|
|
368
|
+
this.noteLoss(
|
|
369
|
+
`arg done item_id=${event.item_id} had no tool call to attach to`,
|
|
370
|
+
event,
|
|
371
|
+
)
|
|
372
|
+
return null
|
|
373
|
+
}
|
|
374
|
+
const index = this.toolIndexes.get(key)!
|
|
293
375
|
const full = (event.arguments as string | undefined) ?? ""
|
|
294
|
-
if (full && !(this.
|
|
376
|
+
if (full && !(this.argsRescued.has(key) || this.argsAccum.get(key))) {
|
|
377
|
+
this.argsRescued.add(key)
|
|
378
|
+
this.argsAccum.set(key, full)
|
|
295
379
|
return {
|
|
296
380
|
id: key,
|
|
297
381
|
model: "",
|
package/src/server.ts
CHANGED
|
@@ -361,6 +361,10 @@ function sseResponse(
|
|
|
361
361
|
if (!errored) {
|
|
362
362
|
for (const ev of translator.finish()) send(ev)
|
|
363
363
|
}
|
|
364
|
+
// Item-id mismatches and drops must never be silent again: this
|
|
365
|
+
// incident hid for 97 tool calls behind a debug-only log path.
|
|
366
|
+
const loss = eventAdapter?.lossSummary()
|
|
367
|
+
if (loss) console.error("[clco:responses]", loss)
|
|
364
368
|
} catch (err) {
|
|
365
369
|
send({
|
|
366
370
|
event: "error",
|