@playwo/opencode-cursor-oauth 0.0.0-dev.e795e5ffd849 → 0.0.0-dev.fc97acd8b777
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/dist/index.js +9 -0
- package/dist/proxy.js +304 -65
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -128,6 +128,15 @@ export const CursorAuthPlugin = async (input) => {
|
|
|
128
128
|
},
|
|
129
129
|
],
|
|
130
130
|
},
|
|
131
|
+
async "chat.headers"(incoming, output) {
|
|
132
|
+
if (incoming.model.providerID !== CURSOR_PROVIDER_ID)
|
|
133
|
+
return;
|
|
134
|
+
output.headers["x-opencode-session-id"] = incoming.sessionID;
|
|
135
|
+
output.headers["x-session-id"] = incoming.sessionID;
|
|
136
|
+
if (incoming.agent) {
|
|
137
|
+
output.headers["x-opencode-agent"] = incoming.agent;
|
|
138
|
+
}
|
|
139
|
+
},
|
|
131
140
|
};
|
|
132
141
|
};
|
|
133
142
|
function buildCursorProviderModels(models, port) {
|
package/dist/proxy.js
CHANGED
|
@@ -41,6 +41,31 @@ function evictStaleConversations() {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
function normalizeAgentKey(agentKey) {
|
|
45
|
+
const trimmed = agentKey?.trim();
|
|
46
|
+
return trimmed ? trimmed : "default";
|
|
47
|
+
}
|
|
48
|
+
function hashString(value) {
|
|
49
|
+
return createHash("sha256").update(value).digest("hex");
|
|
50
|
+
}
|
|
51
|
+
function createStoredConversation() {
|
|
52
|
+
return {
|
|
53
|
+
conversationId: crypto.randomUUID(),
|
|
54
|
+
checkpoint: null,
|
|
55
|
+
blobStore: new Map(),
|
|
56
|
+
lastAccessMs: Date.now(),
|
|
57
|
+
systemPromptHash: "",
|
|
58
|
+
completedTurnsFingerprint: "",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function resetStoredConversation(stored) {
|
|
62
|
+
stored.conversationId = crypto.randomUUID();
|
|
63
|
+
stored.checkpoint = null;
|
|
64
|
+
stored.blobStore = new Map();
|
|
65
|
+
stored.lastAccessMs = Date.now();
|
|
66
|
+
stored.systemPromptHash = "";
|
|
67
|
+
stored.completedTurnsFingerprint = "";
|
|
68
|
+
}
|
|
44
69
|
/** Connect protocol frame: [1-byte flags][4-byte BE length][payload] */
|
|
45
70
|
function frameConnectMessage(data, flags = 0) {
|
|
46
71
|
const frame = Buffer.alloc(5 + data.length);
|
|
@@ -471,7 +496,11 @@ export async function startProxy(getAccessToken, models = []) {
|
|
|
471
496
|
throw new Error("Cursor proxy access token provider not configured");
|
|
472
497
|
}
|
|
473
498
|
const accessToken = await proxyAccessTokenProvider();
|
|
474
|
-
|
|
499
|
+
const sessionId = req.headers.get("x-opencode-session-id")
|
|
500
|
+
?? req.headers.get("x-session-id")
|
|
501
|
+
?? undefined;
|
|
502
|
+
const agentKey = req.headers.get("x-opencode-agent") ?? undefined;
|
|
503
|
+
return handleChatCompletion(body, accessToken, { sessionId, agentKey });
|
|
475
504
|
}
|
|
476
505
|
catch (err) {
|
|
477
506
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -509,10 +538,11 @@ export function stopProxy() {
|
|
|
509
538
|
activeBridges.clear();
|
|
510
539
|
conversationStates.clear();
|
|
511
540
|
}
|
|
512
|
-
function handleChatCompletion(body, accessToken) {
|
|
513
|
-
const
|
|
541
|
+
function handleChatCompletion(body, accessToken, context = {}) {
|
|
542
|
+
const parsed = parseMessages(body.messages);
|
|
543
|
+
const { systemPrompt, userText, turns, toolResults, pendingAssistantSummary, completedTurnsFingerprint, } = parsed;
|
|
514
544
|
const modelId = body.model;
|
|
515
|
-
const tools = body.tools ?? [];
|
|
545
|
+
const tools = selectToolsForChoice(body.tools ?? [], body.tool_choice);
|
|
516
546
|
if (!userText && toolResults.length === 0) {
|
|
517
547
|
return new Response(JSON.stringify({
|
|
518
548
|
error: {
|
|
@@ -521,16 +551,24 @@ function handleChatCompletion(body, accessToken) {
|
|
|
521
551
|
},
|
|
522
552
|
}), { status: 400, headers: { "Content-Type": "application/json" } });
|
|
523
553
|
}
|
|
524
|
-
// bridgeKey:
|
|
554
|
+
// bridgeKey: session/agent-scoped, for active tool-call bridges
|
|
525
555
|
// convKey: model-independent, for conversation state that survives model switches
|
|
526
|
-
const bridgeKey = deriveBridgeKey(modelId, body.messages);
|
|
527
|
-
const convKey = deriveConversationKey(body.messages);
|
|
556
|
+
const bridgeKey = deriveBridgeKey(modelId, body.messages, context.sessionId, context.agentKey);
|
|
557
|
+
const convKey = deriveConversationKey(body.messages, context.sessionId, context.agentKey);
|
|
528
558
|
const activeBridge = activeBridges.get(bridgeKey);
|
|
529
559
|
if (activeBridge && toolResults.length > 0) {
|
|
530
560
|
activeBridges.delete(bridgeKey);
|
|
531
561
|
if (activeBridge.bridge.alive) {
|
|
562
|
+
if (activeBridge.modelId !== modelId) {
|
|
563
|
+
logPluginWarn("Resuming pending Cursor tool call on original model after model switch", {
|
|
564
|
+
requestedModelId: modelId,
|
|
565
|
+
resumedModelId: activeBridge.modelId,
|
|
566
|
+
convKey,
|
|
567
|
+
bridgeKey,
|
|
568
|
+
});
|
|
569
|
+
}
|
|
532
570
|
// Resume the live bridge with tool results
|
|
533
|
-
return handleToolResultResume(activeBridge, toolResults,
|
|
571
|
+
return handleToolResultResume(activeBridge, toolResults, bridgeKey, convKey);
|
|
534
572
|
}
|
|
535
573
|
// Bridge died (timeout, server disconnect, etc.).
|
|
536
574
|
// Clean up and fall through to start a fresh bridge.
|
|
@@ -545,28 +583,43 @@ function handleChatCompletion(body, accessToken) {
|
|
|
545
583
|
}
|
|
546
584
|
let stored = conversationStates.get(convKey);
|
|
547
585
|
if (!stored) {
|
|
548
|
-
stored =
|
|
549
|
-
conversationId: crypto.randomUUID(),
|
|
550
|
-
checkpoint: null,
|
|
551
|
-
blobStore: new Map(),
|
|
552
|
-
lastAccessMs: Date.now(),
|
|
553
|
-
};
|
|
586
|
+
stored = createStoredConversation();
|
|
554
587
|
conversationStates.set(convKey, stored);
|
|
555
588
|
}
|
|
589
|
+
const systemPromptHash = hashString(systemPrompt);
|
|
590
|
+
if (stored.checkpoint
|
|
591
|
+
&& (stored.systemPromptHash !== systemPromptHash
|
|
592
|
+
|| (turns.length > 0 && stored.completedTurnsFingerprint !== completedTurnsFingerprint))) {
|
|
593
|
+
resetStoredConversation(stored);
|
|
594
|
+
}
|
|
595
|
+
stored.systemPromptHash = systemPromptHash;
|
|
596
|
+
stored.completedTurnsFingerprint = completedTurnsFingerprint;
|
|
556
597
|
stored.lastAccessMs = Date.now();
|
|
557
598
|
evictStaleConversations();
|
|
558
599
|
// Build the request. When tool results are present but the bridge died,
|
|
559
600
|
// we must still include the last user text so Cursor has context.
|
|
560
601
|
const mcpTools = buildMcpToolDefinitions(tools);
|
|
561
|
-
const effectiveUserText =
|
|
562
|
-
?
|
|
563
|
-
:
|
|
602
|
+
const effectiveUserText = toolResults.length > 0
|
|
603
|
+
? buildToolResumePrompt(userText, pendingAssistantSummary, toolResults)
|
|
604
|
+
: userText;
|
|
564
605
|
const payload = buildCursorRequest(modelId, systemPrompt, effectiveUserText, turns, stored.conversationId, stored.checkpoint, stored.blobStore);
|
|
565
606
|
payload.mcpTools = mcpTools;
|
|
566
607
|
if (body.stream === false) {
|
|
567
|
-
return handleNonStreamingResponse(payload, accessToken, modelId, convKey
|
|
608
|
+
return handleNonStreamingResponse(payload, accessToken, modelId, convKey, {
|
|
609
|
+
systemPrompt,
|
|
610
|
+
systemPromptHash,
|
|
611
|
+
completedTurnsFingerprint,
|
|
612
|
+
turns,
|
|
613
|
+
userText,
|
|
614
|
+
});
|
|
568
615
|
}
|
|
569
|
-
return handleStreamingResponse(payload, accessToken, modelId, bridgeKey, convKey
|
|
616
|
+
return handleStreamingResponse(payload, accessToken, modelId, bridgeKey, convKey, {
|
|
617
|
+
systemPrompt,
|
|
618
|
+
systemPromptHash,
|
|
619
|
+
completedTurnsFingerprint,
|
|
620
|
+
turns,
|
|
621
|
+
userText,
|
|
622
|
+
});
|
|
570
623
|
}
|
|
571
624
|
/** Normalize OpenAI message content to a plain string. */
|
|
572
625
|
function textContent(content) {
|
|
@@ -581,8 +634,6 @@ function textContent(content) {
|
|
|
581
634
|
}
|
|
582
635
|
function parseMessages(messages) {
|
|
583
636
|
let systemPrompt = "You are a helpful assistant.";
|
|
584
|
-
const pairs = [];
|
|
585
|
-
const toolResults = [];
|
|
586
637
|
// Collect system messages
|
|
587
638
|
const systemParts = messages
|
|
588
639
|
.filter((m) => m.role === "system")
|
|
@@ -590,40 +641,152 @@ function parseMessages(messages) {
|
|
|
590
641
|
if (systemParts.length > 0) {
|
|
591
642
|
systemPrompt = systemParts.join("\n");
|
|
592
643
|
}
|
|
593
|
-
// Separate tool results from conversation turns
|
|
594
644
|
const nonSystem = messages.filter((m) => m.role !== "system");
|
|
595
|
-
|
|
645
|
+
const parsedTurns = [];
|
|
646
|
+
let currentTurn;
|
|
596
647
|
for (const msg of nonSystem) {
|
|
597
|
-
if (msg.role === "
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
648
|
+
if (msg.role === "user") {
|
|
649
|
+
if (currentTurn)
|
|
650
|
+
parsedTurns.push(currentTurn);
|
|
651
|
+
currentTurn = {
|
|
652
|
+
userText: textContent(msg.content),
|
|
653
|
+
segments: [],
|
|
654
|
+
};
|
|
655
|
+
continue;
|
|
602
656
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
pairs.push({ userText: pendingUser, assistantText: "" });
|
|
606
|
-
}
|
|
607
|
-
pendingUser = textContent(msg.content);
|
|
657
|
+
if (!currentTurn) {
|
|
658
|
+
currentTurn = { userText: "", segments: [] };
|
|
608
659
|
}
|
|
609
|
-
|
|
610
|
-
// Skip assistant messages that are just tool_calls with no text
|
|
660
|
+
if (msg.role === "assistant") {
|
|
611
661
|
const text = textContent(msg.content);
|
|
612
|
-
if (
|
|
613
|
-
|
|
614
|
-
|
|
662
|
+
if (text) {
|
|
663
|
+
currentTurn.segments.push({ kind: "assistantText", text });
|
|
664
|
+
}
|
|
665
|
+
if (msg.tool_calls?.length) {
|
|
666
|
+
currentTurn.segments.push({
|
|
667
|
+
kind: "assistantToolCalls",
|
|
668
|
+
toolCalls: msg.tool_calls,
|
|
669
|
+
});
|
|
615
670
|
}
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
if (msg.role === "tool") {
|
|
674
|
+
currentTurn.segments.push({
|
|
675
|
+
kind: "toolResult",
|
|
676
|
+
result: {
|
|
677
|
+
toolCallId: msg.tool_call_id ?? "",
|
|
678
|
+
content: textContent(msg.content),
|
|
679
|
+
},
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
if (currentTurn)
|
|
684
|
+
parsedTurns.push(currentTurn);
|
|
685
|
+
let userText = "";
|
|
686
|
+
let toolResults = [];
|
|
687
|
+
let pendingAssistantSummary = "";
|
|
688
|
+
let completedTurnStates = parsedTurns;
|
|
689
|
+
const lastTurn = parsedTurns.at(-1);
|
|
690
|
+
if (lastTurn) {
|
|
691
|
+
const trailingSegments = splitTrailingToolResults(lastTurn.segments);
|
|
692
|
+
const hasAssistantSummary = trailingSegments.base.length > 0;
|
|
693
|
+
if (trailingSegments.trailing.length > 0 && hasAssistantSummary) {
|
|
694
|
+
completedTurnStates = parsedTurns.slice(0, -1);
|
|
695
|
+
userText = lastTurn.userText;
|
|
696
|
+
toolResults = trailingSegments.trailing.map((segment) => segment.result);
|
|
697
|
+
pendingAssistantSummary = summarizeTurnSegments(trailingSegments.base);
|
|
698
|
+
}
|
|
699
|
+
else if (lastTurn.userText && lastTurn.segments.length === 0) {
|
|
700
|
+
completedTurnStates = parsedTurns.slice(0, -1);
|
|
701
|
+
userText = lastTurn.userText;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
const turns = completedTurnStates
|
|
705
|
+
.map((turn) => ({
|
|
706
|
+
userText: turn.userText,
|
|
707
|
+
assistantText: summarizeTurnSegments(turn.segments),
|
|
708
|
+
}))
|
|
709
|
+
.filter((turn) => turn.userText || turn.assistantText);
|
|
710
|
+
return {
|
|
711
|
+
systemPrompt,
|
|
712
|
+
userText,
|
|
713
|
+
turns,
|
|
714
|
+
toolResults,
|
|
715
|
+
pendingAssistantSummary,
|
|
716
|
+
completedTurnsFingerprint: buildCompletedTurnsFingerprint(systemPrompt, turns),
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
function splitTrailingToolResults(segments) {
|
|
720
|
+
let index = segments.length;
|
|
721
|
+
while (index > 0 && segments[index - 1]?.kind === "toolResult") {
|
|
722
|
+
index -= 1;
|
|
723
|
+
}
|
|
724
|
+
return {
|
|
725
|
+
base: segments.slice(0, index),
|
|
726
|
+
trailing: segments.slice(index).filter((segment) => segment.kind === "toolResult"),
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
function summarizeTurnSegments(segments) {
|
|
730
|
+
const parts = [];
|
|
731
|
+
for (const segment of segments) {
|
|
732
|
+
if (segment.kind === "assistantText") {
|
|
733
|
+
const trimmed = segment.text.trim();
|
|
734
|
+
if (trimmed)
|
|
735
|
+
parts.push(trimmed);
|
|
736
|
+
continue;
|
|
616
737
|
}
|
|
738
|
+
if (segment.kind === "assistantToolCalls") {
|
|
739
|
+
const summary = segment.toolCalls.map(formatToolCallSummary).join("\n\n");
|
|
740
|
+
if (summary)
|
|
741
|
+
parts.push(summary);
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
parts.push(formatToolResultSummary(segment.result));
|
|
745
|
+
}
|
|
746
|
+
return parts.join("\n\n").trim();
|
|
747
|
+
}
|
|
748
|
+
function formatToolCallSummary(call) {
|
|
749
|
+
const args = call.function.arguments?.trim();
|
|
750
|
+
return args
|
|
751
|
+
? `[assistant requested tool ${call.function.name} id=${call.id}]\n${args}`
|
|
752
|
+
: `[assistant requested tool ${call.function.name} id=${call.id}]`;
|
|
753
|
+
}
|
|
754
|
+
function formatToolResultSummary(result) {
|
|
755
|
+
const label = result.toolCallId
|
|
756
|
+
? `[tool result id=${result.toolCallId}]`
|
|
757
|
+
: "[tool result]";
|
|
758
|
+
const content = result.content.trim();
|
|
759
|
+
return content ? `${label}\n${content}` : label;
|
|
760
|
+
}
|
|
761
|
+
function buildCompletedTurnsFingerprint(systemPrompt, turns) {
|
|
762
|
+
return hashString(JSON.stringify({ systemPrompt, turns }));
|
|
763
|
+
}
|
|
764
|
+
function buildToolResumePrompt(userText, pendingAssistantSummary, toolResults) {
|
|
765
|
+
const parts = [userText.trim()];
|
|
766
|
+
if (pendingAssistantSummary.trim()) {
|
|
767
|
+
parts.push(`[previous assistant tool activity]\n${pendingAssistantSummary.trim()}`);
|
|
768
|
+
}
|
|
769
|
+
if (toolResults.length > 0) {
|
|
770
|
+
parts.push(toolResults.map(formatToolResultSummary).join("\n\n"));
|
|
771
|
+
}
|
|
772
|
+
return parts.filter(Boolean).join("\n\n");
|
|
773
|
+
}
|
|
774
|
+
function selectToolsForChoice(tools, toolChoice) {
|
|
775
|
+
if (!tools.length)
|
|
776
|
+
return [];
|
|
777
|
+
if (toolChoice === undefined || toolChoice === null || toolChoice === "auto" || toolChoice === "required") {
|
|
778
|
+
return tools;
|
|
617
779
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
lastUserText = pendingUser;
|
|
780
|
+
if (toolChoice === "none") {
|
|
781
|
+
return [];
|
|
621
782
|
}
|
|
622
|
-
|
|
623
|
-
const
|
|
624
|
-
|
|
783
|
+
if (typeof toolChoice === "object") {
|
|
784
|
+
const choice = toolChoice;
|
|
785
|
+
if (choice.type === "function" && typeof choice.function?.name === "string") {
|
|
786
|
+
return tools.filter((tool) => tool.function.name === choice.function.name);
|
|
787
|
+
}
|
|
625
788
|
}
|
|
626
|
-
return
|
|
789
|
+
return tools;
|
|
627
790
|
}
|
|
628
791
|
/** Convert OpenAI tool definitions to Cursor's MCP tool protobuf format. */
|
|
629
792
|
function buildMcpToolDefinitions(tools) {
|
|
@@ -1084,19 +1247,34 @@ function sendExecResult(execMsg, messageCase, value, sendFrame) {
|
|
|
1084
1247
|
});
|
|
1085
1248
|
sendFrame(toBinary(AgentClientMessageSchema, clientMessage));
|
|
1086
1249
|
}
|
|
1087
|
-
/** Derive a key for active bridge lookup (tool-call continuations).
|
|
1088
|
-
function deriveBridgeKey(modelId, messages) {
|
|
1250
|
+
/** Derive a key for active bridge lookup (tool-call continuations). */
|
|
1251
|
+
function deriveBridgeKey(modelId, messages, sessionId, agentKey) {
|
|
1252
|
+
if (sessionId) {
|
|
1253
|
+
const normalizedAgent = normalizeAgentKey(agentKey);
|
|
1254
|
+
return createHash("sha256")
|
|
1255
|
+
.update(`bridge:${sessionId}:${normalizedAgent}`)
|
|
1256
|
+
.digest("hex")
|
|
1257
|
+
.slice(0, 16);
|
|
1258
|
+
}
|
|
1089
1259
|
const firstUserMsg = messages.find((m) => m.role === "user");
|
|
1090
1260
|
const firstUserText = firstUserMsg ? textContent(firstUserMsg.content) : "";
|
|
1261
|
+
const normalizedAgent = normalizeAgentKey(agentKey);
|
|
1091
1262
|
return createHash("sha256")
|
|
1092
|
-
.update(`bridge:${modelId}:${firstUserText.slice(0, 200)}`)
|
|
1263
|
+
.update(`bridge:${normalizedAgent}:${modelId}:${firstUserText.slice(0, 200)}`)
|
|
1093
1264
|
.digest("hex")
|
|
1094
1265
|
.slice(0, 16);
|
|
1095
1266
|
}
|
|
1096
1267
|
/** Derive a key for conversation state. Model-independent so context survives model switches. */
|
|
1097
|
-
function deriveConversationKey(messages) {
|
|
1268
|
+
function deriveConversationKey(messages, sessionId, agentKey) {
|
|
1269
|
+
if (sessionId) {
|
|
1270
|
+
const normalizedAgent = normalizeAgentKey(agentKey);
|
|
1271
|
+
return createHash("sha256")
|
|
1272
|
+
.update(`session:${sessionId}:${normalizedAgent}`)
|
|
1273
|
+
.digest("hex")
|
|
1274
|
+
.slice(0, 16);
|
|
1275
|
+
}
|
|
1098
1276
|
return createHash("sha256")
|
|
1099
|
-
.update(buildConversationFingerprint(messages))
|
|
1277
|
+
.update(`${normalizeAgentKey(agentKey)}:${buildConversationFingerprint(messages)}`)
|
|
1100
1278
|
.digest("hex")
|
|
1101
1279
|
.slice(0, 16);
|
|
1102
1280
|
}
|
|
@@ -1106,8 +1284,19 @@ function buildConversationFingerprint(messages) {
|
|
|
1106
1284
|
return `${message.role}:${textContent(message.content)}:${message.tool_call_id ?? ""}:${toolCallIDs}`;
|
|
1107
1285
|
}).join("\n---\n");
|
|
1108
1286
|
}
|
|
1287
|
+
function updateStoredConversationAfterCompletion(convKey, metadata, assistantText) {
|
|
1288
|
+
const stored = conversationStates.get(convKey);
|
|
1289
|
+
if (!stored)
|
|
1290
|
+
return;
|
|
1291
|
+
const nextTurns = metadata.userText
|
|
1292
|
+
? [...metadata.turns, { userText: metadata.userText, assistantText: assistantText.trim() }]
|
|
1293
|
+
: metadata.turns;
|
|
1294
|
+
stored.systemPromptHash = metadata.systemPromptHash;
|
|
1295
|
+
stored.completedTurnsFingerprint = buildCompletedTurnsFingerprint(metadata.systemPrompt, nextTurns);
|
|
1296
|
+
stored.lastAccessMs = Date.now();
|
|
1297
|
+
}
|
|
1109
1298
|
/** Create an SSE streaming Response that reads from a live bridge. */
|
|
1110
|
-
function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey) {
|
|
1299
|
+
function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey, metadata) {
|
|
1111
1300
|
const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
|
|
1112
1301
|
const created = Math.floor(Date.now() / 1000);
|
|
1113
1302
|
const stream = new ReadableStream({
|
|
@@ -1155,6 +1344,7 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
1155
1344
|
totalTokens: 0,
|
|
1156
1345
|
};
|
|
1157
1346
|
const tagFilter = createThinkingTagFilter();
|
|
1347
|
+
let assistantText = metadata.assistantSeedText ?? "";
|
|
1158
1348
|
let mcpExecReceived = false;
|
|
1159
1349
|
let endStreamError = null;
|
|
1160
1350
|
const processChunk = createConnectFrameParser((messageBytes) => {
|
|
@@ -1168,8 +1358,10 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
1168
1358
|
const { content, reasoning } = tagFilter.process(text);
|
|
1169
1359
|
if (reasoning)
|
|
1170
1360
|
sendSSE(makeChunk({ reasoning_content: reasoning }));
|
|
1171
|
-
if (content)
|
|
1361
|
+
if (content) {
|
|
1362
|
+
assistantText += content;
|
|
1172
1363
|
sendSSE(makeChunk({ content }));
|
|
1364
|
+
}
|
|
1173
1365
|
}
|
|
1174
1366
|
},
|
|
1175
1367
|
// onMcpExec — the model wants to execute a tool.
|
|
@@ -1179,8 +1371,21 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
1179
1371
|
const flushed = tagFilter.flush();
|
|
1180
1372
|
if (flushed.reasoning)
|
|
1181
1373
|
sendSSE(makeChunk({ reasoning_content: flushed.reasoning }));
|
|
1182
|
-
if (flushed.content)
|
|
1374
|
+
if (flushed.content) {
|
|
1375
|
+
assistantText += flushed.content;
|
|
1183
1376
|
sendSSE(makeChunk({ content: flushed.content }));
|
|
1377
|
+
}
|
|
1378
|
+
const assistantSeedText = [
|
|
1379
|
+
assistantText.trim(),
|
|
1380
|
+
formatToolCallSummary({
|
|
1381
|
+
id: exec.toolCallId,
|
|
1382
|
+
type: "function",
|
|
1383
|
+
function: {
|
|
1384
|
+
name: exec.toolName,
|
|
1385
|
+
arguments: exec.decodedArgs,
|
|
1386
|
+
},
|
|
1387
|
+
}),
|
|
1388
|
+
].filter(Boolean).join("\n\n");
|
|
1184
1389
|
const toolCallIndex = state.toolCallIndex++;
|
|
1185
1390
|
sendSSE(makeChunk({
|
|
1186
1391
|
tool_calls: [{
|
|
@@ -1200,6 +1405,11 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
1200
1405
|
blobStore,
|
|
1201
1406
|
mcpTools,
|
|
1202
1407
|
pendingExecs: state.pendingExecs,
|
|
1408
|
+
modelId,
|
|
1409
|
+
metadata: {
|
|
1410
|
+
...metadata,
|
|
1411
|
+
assistantSeedText,
|
|
1412
|
+
},
|
|
1203
1413
|
});
|
|
1204
1414
|
sendSSE(makeChunk({}, "tool_calls"));
|
|
1205
1415
|
sendDone();
|
|
@@ -1248,8 +1458,11 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
1248
1458
|
const flushed = tagFilter.flush();
|
|
1249
1459
|
if (flushed.reasoning)
|
|
1250
1460
|
sendSSE(makeChunk({ reasoning_content: flushed.reasoning }));
|
|
1251
|
-
if (flushed.content)
|
|
1461
|
+
if (flushed.content) {
|
|
1462
|
+
assistantText += flushed.content;
|
|
1252
1463
|
sendSSE(makeChunk({ content: flushed.content }));
|
|
1464
|
+
}
|
|
1465
|
+
updateStoredConversationAfterCompletion(convKey, metadata, assistantText);
|
|
1253
1466
|
sendSSE(makeChunk({}, "stop"));
|
|
1254
1467
|
sendSSE(makeUsageChunk());
|
|
1255
1468
|
sendDone();
|
|
@@ -1283,13 +1496,20 @@ async function startBridge(accessToken, requestBytes) {
|
|
|
1283
1496
|
const heartbeatTimer = setInterval(() => bridge.write(makeHeartbeatBytes()), 5_000);
|
|
1284
1497
|
return { bridge, heartbeatTimer };
|
|
1285
1498
|
}
|
|
1286
|
-
async function handleStreamingResponse(payload, accessToken, modelId, bridgeKey, convKey) {
|
|
1499
|
+
async function handleStreamingResponse(payload, accessToken, modelId, bridgeKey, convKey, metadata) {
|
|
1287
1500
|
const { bridge, heartbeatTimer } = await startBridge(accessToken, payload.requestBytes);
|
|
1288
|
-
return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.mcpTools, modelId, bridgeKey, convKey);
|
|
1501
|
+
return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.mcpTools, modelId, bridgeKey, convKey, metadata);
|
|
1289
1502
|
}
|
|
1290
1503
|
/** Resume a paused bridge by sending MCP results and continuing to stream. */
|
|
1291
|
-
function handleToolResultResume(active, toolResults,
|
|
1292
|
-
const { bridge, heartbeatTimer, blobStore, mcpTools, pendingExecs } = active;
|
|
1504
|
+
function handleToolResultResume(active, toolResults, bridgeKey, convKey) {
|
|
1505
|
+
const { bridge, heartbeatTimer, blobStore, mcpTools, pendingExecs, modelId, metadata } = active;
|
|
1506
|
+
const resumeMetadata = {
|
|
1507
|
+
...metadata,
|
|
1508
|
+
assistantSeedText: [
|
|
1509
|
+
metadata.assistantSeedText?.trim() ?? "",
|
|
1510
|
+
toolResults.map(formatToolResultSummary).join("\n\n"),
|
|
1511
|
+
].filter(Boolean).join("\n\n"),
|
|
1512
|
+
};
|
|
1293
1513
|
// Send mcpResult for each pending exec that has a matching tool result
|
|
1294
1514
|
for (const exec of pendingExecs) {
|
|
1295
1515
|
const result = toolResults.find((r) => r.toolCallId === exec.toolCallId);
|
|
@@ -1329,12 +1549,15 @@ function handleToolResultResume(active, toolResults, modelId, bridgeKey, convKey
|
|
|
1329
1549
|
});
|
|
1330
1550
|
bridge.write(toBinary(AgentClientMessageSchema, clientMessage));
|
|
1331
1551
|
}
|
|
1332
|
-
return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey);
|
|
1552
|
+
return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey, resumeMetadata);
|
|
1333
1553
|
}
|
|
1334
|
-
async function handleNonStreamingResponse(payload, accessToken, modelId, convKey) {
|
|
1554
|
+
async function handleNonStreamingResponse(payload, accessToken, modelId, convKey, metadata) {
|
|
1335
1555
|
const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
|
|
1336
1556
|
const created = Math.floor(Date.now() / 1000);
|
|
1337
|
-
const { text, usage } = await collectFullResponse(payload, accessToken, modelId, convKey);
|
|
1557
|
+
const { text, usage, finishReason, toolCalls } = await collectFullResponse(payload, accessToken, modelId, convKey, metadata);
|
|
1558
|
+
const message = finishReason === "tool_calls"
|
|
1559
|
+
? { role: "assistant", content: null, tool_calls: toolCalls }
|
|
1560
|
+
: { role: "assistant", content: text };
|
|
1338
1561
|
return new Response(JSON.stringify({
|
|
1339
1562
|
id: completionId,
|
|
1340
1563
|
object: "chat.completion",
|
|
@@ -1343,17 +1566,18 @@ async function handleNonStreamingResponse(payload, accessToken, modelId, convKey
|
|
|
1343
1566
|
choices: [
|
|
1344
1567
|
{
|
|
1345
1568
|
index: 0,
|
|
1346
|
-
message
|
|
1347
|
-
finish_reason:
|
|
1569
|
+
message,
|
|
1570
|
+
finish_reason: finishReason,
|
|
1348
1571
|
},
|
|
1349
1572
|
],
|
|
1350
1573
|
usage,
|
|
1351
1574
|
}), { headers: { "Content-Type": "application/json" } });
|
|
1352
1575
|
}
|
|
1353
|
-
async function collectFullResponse(payload, accessToken, modelId, convKey) {
|
|
1576
|
+
async function collectFullResponse(payload, accessToken, modelId, convKey, metadata) {
|
|
1354
1577
|
const { promise, resolve, reject } = Promise.withResolvers();
|
|
1355
1578
|
let fullText = "";
|
|
1356
1579
|
let endStreamError = null;
|
|
1580
|
+
const pendingToolCalls = [];
|
|
1357
1581
|
const { bridge, heartbeatTimer } = await startBridge(accessToken, payload.requestBytes);
|
|
1358
1582
|
const state = {
|
|
1359
1583
|
toolCallIndex: 0,
|
|
@@ -1370,7 +1594,17 @@ async function collectFullResponse(payload, accessToken, modelId, convKey) {
|
|
|
1370
1594
|
return;
|
|
1371
1595
|
const { content } = tagFilter.process(text);
|
|
1372
1596
|
fullText += content;
|
|
1373
|
-
}, () => {
|
|
1597
|
+
}, (exec) => {
|
|
1598
|
+
pendingToolCalls.push({
|
|
1599
|
+
id: exec.toolCallId,
|
|
1600
|
+
type: "function",
|
|
1601
|
+
function: {
|
|
1602
|
+
name: exec.toolName,
|
|
1603
|
+
arguments: exec.decodedArgs,
|
|
1604
|
+
},
|
|
1605
|
+
});
|
|
1606
|
+
scheduleBridgeEnd(bridge);
|
|
1607
|
+
}, (checkpointBytes) => {
|
|
1374
1608
|
const stored = conversationStates.get(convKey);
|
|
1375
1609
|
if (stored) {
|
|
1376
1610
|
stored.checkpoint = checkpointBytes;
|
|
@@ -1406,10 +1640,15 @@ async function collectFullResponse(payload, accessToken, modelId, convKey) {
|
|
|
1406
1640
|
reject(endStreamError);
|
|
1407
1641
|
return;
|
|
1408
1642
|
}
|
|
1643
|
+
if (pendingToolCalls.length === 0) {
|
|
1644
|
+
updateStoredConversationAfterCompletion(convKey, metadata, fullText);
|
|
1645
|
+
}
|
|
1409
1646
|
const usage = computeUsage(state);
|
|
1410
1647
|
resolve({
|
|
1411
1648
|
text: fullText,
|
|
1412
1649
|
usage,
|
|
1650
|
+
finishReason: pendingToolCalls.length > 0 ? "tool_calls" : "stop",
|
|
1651
|
+
toolCalls: pendingToolCalls,
|
|
1413
1652
|
});
|
|
1414
1653
|
});
|
|
1415
1654
|
return promise;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playwo/opencode-cursor-oauth",
|
|
3
|
-
"version": "0.0.0-dev.
|
|
3
|
+
"version": "0.0.0-dev.fc97acd8b777",
|
|
4
4
|
"description": "OpenCode plugin that connects Cursor's API to OpenCode via OAuth, model discovery, and a local OpenAI-compatible proxy.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|