Skip to content

Commit 9a61c4a

Browse files
fix: handle unknown stream part types for thinking models (gpt-5-mini) #52
Models like gpt-5-mini emit LanguageModelThinkingPart in the response stream, which was being silently dropped. Added extractTextFromPart() helper and fallback handling across all 9 stream processing locations.
1 parent 9dc46fa commit 9a61c4a

3 files changed

Lines changed: 253 additions & 28 deletions

File tree

dist/extension.js

Lines changed: 163 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -52142,6 +52142,9 @@ var CopilotApiGateway = class {
5214252142
const normalized = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 10;
5214352143
await this.updateServerConfig({ maxConnectionsPerIp: normalized });
5214452144
}
52145+
async setCloudflaredPath(path4) {
52146+
await this.updateServerConfig({ cloudflaredPath: path4 });
52147+
}
5214552148
async setMaxConcurrency(limit) {
5214652149
const normalized = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 4;
5214752150
await this.updateServerConfig({ maxConcurrentRequests: normalized });
@@ -52184,22 +52187,43 @@ var CopilotApiGateway = class {
5218452187
if (!fs4.existsSync(globalStoragePath)) {
5218552188
fs4.mkdirSync(globalStoragePath, { recursive: true });
5218652189
}
52187-
const cloudflaredBin = path4.join(
52188-
globalStoragePath,
52189-
process.platform === "win32" ? "cloudflared.exe" : "cloudflared"
52190-
);
52191-
if (!fs4.existsSync(cloudflaredBin)) {
52192-
this.logInfo("Cloudflared binary not found, downloading...");
52190+
let cloudflaredBin = "";
52191+
const customPath = this.config.cloudflaredPath;
52192+
if (customPath && fs4.existsSync(customPath)) {
52193+
this.logInfo(`Using custom cloudflared path from configuration: ${customPath}`);
52194+
cloudflaredBin = customPath;
52195+
} else {
52196+
const { execSync } = await import("child_process");
5219352197
try {
52194-
await install(cloudflaredBin);
52195-
this.logInfo("Cloudflared binary downloaded successfully");
52196-
} catch (downloadError) {
52197-
const errMsg = downloadError instanceof Error ? downloadError.message : String(downloadError);
52198-
this.logError("Failed to download cloudflared binary", downloadError);
52199-
return { success: false, error: `Failed to download cloudflared: ${errMsg}. Check your internet connection.` };
52198+
const sysPath = execSync(process.platform === "win32" ? "where cloudflared" : "which cloudflared").toString().split("\n")[0].trim();
52199+
if (sysPath && fs4.existsSync(sysPath)) {
52200+
this.logInfo(`Found cloudflared in system PATH: ${sysPath}`);
52201+
cloudflaredBin = sysPath;
52202+
}
52203+
} catch (e) {
52204+
}
52205+
}
52206+
if (!cloudflaredBin) {
52207+
cloudflaredBin = path4.join(
52208+
globalStoragePath,
52209+
process.platform === "win32" ? "cloudflared.exe" : "cloudflared"
52210+
);
52211+
if (!fs4.existsSync(cloudflaredBin)) {
52212+
this.logInfo("Cloudflared binary not found locally or in PATH, downloading...");
52213+
try {
52214+
await install(cloudflaredBin);
52215+
this.logInfo("Cloudflared binary downloaded successfully");
52216+
} catch (downloadError) {
52217+
const errMsg = downloadError instanceof Error ? downloadError.message : String(downloadError);
52218+
this.logError("Failed to download cloudflared binary", downloadError);
52219+
return { success: false, error: `Failed to download cloudflared: ${errMsg}. Check your internet connection.` };
52220+
}
52221+
} else {
52222+
this.logInfo(`Using cached cloudflared binary: ${cloudflaredBin}`);
5220052223
}
52224+
} else {
52225+
this.logInfo(`Using chosen cloudflared binary: ${cloudflaredBin}`);
5220152226
}
52202-
this.logInfo(`Using cloudflared binary: ${cloudflaredBin}`);
5220352227
use(cloudflaredBin);
5220452228
const localUrl = `http://${this.config.host === "0.0.0.0" ? "127.0.0.1" : this.config.host}:${this.config.port}`;
5220552229
const tunnelInstance = Tunnel.quick(localUrl);
@@ -52973,16 +52997,22 @@ var CopilotApiGateway = class {
5297352997
if (cts.token.isCancellationRequested) {
5297452998
break;
5297552999
}
53000+
let textValue;
5297653001
if (part instanceof vscode4.LanguageModelTextPart) {
52977-
totalContent += part.value;
53002+
textValue = part.value;
53003+
} else if (!(part instanceof vscode4.LanguageModelToolCallPart)) {
53004+
textValue = this.extractTextFromPart(part);
53005+
}
53006+
if (textValue) {
53007+
totalContent += textValue;
5297853008
if (!firstPart) {
5297953009
res.write(",\n");
5298053010
}
5298153011
const chunk = {
5298253012
candidates: [{
5298353013
content: {
5298453014
role: "model",
52985-
parts: [{ text: part.value }]
53015+
parts: [{ text: textValue }]
5298653016
},
5298753017
finishReason: "STOP",
5298853018
index: 0
@@ -53167,6 +53197,19 @@ data: ${JSON.stringify({
5316753197
data: ${JSON.stringify({ type: "content_block_stop", index: contentBlockIndex })}
5316853198

5316953199
`);
53200+
} else {
53201+
const textValue = this.extractTextFromPart(part);
53202+
if (textValue) {
53203+
totalContent += textValue;
53204+
res.write(`event: content_block_delta
53205+
data: ${JSON.stringify({
53206+
type: "content_block_delta",
53207+
index: contentBlockIndex,
53208+
delta: { type: "text_delta", text: textValue }
53209+
})}
53210+
53211+
`);
53212+
}
5317053213
}
5317153214
}
5317253215
if (!cts.token.isCancellationRequested) {
@@ -53363,6 +53406,25 @@ data: ${JSON.stringify({ type: "error", error: { type: apiError.code || "api_err
5336353406

5336453407
`);
5336553408
toolCallIndex++;
53409+
} else {
53410+
const textValue = this.extractTextFromPart(part);
53411+
if (textValue) {
53412+
totalContent += textValue;
53413+
const chunk = {
53414+
id: requestId,
53415+
object: "chat.completion.chunk",
53416+
created,
53417+
model,
53418+
choices: [{
53419+
index: 0,
53420+
delta: { content: textValue },
53421+
finish_reason: null
53422+
}]
53423+
};
53424+
res.write(`data: ${JSON.stringify(chunk)}
53425+
53426+
`);
53427+
}
5336653428
}
5336753429
}
5336853430
const finishReason = toolCalls.length > 0 ? "tool_calls" : "stop";
@@ -53516,6 +53578,11 @@ data: ${JSON.stringify({ type: "error", error: { type: apiError.code || "api_err
5351653578
for await (const part of response.stream) {
5351753579
if (part instanceof vscode4.LanguageModelTextPart) {
5351853580
result += part.value;
53581+
} else if (!(part instanceof vscode4.LanguageModelToolCallPart)) {
53582+
const textValue = this.extractTextFromPart(part);
53583+
if (textValue) {
53584+
result += textValue;
53585+
}
5351953586
}
5352053587
}
5352153588
return result;
@@ -53718,14 +53785,20 @@ data: ${JSON.stringify({
5371853785
if (cts.token.isCancellationRequested) {
5371953786
break;
5372053787
}
53788+
let textValue;
5372153789
if (part instanceof vscode4.LanguageModelTextPart) {
53722-
totalContent += part.value;
53790+
textValue = part.value;
53791+
} else if (!(part instanceof vscode4.LanguageModelToolCallPart)) {
53792+
textValue = this.extractTextFromPart(part);
53793+
}
53794+
if (textValue) {
53795+
totalContent += textValue;
5372353796
res.write(`event: response.output_text.delta
5372453797
data: ${JSON.stringify({
5372553798
type: "response.output_text.delta",
5372653799
item_id: messageId,
5372753800
content_index: 0,
53728-
delta: part.value
53801+
delta: textValue
5372953802
})}
5373053803

5373153804
`);
@@ -53881,6 +53954,11 @@ data: ${JSON.stringify({
5388153954
for await (const part of result.stream) {
5388253955
if (part instanceof vscode4.LanguageModelTextPart) {
5388353956
output += part.value;
53957+
} else if (!(part instanceof vscode4.LanguageModelToolCallPart)) {
53958+
const textValue = this.extractTextFromPart(part);
53959+
if (textValue) {
53960+
output += textValue;
53961+
}
5388453962
}
5388553963
}
5388653964
return output;
@@ -53954,6 +54032,11 @@ data: ${JSON.stringify({
5395454032
for await (const part of result.stream) {
5395554033
if (part instanceof vscode4.LanguageModelTextPart) {
5395654034
output += part.value;
54035+
} else if (!(part instanceof vscode4.LanguageModelToolCallPart)) {
54036+
const textValue = this.extractTextFromPart(part);
54037+
if (textValue) {
54038+
output += textValue;
54039+
}
5395754040
}
5395854041
}
5395954042
return output;
@@ -54237,6 +54320,11 @@ IMPORTANT: You MUST respond with valid JSON only.No markdown, no explanation, ju
5423754320
name: part.name,
5423854321
arguments: part.input
5423954322
});
54323+
} else {
54324+
const textValue = this.extractTextFromPart(part);
54325+
if (textValue) {
54326+
textContent += textValue;
54327+
}
5424054328
}
5424154329
}
5424254330
return {
@@ -54343,16 +54431,22 @@ IMPORTANT: You MUST respond with valid JSON only.No markdown, no explanation, ju
5434354431
if (cts.token.isCancellationRequested) {
5434454432
break;
5434554433
}
54434+
let textValue;
5434654435
if (part instanceof vscode4.LanguageModelTextPart) {
54347-
totalContent += part.value;
54436+
textValue = part.value;
54437+
} else if (!(part instanceof vscode4.LanguageModelToolCallPart)) {
54438+
textValue = this.extractTextFromPart(part);
54439+
}
54440+
if (textValue) {
54441+
totalContent += textValue;
5434854442
const chunk = {
5434954443
id: completionId,
5435054444
object: "text_completion",
5435154445
created,
5435254446
model,
5435354447
choices: [{
5435454448
index: 0,
54355-
text: part.value,
54449+
text: textValue,
5435654450
finish_reason: null,
5435754451
logprobs: null
5435854452
}]
@@ -54677,6 +54771,25 @@ ${text} `;
5467754771
}
5467854772
return String(content);
5467954773
}
54774+
/**
54775+
* Extract text from an unknown stream part (e.g. LanguageModelThinkingPart or future part types).
54776+
* The VS Code LM API stream type is `AsyncIterable<LanguageModelTextPart | LanguageModelToolCallPart | unknown>`,
54777+
* meaning newer part types (like thinking parts from reasoning models such as gpt-5-mini) may appear
54778+
* as `unknown`. This method duck-types them to extract any text content they carry.
54779+
*/
54780+
extractTextFromPart(part) {
54781+
if (!part || typeof part !== "object") {
54782+
return void 0;
54783+
}
54784+
const p = part;
54785+
if (typeof p.value === "string") {
54786+
return p.value;
54787+
}
54788+
if (typeof p.text === "string") {
54789+
return p.text;
54790+
}
54791+
return void 0;
54792+
}
5468054793
resolveModel(model) {
5468154794
if (typeof model === "string" && model.trim()) {
5468254795
return model.trim();
@@ -56092,6 +56205,9 @@ ${text} `;
5609256205
if (patch.maxConnectionsPerIp !== void 0) {
5609356206
updates.push(Promise.resolve(config2.update("server.maxConnectionsPerIp", patch.maxConnectionsPerIp, vscode4.ConfigurationTarget.Global)));
5609456207
}
56208+
if (patch.cloudflaredPath !== void 0) {
56209+
updates.push(Promise.resolve(config2.update("tunnel.cloudflaredPath", patch.cloudflaredPath, vscode4.ConfigurationTarget.Global)));
56210+
}
5609556211
if (patch.redactionPatterns !== void 0) {
5609656212
updates.push(Promise.resolve(config2.update("server.redactionPatterns", patch.redactionPatterns, vscode4.ConfigurationTarget.Global)));
5609756213
}
@@ -56285,6 +56401,7 @@ function getServerConfig() {
5628556401
const maxPayloadSizeMb = configuration.get("server.maxPayloadSizeMb", 1);
5628656402
const maxConnectionsPerIp = configuration.get("server.maxConnectionsPerIp", 10);
5628756403
const mcpEnabled = vscode4.workspace.getConfiguration("githubCopilotApi.mcp").get("enabled", true);
56404+
const cloudflaredPath = vscode4.workspace.getConfiguration("githubCopilotApi.tunnel").get("cloudflaredPath", "").trim();
5628856405
return {
5628956406
enabled,
5629056407
enableHttp,
@@ -56305,7 +56422,8 @@ function getServerConfig() {
5630556422
requestTimeoutSeconds,
5630656423
maxPayloadSizeMb,
5630756424
maxConnectionsPerIp,
56308-
mcpEnabled
56425+
mcpEnabled,
56426+
cloudflaredPath
5630956427
};
5631056428
}
5631156429
function getErrorMessage(error2) {
@@ -56974,6 +57092,15 @@ for await (const chunk of stream) {
5697457092
});
5697557093
}
5697657094
break;
57095+
case "setCloudflaredPath":
57096+
if (typeof data.value === "string") {
57097+
void gateway2.setCloudflaredPath(data.value).then(async () => {
57098+
if (_CopilotPanel.currentPanel) {
57099+
_CopilotPanel.currentPanel.webview.html = await _CopilotPanel.getPanelHtml(_CopilotPanel.currentPanel.webview, gateway2);
57100+
}
57101+
});
57102+
}
57103+
break;
5697757104
case "setMaxConcurrency":
5697857105
if (typeof data.value === "number") {
5697957106
void gateway2.setMaxConcurrency(data.value).then(async () => {
@@ -58323,6 +58450,14 @@ print(response.choices[0].message.content)\`;
5832358450
</ul>
5832458451
</div>
5832558452

58453+
<div style="margin-bottom: 16px; border-top: 1px solid var(--vscode-widget-border); padding-top: 16px;">
58454+
<div style="font-size: 11px; font-weight: 600; margin-bottom: 8px; opacity: 0.8;">CLOUDFLARED BINARY PATH (OPTIONAL)</div>
58455+
<div style="display: flex; gap: 8px; align-items: center; flex-wrap: wrap;">
58456+
<input type="text" id="cloudflared-path-input" value="${config2.cloudflaredPath || ""}" placeholder="Leave empty for auto-download or PATH" style="flex: 1; min-width: 200px;">
58457+
<button class="secondary" id="btn-set-cloudflared-path" style="width: auto; padding: 4px 12px; font-size: 11px;">Save Path</button>
58458+
</div>
58459+
</div>
58460+
5832658461
<div id="tunnel-status-area" style="margin-bottom: 16px;">
5832758462
${status.tunnel?.running ? status.tunnel?.url ? `
5832858463
<div style="background: color-mix(in srgb, var(--vscode-testing-iconPassed) 15%, transparent); border: 1px solid var(--vscode-testing-iconPassed); border-radius: 8px; padding: 16px;">
@@ -58975,6 +59110,14 @@ print(response.choices[0].message.content)\`;
5897559110
vscode.postMessage({ type: 'setMaxConnectionsPerIp', value: Number(val) });
5897659111
};
5897759112
}
59113+
59114+
const btnSetCloudflaredPath = document.getElementById('btn-set-cloudflared-path');
59115+
if (btnSetCloudflaredPath) {
59116+
btnSetCloudflaredPath.onclick = function() {
59117+
var val = document.getElementById('cloudflared-path-input').value;
59118+
vscode.postMessage({ type: 'setCloudflaredPath', value: String(val).trim() });
59119+
};
59120+
}
5897859121
document.getElementById('btn-set-concurrency').onclick = function() {
5897959122
var val = document.getElementById('concurrency-input').value;
5898059123
vscode.postMessage({ type: 'setMaxConcurrency', value: Number(val) });

0 commit comments

Comments
 (0)