@oh-my-pi/pi-ai 17.2.0 → 17.2.2
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/CHANGELOG.md +27 -0
- package/dist/types/auth-storage.d.ts +15 -1
- package/dist/types/providers/anthropic.d.ts +8 -10
- package/dist/types/providers/claude-code-fingerprint.d.ts +12 -8
- package/dist/types/providers/cowork-fetch.d.ts +3 -0
- package/dist/types/providers/cursor.d.ts +1 -1
- package/dist/types/providers/transform-messages.d.ts +18 -0
- package/dist/types/registry/gmi-cloud.d.ts +7 -0
- package/dist/types/registry/registry.d.ts +4 -0
- package/dist/types/usage/openai-codex-reset.d.ts +9 -0
- package/dist/types/utils/proxy.d.ts +2 -0
- package/package.json +4 -4
- package/src/auth-storage.ts +58 -6
- package/src/providers/anthropic.ts +49 -69
- package/src/providers/claude-code-fingerprint.ts +12 -11
- package/src/providers/cowork-fetch.ts +201 -0
- package/src/providers/cursor.ts +240 -56
- package/src/providers/transform-messages.ts +9 -1
- package/src/registry/gmi-cloud.ts +22 -0
- package/src/registry/registry.ts +2 -0
- package/src/stream.ts +12 -4
- package/src/usage/openai-codex-reset.ts +27 -0
- package/src/utils/proxy.ts +6 -2
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { ClientRequest, IncomingMessage } from "node:http";
|
|
2
|
+
import * as https from "node:https";
|
|
3
|
+
import * as stream from "node:stream";
|
|
4
|
+
import * as tls from "node:tls";
|
|
5
|
+
import * as zlib from "node:zlib";
|
|
6
|
+
import type { FetchImpl } from "../types";
|
|
7
|
+
import { connectProxiedSocket } from "../utils/proxy";
|
|
8
|
+
|
|
9
|
+
type CoworkTlsOptions = {
|
|
10
|
+
ca?: string | string[];
|
|
11
|
+
cert?: string;
|
|
12
|
+
key?: string;
|
|
13
|
+
rejectUnauthorized?: boolean;
|
|
14
|
+
serverName?: string;
|
|
15
|
+
ciphers?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type CoworkRequestInit = RequestInit & {
|
|
19
|
+
proxy?: string;
|
|
20
|
+
tls?: CoworkTlsOptions;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type RequestBody = string | Uint8Array;
|
|
24
|
+
|
|
25
|
+
type AgentLease = {
|
|
26
|
+
agent: https.Agent;
|
|
27
|
+
release?: () => void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const directAgent = new https.Agent({ keepAlive: true });
|
|
31
|
+
const fallbackFetch: FetchImpl = globalThis.fetch;
|
|
32
|
+
|
|
33
|
+
function isHeaderRecord(headers: RequestInit["headers"]): headers is Record<string, string> {
|
|
34
|
+
return headers !== undefined && !(headers instanceof Headers) && !Array.isArray(headers);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveBody(body: RequestInit["body"]): RequestBody | undefined {
|
|
38
|
+
if (typeof body === "string" || body instanceof Uint8Array) return body;
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildOrderedHeaders(
|
|
43
|
+
url: URL,
|
|
44
|
+
source: Record<string, string>,
|
|
45
|
+
body: RequestBody | undefined,
|
|
46
|
+
): Record<string, string> {
|
|
47
|
+
const headers: Record<string, string> = {};
|
|
48
|
+
let hasHost = false;
|
|
49
|
+
let hasContentLength = false;
|
|
50
|
+
for (const name in source) {
|
|
51
|
+
const lowerName = name.toLowerCase();
|
|
52
|
+
if (lowerName === "host") hasHost = true;
|
|
53
|
+
if (lowerName === "content-length") hasContentLength = true;
|
|
54
|
+
if (lowerName === "accept-encoding" && !hasHost) {
|
|
55
|
+
headers.Host = url.host;
|
|
56
|
+
hasHost = true;
|
|
57
|
+
}
|
|
58
|
+
headers[name] = source[name];
|
|
59
|
+
}
|
|
60
|
+
if (!hasHost) headers.Host = url.host;
|
|
61
|
+
const length = typeof body === "string" ? Buffer.byteLength(body) : body?.byteLength;
|
|
62
|
+
if (!hasContentLength && length !== undefined) headers["Content-Length"] = String(length);
|
|
63
|
+
return headers;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function resolveTlsOptions(url: URL, options: CoworkTlsOptions | undefined): tls.ConnectionOptions {
|
|
67
|
+
const resolved: tls.ConnectionOptions = {
|
|
68
|
+
ALPNProtocols: ["http/1.1"],
|
|
69
|
+
ciphers: options?.ciphers ?? tls.DEFAULT_CIPHERS,
|
|
70
|
+
rejectUnauthorized: options?.rejectUnauthorized ?? true,
|
|
71
|
+
servername: options?.serverName ?? url.hostname,
|
|
72
|
+
};
|
|
73
|
+
if (options?.ca !== undefined) resolved.ca = options.ca;
|
|
74
|
+
if (options?.cert !== undefined) resolved.cert = options.cert;
|
|
75
|
+
if (options?.key !== undefined) resolved.key = options.key;
|
|
76
|
+
return resolved;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function acquireAgent(
|
|
80
|
+
url: URL,
|
|
81
|
+
proxy: string | undefined,
|
|
82
|
+
tlsOptions: tls.ConnectionOptions,
|
|
83
|
+
signal: AbortSignal | undefined,
|
|
84
|
+
): Promise<AgentLease> {
|
|
85
|
+
if (!proxy) return { agent: directAgent };
|
|
86
|
+
const socket = await connectProxiedSocket(proxy, url.origin, { signal, tls: tlsOptions });
|
|
87
|
+
const agent = new https.Agent({ keepAlive: false });
|
|
88
|
+
agent.createConnection = () => socket;
|
|
89
|
+
return { agent, release: () => agent.destroy() };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function responseHeaders(message: IncomingMessage): Headers {
|
|
93
|
+
const headers = new Headers();
|
|
94
|
+
for (let index = 0; index < message.rawHeaders.length; index += 2) {
|
|
95
|
+
headers.append(message.rawHeaders[index], message.rawHeaders[index + 1]);
|
|
96
|
+
}
|
|
97
|
+
return headers;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function decodedResponseStream(message: IncomingMessage): stream.Readable {
|
|
101
|
+
const rawEncoding = message.headers["content-encoding"];
|
|
102
|
+
const encoding = (Array.isArray(rawEncoding) ? rawEncoding[0] : rawEncoding)?.trim().toLowerCase();
|
|
103
|
+
switch (encoding) {
|
|
104
|
+
case "gzip":
|
|
105
|
+
return message.pipe(zlib.createGunzip());
|
|
106
|
+
case "deflate":
|
|
107
|
+
return message.pipe(zlib.createInflate());
|
|
108
|
+
case "br":
|
|
109
|
+
return message.pipe(zlib.createBrotliDecompress());
|
|
110
|
+
case "zstd":
|
|
111
|
+
return message.pipe(zlib.createZstdDecompress());
|
|
112
|
+
default:
|
|
113
|
+
return message;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function createResponse(message: IncomingMessage, method: string): Response {
|
|
118
|
+
const status = message.statusCode;
|
|
119
|
+
if (status === undefined) throw new Error("Cowork transport received a response without an HTTP status.");
|
|
120
|
+
const hasBody = method !== "HEAD" && status !== 204 && status !== 304;
|
|
121
|
+
const body = hasBody ? stream.Readable.toWeb(decodedResponseStream(message)) : null;
|
|
122
|
+
return new Response(body, {
|
|
123
|
+
status,
|
|
124
|
+
statusText: message.statusMessage,
|
|
125
|
+
headers: responseHeaders(message),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function sendCoworkRequest(
|
|
130
|
+
url: URL,
|
|
131
|
+
init: CoworkRequestInit,
|
|
132
|
+
sourceHeaders: Record<string, string>,
|
|
133
|
+
body: RequestBody | undefined,
|
|
134
|
+
): Promise<Response> {
|
|
135
|
+
const method = init.method ?? "GET";
|
|
136
|
+
const signal = init.signal ?? undefined;
|
|
137
|
+
const tlsOptions = resolveTlsOptions(url, init.tls);
|
|
138
|
+
const lease = await acquireAgent(url, init.proxy, tlsOptions, signal);
|
|
139
|
+
const headers = buildOrderedHeaders(url, sourceHeaders, body);
|
|
140
|
+
const result = Promise.withResolvers<Response>();
|
|
141
|
+
let request: ClientRequest | undefined;
|
|
142
|
+
const release = (): void => {
|
|
143
|
+
signal?.removeEventListener("abort", abort);
|
|
144
|
+
lease.release?.();
|
|
145
|
+
};
|
|
146
|
+
const abort = (): void => {
|
|
147
|
+
const reason = signal?.reason;
|
|
148
|
+
request?.destroy(reason instanceof Error ? reason : new DOMException("The operation was aborted.", "AbortError"));
|
|
149
|
+
};
|
|
150
|
+
if (signal?.aborted) {
|
|
151
|
+
release();
|
|
152
|
+
signal.throwIfAborted();
|
|
153
|
+
}
|
|
154
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
155
|
+
request = https.request(
|
|
156
|
+
{
|
|
157
|
+
protocol: url.protocol,
|
|
158
|
+
hostname: url.hostname,
|
|
159
|
+
port: url.port || 443,
|
|
160
|
+
path: `${url.pathname}${url.search}`,
|
|
161
|
+
method,
|
|
162
|
+
headers,
|
|
163
|
+
agent: lease.agent,
|
|
164
|
+
...tlsOptions,
|
|
165
|
+
},
|
|
166
|
+
message => {
|
|
167
|
+
message.once("close", release);
|
|
168
|
+
try {
|
|
169
|
+
result.resolve(createResponse(message, method));
|
|
170
|
+
} catch (error) {
|
|
171
|
+
message.destroy();
|
|
172
|
+
release();
|
|
173
|
+
result.reject(error);
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
request.once("error", error => {
|
|
178
|
+
release();
|
|
179
|
+
result.reject(error);
|
|
180
|
+
});
|
|
181
|
+
request.end(body);
|
|
182
|
+
return result.promise;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Sends Cowork-profiled HTTPS requests with stable header order, HTTP/1.1, and streaming decompression. */
|
|
186
|
+
export const coworkFetch: FetchImpl = async (input, init) => {
|
|
187
|
+
if (input instanceof Request || init === undefined || !isHeaderRecord(init.headers)) {
|
|
188
|
+
return fallbackFetch(input, init);
|
|
189
|
+
}
|
|
190
|
+
let url: URL;
|
|
191
|
+
try {
|
|
192
|
+
url = new URL(input);
|
|
193
|
+
} catch {
|
|
194
|
+
return fallbackFetch(input, init);
|
|
195
|
+
}
|
|
196
|
+
if (url.protocol !== "https:") return fallbackFetch(input, init);
|
|
197
|
+
const body = resolveBody(init.body);
|
|
198
|
+
if (init.body != null && body === undefined) return fallbackFetch(input, init);
|
|
199
|
+
const coworkInit: CoworkRequestInit = init;
|
|
200
|
+
return sendCoworkRequest(url, coworkInit, init.headers, body);
|
|
201
|
+
};
|
package/src/providers/cursor.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as fs from "node:fs/promises";
|
|
|
3
3
|
import http2 from "node:http2";
|
|
4
4
|
import { create, fromBinary, fromJson, type JsonValue, toBinary, toJson } from "@bufbuild/protobuf";
|
|
5
5
|
import { ValueSchema } from "@bufbuild/protobuf/wkt";
|
|
6
|
-
import type { McpToolDefinition } from "@oh-my-pi/pi-catalog/discovery/cursor-gen/agent_pb";
|
|
6
|
+
import type { ConversationStep, McpToolDefinition } from "@oh-my-pi/pi-catalog/discovery/cursor-gen/agent_pb";
|
|
7
7
|
import {
|
|
8
8
|
AgentClientMessageSchema,
|
|
9
9
|
AgentConversationTurnStructureSchema,
|
|
@@ -76,15 +76,19 @@ import {
|
|
|
76
76
|
LsSuccessSchema,
|
|
77
77
|
McpAllowlistPrecheckResultSchema,
|
|
78
78
|
McpApprovedSchema,
|
|
79
|
+
McpArgsSchema,
|
|
79
80
|
McpErrorSchema,
|
|
80
81
|
McpImageContentSchema,
|
|
81
82
|
McpRejectedSchema,
|
|
82
83
|
McpResultSchema,
|
|
83
84
|
McpSuccessSchema,
|
|
84
85
|
McpTextContentSchema,
|
|
86
|
+
McpToolCallSchema,
|
|
85
87
|
McpToolDefinitionSchema,
|
|
88
|
+
McpToolErrorSchema,
|
|
86
89
|
McpToolNotFoundSchema,
|
|
87
90
|
McpToolResultContentItemSchema,
|
|
91
|
+
McpToolResultSchema,
|
|
88
92
|
ModelDetailsSchema,
|
|
89
93
|
ReadErrorSchema,
|
|
90
94
|
ReadMcpResourceErrorSchema,
|
|
@@ -124,6 +128,8 @@ import {
|
|
|
124
128
|
SubagentAwaitResultSchema,
|
|
125
129
|
SubagentErrorSchema,
|
|
126
130
|
SubagentResultSchema,
|
|
131
|
+
ThinkingMessageSchema,
|
|
132
|
+
ToolCallSchema,
|
|
127
133
|
UserMessageActionSchema,
|
|
128
134
|
UserMessageSchema,
|
|
129
135
|
WebFetchAllowlistPrecheckResultSchema,
|
|
@@ -134,6 +140,7 @@ import {
|
|
|
134
140
|
WriteShellStdinResultSchema,
|
|
135
141
|
WriteSuccessSchema,
|
|
136
142
|
} from "@oh-my-pi/pi-catalog/discovery/cursor-gen/agent_pb";
|
|
143
|
+
import { isKimiK3ModelId } from "@oh-my-pi/pi-catalog/identity";
|
|
137
144
|
import { calculateCost } from "@oh-my-pi/pi-catalog/models";
|
|
138
145
|
import {
|
|
139
146
|
$env,
|
|
@@ -3914,9 +3921,8 @@ function handleConversationCheckpointUpdate(
|
|
|
3914
3921
|
if (usedTokens <= 0) {
|
|
3915
3922
|
return;
|
|
3916
3923
|
}
|
|
3917
|
-
if (output.usage.
|
|
3918
|
-
output.usage.
|
|
3919
|
-
output.usage.totalTokens = output.usage.input + output.usage.output;
|
|
3924
|
+
if (output.usage.contextTokens !== usedTokens) {
|
|
3925
|
+
output.usage.contextTokens = usedTokens;
|
|
3920
3926
|
}
|
|
3921
3927
|
}
|
|
3922
3928
|
|
|
@@ -4047,16 +4053,74 @@ function cursorUserContentKey(content: string | (TextContent | ImageContent)[]):
|
|
|
4047
4053
|
return hash.digest("hex");
|
|
4048
4054
|
}
|
|
4049
4055
|
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4056
|
+
type CursorRootPromptAssistantContentPart =
|
|
4057
|
+
| { type: "text"; text: string }
|
|
4058
|
+
| {
|
|
4059
|
+
type: "reasoning";
|
|
4060
|
+
text: string;
|
|
4061
|
+
providerOptions: { cursor: { modelName: string } };
|
|
4062
|
+
signature?: string;
|
|
4063
|
+
}
|
|
4064
|
+
| { type: "tool-call"; toolCallId: string; toolName: string; args: Record<string, unknown> };
|
|
4065
|
+
|
|
4066
|
+
function canReplayCursorThinking(msg: AssistantMessage, targetModelId: string | undefined): boolean {
|
|
4067
|
+
return (
|
|
4068
|
+
targetModelId !== undefined &&
|
|
4069
|
+
isKimiK3ModelId(targetModelId) &&
|
|
4070
|
+
msg.api === "cursor-agent" &&
|
|
4071
|
+
msg.provider === "cursor" &&
|
|
4072
|
+
msg.model === targetModelId
|
|
4073
|
+
);
|
|
4074
|
+
}
|
|
4075
|
+
|
|
4076
|
+
function buildCursorAssistantContent(
|
|
4077
|
+
msg: AssistantMessage,
|
|
4078
|
+
targetModelId: string | undefined,
|
|
4079
|
+
): CursorRootPromptAssistantContentPart[] {
|
|
4080
|
+
const content: CursorRootPromptAssistantContentPart[] = [];
|
|
4081
|
+
const replayThinking = canReplayCursorThinking(msg, targetModelId);
|
|
4082
|
+
for (const item of msg.content) {
|
|
4083
|
+
if (item.type === "text") {
|
|
4084
|
+
if (item.text) content.push({ type: "text", text: item.text });
|
|
4085
|
+
} else if (item.type === "thinking") {
|
|
4086
|
+
if (replayThinking && item.thinking) {
|
|
4087
|
+
content.push({
|
|
4088
|
+
type: "reasoning",
|
|
4089
|
+
text: item.thinking,
|
|
4090
|
+
providerOptions: { cursor: { modelName: msg.model } },
|
|
4091
|
+
...(item.thinkingSignature ? { signature: item.thinkingSignature } : {}),
|
|
4092
|
+
});
|
|
4093
|
+
}
|
|
4094
|
+
} else if (item.type === "toolCall") {
|
|
4095
|
+
content.push({
|
|
4096
|
+
type: "tool-call",
|
|
4097
|
+
toolCallId: item.id,
|
|
4098
|
+
toolName: item.name,
|
|
4099
|
+
args: item.arguments,
|
|
4100
|
+
});
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
return content;
|
|
4104
|
+
}
|
|
4105
|
+
|
|
4106
|
+
function assertCursorKimiK3HistoryReplayable(
|
|
4107
|
+
messages: Message[],
|
|
4108
|
+
activeUserMessageIndex: number,
|
|
4109
|
+
targetModelId: string | undefined,
|
|
4110
|
+
): void {
|
|
4111
|
+
if (!targetModelId || !isKimiK3ModelId(targetModelId)) return;
|
|
4112
|
+
const historyEnd = activeUserMessageIndex >= 0 ? activeUserMessageIndex : messages.length;
|
|
4113
|
+
for (let i = 0; i < historyEnd; i++) {
|
|
4114
|
+
const msg = messages[i];
|
|
4115
|
+
if (msg.role !== "assistant") continue;
|
|
4116
|
+
const isSameCursorModel = msg.api === "cursor-agent" && msg.provider === "cursor" && msg.model === targetModelId;
|
|
4117
|
+
const hasThinking = msg.content.some(item => item.type === "thinking" && item.thinking.length > 0);
|
|
4118
|
+
if (!isSameCursorModel || !hasThinking) {
|
|
4119
|
+
throw new AIError.ValidationError(
|
|
4120
|
+
`Cursor ${targetModelId} requires complete same-model thinking history; start a new session instead of continuing history from ${msg.provider}/${msg.model}.`,
|
|
4121
|
+
);
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
4060
4124
|
}
|
|
4061
4125
|
|
|
4062
4126
|
/**
|
|
@@ -4107,7 +4171,9 @@ function buildRootPromptMessagesJson(
|
|
|
4107
4171
|
systemPromptIds: Uint8Array[],
|
|
4108
4172
|
blobStore: Map<string, Uint8Array>,
|
|
4109
4173
|
activeUserMessageIndex = findLastUserMessageIndex(messages),
|
|
4174
|
+
targetModelId?: string,
|
|
4110
4175
|
): Uint8Array[] {
|
|
4176
|
+
assertCursorKimiK3HistoryReplayable(messages, activeUserMessageIndex, targetModelId);
|
|
4111
4177
|
const entries: Uint8Array[] = [...systemPromptIds];
|
|
4112
4178
|
const pushJson = (obj: unknown) => {
|
|
4113
4179
|
const bytes = new TextEncoder().encode(JSON.stringify(obj));
|
|
@@ -4122,16 +4188,24 @@ function buildRootPromptMessagesJson(
|
|
|
4122
4188
|
if (content.length === 0) continue;
|
|
4123
4189
|
pushJson({ role: "user", content });
|
|
4124
4190
|
} else if (msg.role === "assistant") {
|
|
4125
|
-
const
|
|
4126
|
-
if (
|
|
4127
|
-
pushJson({ role: "assistant", content
|
|
4191
|
+
const content = buildCursorAssistantContent(msg, targetModelId);
|
|
4192
|
+
if (content.length === 0) continue;
|
|
4193
|
+
pushJson({ role: "assistant", content });
|
|
4128
4194
|
} else if (msg.role === "toolResult") {
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
const prefix = msg.isError ? "[Tool Error]" : "[Tool Result]";
|
|
4195
|
+
// Emit even when the result text is empty: the assistant `tool-call` is
|
|
4196
|
+
// already in history, so dropping the pair would replay an orphaned call.
|
|
4132
4197
|
pushJson({
|
|
4133
|
-
role: "
|
|
4134
|
-
|
|
4198
|
+
role: "tool",
|
|
4199
|
+
id: msg.toolCallId,
|
|
4200
|
+
content: [
|
|
4201
|
+
{
|
|
4202
|
+
type: "tool-result",
|
|
4203
|
+
toolName: msg.toolName,
|
|
4204
|
+
toolCallId: msg.toolCallId,
|
|
4205
|
+
result: toolResultToText(msg),
|
|
4206
|
+
...(msg.isError ? { isError: true } : {}),
|
|
4207
|
+
},
|
|
4208
|
+
],
|
|
4135
4209
|
});
|
|
4136
4210
|
}
|
|
4137
4211
|
}
|
|
@@ -4139,6 +4213,91 @@ function buildRootPromptMessagesJson(
|
|
|
4139
4213
|
return entries;
|
|
4140
4214
|
}
|
|
4141
4215
|
|
|
4216
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
4217
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
4218
|
+
const prototype = Object.getPrototypeOf(value);
|
|
4219
|
+
return prototype === Object.prototype || prototype === null;
|
|
4220
|
+
}
|
|
4221
|
+
|
|
4222
|
+
function isJsonValue(value: unknown): value is JsonValue {
|
|
4223
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
4224
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
4225
|
+
if (Array.isArray(value)) return value.every(isJsonValue);
|
|
4226
|
+
if (!isPlainRecord(value)) return false;
|
|
4227
|
+
for (const key in value) {
|
|
4228
|
+
if (!isJsonValue(value[key])) return false;
|
|
4229
|
+
}
|
|
4230
|
+
return true;
|
|
4231
|
+
}
|
|
4232
|
+
|
|
4233
|
+
function encodeCursorMcpArguments(toolCall: ToolCall): Record<string, Uint8Array> {
|
|
4234
|
+
const encoded: Record<string, Uint8Array> = {};
|
|
4235
|
+
for (const name in toolCall.arguments) {
|
|
4236
|
+
const value = toolCall.arguments[name];
|
|
4237
|
+
if (value === undefined) continue;
|
|
4238
|
+
if (!isJsonValue(value)) {
|
|
4239
|
+
throw new AIError.ValidationError(`Cursor tool argument ${toolCall.name}.${name} is not JSON-serializable`);
|
|
4240
|
+
}
|
|
4241
|
+
encoded[name] = toBinary(ValueSchema, fromJson(ValueSchema, value));
|
|
4242
|
+
}
|
|
4243
|
+
return encoded;
|
|
4244
|
+
}
|
|
4245
|
+
|
|
4246
|
+
function createCursorMcpResult(result: ToolResultMessage) {
|
|
4247
|
+
if (result.isError) {
|
|
4248
|
+
return create(McpToolResultSchema, {
|
|
4249
|
+
result: {
|
|
4250
|
+
case: "error",
|
|
4251
|
+
value: create(McpToolErrorSchema, { error: toolResultToText(result) }),
|
|
4252
|
+
},
|
|
4253
|
+
});
|
|
4254
|
+
}
|
|
4255
|
+
return create(McpToolResultSchema, {
|
|
4256
|
+
result: {
|
|
4257
|
+
case: "success",
|
|
4258
|
+
value: create(McpSuccessSchema, {
|
|
4259
|
+
content: result.content.map(item =>
|
|
4260
|
+
item.type === "text"
|
|
4261
|
+
? create(McpToolResultContentItemSchema, {
|
|
4262
|
+
content: { case: "text", value: create(McpTextContentSchema, { text: item.text }) },
|
|
4263
|
+
})
|
|
4264
|
+
: create(McpToolResultContentItemSchema, {
|
|
4265
|
+
content: {
|
|
4266
|
+
case: "image",
|
|
4267
|
+
value: create(McpImageContentSchema, {
|
|
4268
|
+
data: Uint8Array.from(Buffer.from(item.data, "base64")),
|
|
4269
|
+
mimeType: item.mimeType,
|
|
4270
|
+
}),
|
|
4271
|
+
},
|
|
4272
|
+
}),
|
|
4273
|
+
),
|
|
4274
|
+
}),
|
|
4275
|
+
},
|
|
4276
|
+
});
|
|
4277
|
+
}
|
|
4278
|
+
|
|
4279
|
+
function createCursorToolCallStep(toolCall: ToolCall, result: ToolResultMessage | undefined) {
|
|
4280
|
+
const mcpCall = create(McpToolCallSchema, {
|
|
4281
|
+
args: create(McpArgsSchema, {
|
|
4282
|
+
name: toolCall.name,
|
|
4283
|
+
args: encodeCursorMcpArguments(toolCall),
|
|
4284
|
+
toolCallId: toolCall.id,
|
|
4285
|
+
providerIdentifier: "pi-agent",
|
|
4286
|
+
toolName: toolCall.name,
|
|
4287
|
+
}),
|
|
4288
|
+
...(result ? { result: createCursorMcpResult(result) } : {}),
|
|
4289
|
+
});
|
|
4290
|
+
return create(ConversationStepSchema, {
|
|
4291
|
+
message: {
|
|
4292
|
+
case: "toolCall",
|
|
4293
|
+
value: create(ToolCallSchema, {
|
|
4294
|
+
tool: { case: "mcpToolCall", value: mcpCall },
|
|
4295
|
+
toolCallId: toolCall.id,
|
|
4296
|
+
}),
|
|
4297
|
+
},
|
|
4298
|
+
});
|
|
4299
|
+
}
|
|
4300
|
+
|
|
4142
4301
|
/**
|
|
4143
4302
|
* Convert context.messages to Cursor's ConversationTurnStructure blob IDs.
|
|
4144
4303
|
* Groups messages into turns: each turn is a user message followed by the assistant's response.
|
|
@@ -4151,28 +4310,32 @@ function buildConversationTurns(
|
|
|
4151
4310
|
messages: Message[],
|
|
4152
4311
|
blobStore: Map<string, Uint8Array>,
|
|
4153
4312
|
activeUserMessageIndex = findLastUserMessageIndex(messages),
|
|
4313
|
+
targetModelId?: string,
|
|
4154
4314
|
): Uint8Array[] {
|
|
4155
4315
|
const turns: Uint8Array[] = [];
|
|
4316
|
+
const historyEnd = activeUserMessageIndex >= 0 ? activeUserMessageIndex : messages.length;
|
|
4317
|
+
const toolResults = new Map<string, ToolResultMessage>();
|
|
4318
|
+
const pairedToolCallIds = new Set<string>();
|
|
4319
|
+
for (let index = 0; index < historyEnd; index++) {
|
|
4320
|
+
const message = messages[index];
|
|
4321
|
+
if (message.role === "toolResult") {
|
|
4322
|
+
toolResults.set(message.toolCallId, message);
|
|
4323
|
+
} else if (message.role === "assistant") {
|
|
4324
|
+
for (const item of message.content) {
|
|
4325
|
+
if (item.type === "toolCall") pairedToolCallIds.add(item.id);
|
|
4326
|
+
}
|
|
4327
|
+
}
|
|
4328
|
+
}
|
|
4156
4329
|
|
|
4157
|
-
// Find turn boundaries - each turn starts with a user message
|
|
4158
4330
|
let i = 0;
|
|
4159
4331
|
while (i < messages.length) {
|
|
4160
4332
|
const msg = messages[i];
|
|
4161
|
-
|
|
4162
|
-
// Skip non-user messages at the start
|
|
4163
4333
|
if (msg.role !== "user" && msg.role !== "developer") {
|
|
4164
4334
|
i++;
|
|
4165
4335
|
continue;
|
|
4166
4336
|
}
|
|
4337
|
+
if (i === activeUserMessageIndex) break;
|
|
4167
4338
|
|
|
4168
|
-
// The active user message goes in the action, not turns. A prior user
|
|
4169
|
-
// followed by assistant/tool-result messages is complete history and
|
|
4170
|
-
// must remain serialized for resume actions.
|
|
4171
|
-
if (i === activeUserMessageIndex) {
|
|
4172
|
-
break;
|
|
4173
|
-
}
|
|
4174
|
-
|
|
4175
|
-
// Create and serialize user message
|
|
4176
4339
|
const userText = extractUserMessageText(msg);
|
|
4177
4340
|
if (userText.length === 0 && !hasUserMessageImages(msg)) {
|
|
4178
4341
|
i++;
|
|
@@ -4184,29 +4347,42 @@ function buildConversationTurns(
|
|
|
4184
4347
|
userText,
|
|
4185
4348
|
deterministicUuid(`u:${turns.length}:${cursorUserContentKey(msg.content)}`),
|
|
4186
4349
|
);
|
|
4187
|
-
const
|
|
4188
|
-
const userMessageBlobId = storeCursorBlob(blobStore, userMessageBytes);
|
|
4189
|
-
|
|
4190
|
-
// Collect and serialize steps until next user message
|
|
4350
|
+
const userMessageBlobId = storeCursorBlob(blobStore, toBinary(UserMessageSchema, userMessage));
|
|
4191
4351
|
const stepBlobIds: Uint8Array[] = [];
|
|
4192
4352
|
i++;
|
|
4193
4353
|
|
|
4194
4354
|
while (i < messages.length && messages[i].role !== "user" && messages[i].role !== "developer") {
|
|
4195
4355
|
const stepMsg = messages[i];
|
|
4196
|
-
|
|
4197
4356
|
if (stepMsg.role === "assistant") {
|
|
4198
|
-
const
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4357
|
+
for (const item of stepMsg.content) {
|
|
4358
|
+
let step: ConversationStep;
|
|
4359
|
+
if (item.type === "text") {
|
|
4360
|
+
if (!item.text) continue;
|
|
4361
|
+
step = create(ConversationStepSchema, {
|
|
4362
|
+
message: {
|
|
4363
|
+
case: "assistantMessage",
|
|
4364
|
+
value: create(AssistantMessageSchema, { text: item.text }),
|
|
4365
|
+
},
|
|
4366
|
+
});
|
|
4367
|
+
} else if (item.type === "thinking") {
|
|
4368
|
+
// Same guard as root-prompt replay: only same-model Cursor K3
|
|
4369
|
+
// thinking is replayed, so foreign/hidden reasoning never leaks
|
|
4370
|
+
// into Cursor's turn history as native thinking.
|
|
4371
|
+
if (!item.thinking || !canReplayCursorThinking(stepMsg, targetModelId)) continue;
|
|
4372
|
+
step = create(ConversationStepSchema, {
|
|
4373
|
+
message: {
|
|
4374
|
+
case: "thinkingMessage",
|
|
4375
|
+
value: create(ThinkingMessageSchema, { text: item.thinking }),
|
|
4376
|
+
},
|
|
4377
|
+
});
|
|
4378
|
+
} else if (item.type === "toolCall") {
|
|
4379
|
+
step = createCursorToolCallStep(item, toolResults.get(item.id));
|
|
4380
|
+
} else {
|
|
4381
|
+
continue;
|
|
4382
|
+
}
|
|
4206
4383
|
stepBlobIds.push(storeCursorBlob(blobStore, toBinary(ConversationStepSchema, step)));
|
|
4207
4384
|
}
|
|
4208
|
-
} else if (stepMsg.role === "toolResult") {
|
|
4209
|
-
// Include tool results as assistant text for context
|
|
4385
|
+
} else if (stepMsg.role === "toolResult" && !pairedToolCallIds.has(stepMsg.toolCallId)) {
|
|
4210
4386
|
const text = toolResultToText(stepMsg);
|
|
4211
4387
|
if (text) {
|
|
4212
4388
|
const prefix = stepMsg.isError ? "[Tool Error]" : "[Tool Result]";
|
|
@@ -4219,12 +4395,9 @@ function buildConversationTurns(
|
|
|
4219
4395
|
stepBlobIds.push(storeCursorBlob(blobStore, toBinary(ConversationStepSchema, step)));
|
|
4220
4396
|
}
|
|
4221
4397
|
}
|
|
4222
|
-
|
|
4223
4398
|
i++;
|
|
4224
4399
|
}
|
|
4225
4400
|
|
|
4226
|
-
// Create the serialized turn using Structure types. The bytes fields
|
|
4227
|
-
// (user_message, steps) are blob IDs resolved through the KV store.
|
|
4228
4401
|
const agentTurn = create(AgentConversationTurnStructureSchema, {
|
|
4229
4402
|
userMessage: userMessageBlobId,
|
|
4230
4403
|
steps: stepBlobIds,
|
|
@@ -4245,18 +4418,23 @@ function buildConversationTurns(
|
|
|
4245
4418
|
export function buildCursorHistoryForTest(
|
|
4246
4419
|
messages: Message[],
|
|
4247
4420
|
activeUserMessageIndex = findLastUserMessageIndex(messages),
|
|
4421
|
+
targetModelId?: string,
|
|
4248
4422
|
): {
|
|
4249
4423
|
rootPromptMessagesJson: unknown[];
|
|
4250
4424
|
turnUserMessagesJson: JsonValue[];
|
|
4251
4425
|
turnStepMessagesJson: JsonValue[][];
|
|
4252
4426
|
} {
|
|
4253
4427
|
const blobStore = new Map<string, Uint8Array>();
|
|
4254
|
-
const rootPromptMessagesJson = buildRootPromptMessagesJson(
|
|
4255
|
-
|
|
4256
|
-
|
|
4428
|
+
const rootPromptMessagesJson = buildRootPromptMessagesJson(
|
|
4429
|
+
messages,
|
|
4430
|
+
[],
|
|
4431
|
+
blobStore,
|
|
4432
|
+
activeUserMessageIndex,
|
|
4433
|
+
targetModelId,
|
|
4434
|
+
).map(blobId => JSON.parse(new TextDecoder().decode(readCursorBlob(blobStore, blobId))));
|
|
4257
4435
|
const turnUserMessagesJson: JsonValue[] = [];
|
|
4258
4436
|
const turnStepMessagesJson: JsonValue[][] = [];
|
|
4259
|
-
for (const turnBlobId of buildConversationTurns(messages, blobStore, activeUserMessageIndex)) {
|
|
4437
|
+
for (const turnBlobId of buildConversationTurns(messages, blobStore, activeUserMessageIndex, targetModelId)) {
|
|
4260
4438
|
const turn = fromBinary(ConversationTurnStructureSchema, readCursorBlob(blobStore, turnBlobId));
|
|
4261
4439
|
if (turn.turn.case !== "agentConversationTurn") {
|
|
4262
4440
|
continue;
|
|
@@ -4360,7 +4538,12 @@ function buildGrpcRequest(
|
|
|
4360
4538
|
|
|
4361
4539
|
// Build conversation turns from prior messages, excluding only the active user message
|
|
4362
4540
|
// when the request is sending one. Resume actions must preserve trailing tool results.
|
|
4363
|
-
const turns = buildConversationTurns(
|
|
4541
|
+
const turns = buildConversationTurns(
|
|
4542
|
+
context.messages,
|
|
4543
|
+
blobStore,
|
|
4544
|
+
activeUserMessage ? activeUserMessageIndex : -1,
|
|
4545
|
+
model.id,
|
|
4546
|
+
);
|
|
4364
4547
|
|
|
4365
4548
|
// Build `rootPromptMessagesJson` from prior messages. Cursor's server uses this
|
|
4366
4549
|
// field (not `turns[]`) to construct the actual model prompt; if we only send the
|
|
@@ -4371,6 +4554,7 @@ function buildGrpcRequest(
|
|
|
4371
4554
|
systemPromptIds,
|
|
4372
4555
|
blobStore,
|
|
4373
4556
|
activeUserMessage ? activeUserMessageIndex : -1,
|
|
4557
|
+
model.id,
|
|
4374
4558
|
);
|
|
4375
4559
|
|
|
4376
4560
|
// Preserve cached non-history state fields (todos, file states, summaries, etc.)
|
|
@@ -295,7 +295,15 @@ function normalizeAnthropicTargetToolCallId<TApi extends Api>(
|
|
|
295
295
|
* - Preserves tool call structure (unlike converting to text summaries)
|
|
296
296
|
* - Injects synthetic "aborted" tool results
|
|
297
297
|
*/
|
|
298
|
-
|
|
298
|
+
/**
|
|
299
|
+
* Credential-shaped token patterns scrubbed from outbound provider traffic when
|
|
300
|
+
* credential redaction is enabled. Exported so hosts can route the same shapes
|
|
301
|
+
* through reversible obfuscation (keyed placeholders restored before local tool
|
|
302
|
+
* execution) instead of the irreversible `[*_token_redacted]` rewrite below —
|
|
303
|
+
* an irreversible placeholder echoed back in edit-tool `old_text` can never
|
|
304
|
+
* match the real bytes on disk.
|
|
305
|
+
*/
|
|
306
|
+
export const SENSITIVE_TOKEN_RE =
|
|
299
307
|
/(?<![a-zA-Z0-9_*-])(gh[opusr]_[a-zA-Z0-9_*]{36,}|github_pat_[a-zA-Z0-9_*]{36,}|glpat-[a-zA-Z0-9_*-]{20,}|sk-proj-[a-zA-Z0-9_*-]{36,}|sk-ant-[a-zA-Z0-9_*-]{36,}|sk-[a-zA-Z0-9_*-]{48,})(?![a-zA-Z0-9_*-])/gi;
|
|
300
308
|
|
|
301
309
|
function hasPlausibleCredentialEntropy(token: string): boolean {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createApiKeyLogin } from "./api-key-login";
|
|
2
|
+
import type { OAuthLoginCallbacks } from "./oauth/types";
|
|
3
|
+
import type { ProviderDefinition } from "./types";
|
|
4
|
+
|
|
5
|
+
export const loginGmiCloud = createApiKeyLogin({
|
|
6
|
+
providerLabel: "GMI Cloud",
|
|
7
|
+
authUrl: "https://console.gmicloud.ai",
|
|
8
|
+
instructions: "Create or copy your GMI Cloud API key",
|
|
9
|
+
promptMessage: "Paste your GMI Cloud API key",
|
|
10
|
+
placeholder: "eyJ...",
|
|
11
|
+
validation: {
|
|
12
|
+
kind: "models-endpoint",
|
|
13
|
+
provider: "GMI Cloud",
|
|
14
|
+
modelsUrl: "https://api.gmi-serving.com/v1/models",
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const gmiCloudProvider = {
|
|
19
|
+
id: "gmi-cloud",
|
|
20
|
+
name: "GMI Cloud",
|
|
21
|
+
login: (cb: OAuthLoginCallbacks) => loginGmiCloud(cb),
|
|
22
|
+
} as const satisfies ProviderDefinition;
|
package/src/registry/registry.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { fireworksProvider } from "./fireworks";
|
|
|
18
18
|
import { githubCopilotProvider } from "./github-copilot";
|
|
19
19
|
import { gitlabDuoProvider } from "./gitlab-duo";
|
|
20
20
|
import { gitLabDuoWorkflowProvider } from "./gitlab-duo-workflow";
|
|
21
|
+
import { gmiCloudProvider } from "./gmi-cloud";
|
|
21
22
|
import { googleProvider } from "./google";
|
|
22
23
|
import { googleAntigravityProvider } from "./google-antigravity";
|
|
23
24
|
import { googleGeminiCliProvider } from "./google-gemini-cli";
|
|
@@ -154,6 +155,7 @@ const ALL = [
|
|
|
154
155
|
mistralProvider,
|
|
155
156
|
minimaxProvider,
|
|
156
157
|
amazonBedrockProvider,
|
|
158
|
+
gmiCloudProvider,
|
|
157
159
|
];
|
|
158
160
|
|
|
159
161
|
export type RegistryDef = (typeof ALL)[number];
|