copilot-reverse 0.5.2 → 0.5.3
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.
|
@@ -86,13 +86,26 @@ export class CopilotAdapter {
|
|
|
86
86
|
const data = (await res.json());
|
|
87
87
|
const choice = data.choices[0];
|
|
88
88
|
const content = [];
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
// Recover inline-XML tool calls in non-stream replies too (same reason as the stream path).
|
|
90
|
+
let xmlTool = false;
|
|
91
|
+
if (choice.message.content) {
|
|
92
|
+
const ex = new ToolCallExtractor();
|
|
93
|
+
for (const ev of [...ex.feed(choice.message.content), ...ex.flush()]) {
|
|
94
|
+
if (ev.kind === "text") {
|
|
95
|
+
if (ev.text)
|
|
96
|
+
content.push({ type: "text", text: ev.text });
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
xmlTool = true;
|
|
100
|
+
content.push({ type: "tool_use", id: ev.tool.id, name: ev.tool.name, input: ev.tool.input });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
91
104
|
for (const tc of choice.message.tool_calls ?? [])
|
|
92
105
|
content.push({ type: "tool_use", id: tc.id, name: tc.function.name, input: safeJson(tc.function.arguments) });
|
|
93
106
|
return {
|
|
94
107
|
id: data.id ?? `cmpl-${randomUUID().replace(/-/g, "")}`, model: req.model, content,
|
|
95
|
-
finishReason: choice.finish_reason === "tool_calls" ? "tool_use" : choice.finish_reason === "length" ? "length" : "stop",
|
|
108
|
+
finishReason: choice.finish_reason === "tool_calls" || xmlTool ? "tool_use" : choice.finish_reason === "length" ? "length" : "stop",
|
|
96
109
|
usage: { promptTokens: data.usage?.prompt_tokens ?? 0, completionTokens: data.usage?.completion_tokens ?? 0 },
|
|
97
110
|
};
|
|
98
111
|
}
|
|
@@ -134,9 +147,11 @@ export class CopilotAdapter {
|
|
|
134
147
|
let usage;
|
|
135
148
|
const mapFinish = (f) => f === "tool_calls" ? "tool_use" : f === "length" ? "length" : "stop";
|
|
136
149
|
// Some models emit a tool call as inline XML text instead of native tool_calls (more likely on
|
|
137
|
-
// long/tool-heavy turns)
|
|
138
|
-
// that
|
|
139
|
-
|
|
150
|
+
// long/tool-heavy turns) — and they do it even when THIS request declared no tools (a follow-up
|
|
151
|
+
// turn, or a model that ignores the tools field). Always run assistant text through the extractor;
|
|
152
|
+
// it only captures on the distinctive `<invoke>`/`<function_calls>` sentinel and flushes anything
|
|
153
|
+
// unparseable back as text, so plain prose is unaffected.
|
|
154
|
+
const extractor = new ToolCallExtractor();
|
|
140
155
|
let extractedTool = false;
|
|
141
156
|
let extIdx = 100; // separate index space so recovered tools never collide with native tool_calls
|
|
142
157
|
const toChunks = (events) => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { ToolCallExtractor } from "../../core/tool-xml.js";
|
|
2
3
|
// Outbound translation to GitHub Copilot's OpenAI Responses API. Newer Copilot models (e.g. gpt-5.5)
|
|
3
4
|
// are served ONLY on /responses — their `supported_endpoints` omits /chat/completions — so the adapter
|
|
4
5
|
// routes them here instead of the chat path. This is the mirror image of core/responses-inbound.ts
|
|
@@ -68,8 +69,20 @@ export function parseResponsesResult(data) {
|
|
|
68
69
|
for (const item of data.output ?? []) {
|
|
69
70
|
if (item.type === "message") {
|
|
70
71
|
const text = (item.content ?? []).filter((p) => p.type === "output_text").map((p) => p.text ?? "").join("");
|
|
71
|
-
if (text)
|
|
72
|
-
|
|
72
|
+
if (text) {
|
|
73
|
+
// Recover inline-XML tool calls here too (some models emit them as output_text).
|
|
74
|
+
const ex = new ToolCallExtractor();
|
|
75
|
+
for (const ev of [...ex.feed(text), ...ex.flush()]) {
|
|
76
|
+
if (ev.kind === "text") {
|
|
77
|
+
if (ev.text)
|
|
78
|
+
content.push({ type: "text", text: ev.text });
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
sawTool = true;
|
|
82
|
+
content.push({ type: "tool_use", id: ev.tool.id, name: ev.tool.name, input: ev.tool.input });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
73
86
|
}
|
|
74
87
|
else if (item.type === "function_call") {
|
|
75
88
|
sawTool = true;
|
|
@@ -99,6 +112,25 @@ export async function* streamResponses(res) {
|
|
|
99
112
|
let usage;
|
|
100
113
|
const toolByOutputIndex = new Map(); // responses output_index -> canonical tool index
|
|
101
114
|
let nextToolIndex = 0;
|
|
115
|
+
// Some models stream a tool call as inline XML text instead of a function_call item; recover it.
|
|
116
|
+
// Extracted tools use a high index space so they never collide with native function_call indices.
|
|
117
|
+
const extractor = new ToolCallExtractor();
|
|
118
|
+
let extIdx = 100, extractedTool = false;
|
|
119
|
+
const toChunks = (events) => {
|
|
120
|
+
const out = [];
|
|
121
|
+
for (const ev of events) {
|
|
122
|
+
if (ev.kind === "text") {
|
|
123
|
+
if (ev.text)
|
|
124
|
+
out.push({ kind: "text", delta: ev.text, done: false });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const index = extIdx++;
|
|
128
|
+
extractedTool = true;
|
|
129
|
+
out.push({ kind: "tool_use_start", index, id: ev.tool.id, name: ev.tool.name, done: false });
|
|
130
|
+
out.push({ kind: "tool_use_delta", index, argsDelta: JSON.stringify(ev.tool.input), done: false });
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
};
|
|
102
134
|
const usageOf = (u) => u ? { promptTokens: u.input_tokens ?? 0, completionTokens: u.output_tokens ?? 0, cachedTokens: u.input_tokens_details?.cached_tokens ?? 0 } : undefined;
|
|
103
135
|
for (;;) {
|
|
104
136
|
const { value, done } = await reader.read();
|
|
@@ -133,7 +165,8 @@ export async function* streamResponses(res) {
|
|
|
133
165
|
}
|
|
134
166
|
case "response.output_text.delta":
|
|
135
167
|
if (ev.delta)
|
|
136
|
-
|
|
168
|
+
for (const ch of toChunks(extractor.feed(ev.delta)))
|
|
169
|
+
yield ch;
|
|
137
170
|
break;
|
|
138
171
|
case "response.function_call_arguments.delta": {
|
|
139
172
|
const idx = toolByOutputIndex.get(ev.output_index);
|
|
@@ -142,7 +175,7 @@ export async function* streamResponses(res) {
|
|
|
142
175
|
break;
|
|
143
176
|
}
|
|
144
177
|
case "response.completed":
|
|
145
|
-
if (toolByOutputIndex.size)
|
|
178
|
+
if (toolByOutputIndex.size || extractedTool)
|
|
146
179
|
finishReason = "tool_use";
|
|
147
180
|
usage = usageOf(ev.response?.usage) ?? usage;
|
|
148
181
|
break;
|
|
@@ -157,5 +190,9 @@ export async function* streamResponses(res) {
|
|
|
157
190
|
}
|
|
158
191
|
}
|
|
159
192
|
}
|
|
193
|
+
for (const ch of toChunks(extractor.flush()))
|
|
194
|
+
yield ch;
|
|
195
|
+
if (extractedTool && finishReason === "stop")
|
|
196
|
+
finishReason = "tool_use";
|
|
160
197
|
yield { kind: "done", done: true, finishReason, usage };
|
|
161
198
|
}
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// AUTO-GENERATED by scripts/gen-version.mjs from package.json — do not edit.
|
|
2
|
-
export const APP_VERSION = "0.5.
|
|
2
|
+
export const APP_VERSION = "0.5.3";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "copilot-reverse",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "Interactive terminal app that exposes your GitHub Copilot subscription as local OpenAI- and Anthropic-compatible endpoints, with a self-healing daemon and a built-in assistant.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|