@oh-my-pi/pi-ai 16.2.0 → 16.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-gateway/server.d.ts +0 -19
- package/dist/types/auth-retry.d.ts +2 -10
- package/dist/types/auth-storage.d.ts +1 -2
- package/dist/types/dialect/demotion.d.ts +22 -0
- package/dist/types/dialect/index.d.ts +2 -0
- package/dist/types/error/abort.d.ts +14 -0
- package/dist/types/error/auth-classify.d.ts +16 -0
- package/dist/types/error/auth.d.ts +27 -0
- package/dist/types/error/aws.d.ts +23 -0
- package/dist/types/error/classes.d.ts +102 -0
- package/dist/types/error/finalize.d.ts +39 -0
- package/dist/types/error/flags.d.ts +70 -0
- package/dist/types/error/format.d.ts +20 -0
- package/dist/types/error/gateway.d.ts +20 -0
- package/dist/types/error/index.d.ts +13 -0
- package/dist/types/error/oauth.d.ts +43 -0
- package/dist/types/error/provider.d.ts +42 -0
- package/dist/types/{rate-limit-utils.d.ts → error/rate-limit.d.ts} +14 -1
- package/dist/types/error/retryable.d.ts +27 -0
- package/dist/types/error/validation.d.ts +32 -0
- package/dist/types/index.d.ts +1 -3
- package/dist/types/providers/amazon-bedrock.d.ts +0 -5
- package/dist/types/providers/anthropic-client.d.ts +2 -16
- package/dist/types/providers/anthropic.d.ts +6 -1
- package/dist/types/providers/aws-eventstream.d.ts +2 -1
- package/dist/types/providers/cursor.d.ts +8 -7
- package/dist/types/providers/google-gemini-cli.d.ts +0 -5
- package/dist/types/providers/google-shared.d.ts +0 -5
- package/dist/types/providers/ollama.d.ts +0 -5
- package/dist/types/providers/openai-codex/request-transformer.d.ts +2 -2
- package/dist/types/providers/openai-codex/response-handler.d.ts +1 -1
- package/dist/types/providers/openai-codex-responses.d.ts +2 -2
- package/dist/types/providers/openai-responses-server-schema.d.ts +6 -0
- package/dist/types/providers/openai-responses-wire.d.ts +1 -1
- package/dist/types/providers/openai-responses.d.ts +1 -0
- package/dist/types/providers/openai-shared.d.ts +8 -6
- package/dist/types/providers/pi-native-client.d.ts +0 -9
- package/dist/types/registry/oauth/xai-oauth.d.ts +0 -9
- package/dist/types/types.d.ts +6 -0
- package/dist/types/utils/block-symbols.d.ts +20 -0
- package/dist/types/utils/openai-http.d.ts +4 -8
- package/dist/types/utils/retry.d.ts +2 -14
- package/dist/types/utils/thinking-loop.d.ts +3 -1
- package/package.json +8 -4
- package/src/api-registry.ts +3 -1
- package/src/auth-broker/discover.ts +7 -2
- package/src/auth-broker/remote-store.ts +8 -7
- package/src/auth-gateway/server.ts +27 -115
- package/src/auth-retry.ts +9 -21
- package/src/auth-storage.ts +34 -49
- package/src/dialect/demotion.ts +31 -0
- package/src/dialect/factory.ts +0 -2
- package/src/dialect/index.ts +2 -0
- package/src/dialect/owned-stream.ts +0 -1
- package/src/dialect/thinking.ts +22 -10
- package/src/error/abort.ts +18 -0
- package/src/error/auth-classify.ts +30 -0
- package/src/error/auth.ts +48 -0
- package/src/error/aws.ts +31 -0
- package/src/error/classes.ts +186 -0
- package/src/error/finalize.ts +69 -0
- package/src/error/flags.ts +486 -0
- package/src/error/format.ts +36 -0
- package/src/error/gateway.ts +96 -0
- package/src/error/index.ts +13 -0
- package/src/error/oauth.ts +58 -0
- package/src/error/provider.ts +56 -0
- package/src/{rate-limit-utils.ts → error/rate-limit.ts} +9 -3
- package/src/error/retryable.ts +70 -0
- package/src/error/validation.ts +44 -0
- package/src/index.ts +1 -3
- package/src/providers/amazon-bedrock.ts +61 -57
- package/src/providers/anthropic-client.ts +13 -41
- package/src/providers/anthropic-messages-server.ts +9 -2
- package/src/providers/anthropic.ts +84 -147
- package/src/providers/aws-credentials.ts +41 -11
- package/src/providers/aws-eventstream.ts +12 -21
- package/src/providers/azure-openai-responses.ts +27 -17
- package/src/providers/cursor.ts +59 -53
- package/src/providers/devin.ts +28 -20
- package/src/providers/gitlab-duo-workflow.ts +30 -10
- package/src/providers/gitlab-duo.ts +22 -6
- package/src/providers/google-auth.ts +15 -5
- package/src/providers/google-gemini-cli.ts +46 -60
- package/src/providers/google-shared.ts +40 -38
- package/src/providers/google-vertex.ts +3 -2
- package/src/providers/google.ts +5 -1
- package/src/providers/mock.ts +10 -7
- package/src/providers/ollama.ts +34 -29
- package/src/providers/openai-chat-server.ts +2 -1
- package/src/providers/openai-codex/request-transformer.ts +13 -12
- package/src/providers/openai-codex/response-handler.ts +1 -1
- package/src/providers/openai-codex-responses.ts +931 -1012
- package/src/providers/openai-completions.ts +46 -36
- package/src/providers/openai-responses-server-schema.ts +3 -0
- package/src/providers/openai-responses-server.ts +82 -30
- package/src/providers/openai-responses-wire.ts +1 -1
- package/src/providers/openai-responses.ts +46 -22
- package/src/providers/openai-shared.ts +118 -51
- package/src/providers/pi-native-client.ts +12 -18
- package/src/providers/pi-native-server.ts +9 -6
- package/src/providers/register-builtins.ts +5 -2
- package/src/providers/transform-messages.ts +31 -63
- package/src/registry/alibaba-coding-plan.ts +6 -5
- package/src/registry/api-key-login.ts +4 -3
- package/src/registry/api-key-validation.ts +4 -3
- package/src/registry/cloudflare-ai-gateway.ts +4 -3
- package/src/registry/coreweave.ts +2 -1
- package/src/registry/deepseek.ts +2 -1
- package/src/registry/google-antigravity.ts +2 -1
- package/src/registry/google-gemini-cli.ts +2 -1
- package/src/registry/kagi.ts +4 -3
- package/src/registry/kilo.ts +32 -10
- package/src/registry/litellm.ts +4 -3
- package/src/registry/llama-cpp.ts +3 -2
- package/src/registry/lm-studio.ts +3 -2
- package/src/registry/nvidia.ts +7 -7
- package/src/registry/oauth/anthropic.ts +23 -6
- package/src/registry/oauth/callback-server.ts +5 -3
- package/src/registry/oauth/cursor.ts +18 -4
- package/src/registry/oauth/devin.ts +14 -3
- package/src/registry/oauth/github-copilot.ts +21 -10
- package/src/registry/oauth/gitlab-duo-workflow.ts +15 -3
- package/src/registry/oauth/gitlab-duo.ts +30 -6
- package/src/registry/oauth/google-antigravity.ts +14 -5
- package/src/registry/oauth/google-gemini-cli.ts +24 -7
- package/src/registry/oauth/google-oauth-shared.ts +6 -3
- package/src/registry/oauth/index.ts +19 -8
- package/src/registry/oauth/kimi.ts +40 -10
- package/src/registry/oauth/openai-codex.ts +26 -11
- package/src/registry/oauth/opencode.ts +4 -3
- package/src/registry/oauth/perplexity.ts +33 -11
- package/src/registry/oauth/xai-oauth.ts +62 -18
- package/src/registry/oauth/xiaomi.ts +21 -9
- package/src/registry/ollama-cloud.ts +5 -4
- package/src/registry/ollama.ts +3 -2
- package/src/registry/parallel.ts +4 -3
- package/src/registry/qwen-portal.ts +4 -3
- package/src/registry/tavily.ts +4 -3
- package/src/registry/vercel-ai-gateway.ts +4 -3
- package/src/registry/vllm.ts +3 -2
- package/src/stream.ts +79 -24
- package/src/types.ts +6 -0
- package/src/usage/claude.ts +2 -1
- package/src/usage/github-copilot.ts +4 -3
- package/src/usage/google-antigravity.ts +2 -1
- package/src/utils/abort.ts +4 -2
- package/src/utils/block-symbols.ts +32 -0
- package/src/utils/event-stream.ts +15 -3
- package/src/utils/http-inspector.ts +4 -4
- package/src/utils/idle-iterator.ts +6 -5
- package/src/utils/openai-http.ts +11 -46
- package/src/utils/parse-bind.ts +7 -5
- package/src/utils/proxy.ts +2 -1
- package/src/utils/retry.ts +7 -23
- package/src/utils/schema/normalize.ts +3 -2
- package/src/utils/thinking-loop.ts +15 -11
- package/src/utils/validation.ts +4 -3
- package/dist/types/dialect/pi.d.ts +0 -9
- package/dist/types/errors.d.ts +0 -24
- package/dist/types/utils/overflow.d.ts +0 -55
- package/src/dialect/pi.md +0 -55
- package/src/dialect/pi.ts +0 -600
- package/src/errors.ts +0 -32
- package/src/utils/overflow.ts +0 -140
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
* practice (`:event-type`, `:message-type`, `:content-type`, `:exception-type`).
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
import * as AIError from "../error";
|
|
21
|
+
|
|
20
22
|
const PRELUDE_LEN = 8;
|
|
21
23
|
const PRELUDE_CRC_LEN = 4;
|
|
22
24
|
const MESSAGE_CRC_LEN = 4;
|
|
@@ -30,20 +32,8 @@ export interface EventStreamMessage {
|
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
/** CRC32 (IEEE / zlib polynomial 0xEDB88320), matches `@aws-crypto/crc32`. */
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
for (let i = 0; i < 256; i++) {
|
|
36
|
-
let c = i;
|
|
37
|
-
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
38
|
-
t[i] = c >>> 0;
|
|
39
|
-
}
|
|
40
|
-
return t;
|
|
41
|
-
})();
|
|
42
|
-
|
|
43
|
-
export function crc32(bytes: Uint8Array, seed = 0): number {
|
|
44
|
-
let c = (seed ^ 0xffffffff) >>> 0;
|
|
45
|
-
for (let i = 0; i < bytes.length; i++) c = (CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8)) >>> 0;
|
|
46
|
-
return (c ^ 0xffffffff) >>> 0;
|
|
35
|
+
export function crc32(bytes: Uint8Array): number {
|
|
36
|
+
return Bun.hash.crc32(bytes) >>> 0;
|
|
47
37
|
}
|
|
48
38
|
|
|
49
39
|
/**
|
|
@@ -53,17 +43,18 @@ export function crc32(bytes: Uint8Array, seed = 0): number {
|
|
|
53
43
|
* frames.
|
|
54
44
|
*/
|
|
55
45
|
export function decodeMessage(frame: Uint8Array): EventStreamMessage {
|
|
56
|
-
if (frame.length < MIN_MESSAGE_LEN) throw new
|
|
46
|
+
if (frame.length < MIN_MESSAGE_LEN) throw new AIError.EventStreamFrameError("frame too short");
|
|
57
47
|
const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
|
|
58
48
|
const total = view.getUint32(0, false);
|
|
59
|
-
if (total !== frame.length)
|
|
49
|
+
if (total !== frame.length)
|
|
50
|
+
throw new AIError.EventStreamFrameError(`framed length ${total} != buffer ${frame.length}`);
|
|
60
51
|
const headersLen = view.getUint32(4, false);
|
|
61
52
|
const preludeCrc = view.getUint32(8, false);
|
|
62
53
|
const computedPreludeCrc = crc32(frame.subarray(0, PRELUDE_LEN));
|
|
63
|
-
if (computedPreludeCrc !== preludeCrc) throw new
|
|
54
|
+
if (computedPreludeCrc !== preludeCrc) throw new AIError.EventStreamFrameError("prelude CRC mismatch");
|
|
64
55
|
const msgCrc = view.getUint32(total - MESSAGE_CRC_LEN, false);
|
|
65
56
|
const computedMsgCrc = crc32(frame.subarray(0, total - MESSAGE_CRC_LEN));
|
|
66
|
-
if (computedMsgCrc !== msgCrc) throw new
|
|
57
|
+
if (computedMsgCrc !== msgCrc) throw new AIError.EventStreamFrameError("message CRC mismatch");
|
|
67
58
|
|
|
68
59
|
const headersBytes = frame.subarray(HEADER_BLOCK_OFFSET, HEADER_BLOCK_OFFSET + headersLen);
|
|
69
60
|
const payload = frame.subarray(HEADER_BLOCK_OFFSET + headersLen, total - MESSAGE_CRC_LEN);
|
|
@@ -136,7 +127,7 @@ function parseHeaders(buf: Uint8Array): Record<string, string> {
|
|
|
136
127
|
break;
|
|
137
128
|
}
|
|
138
129
|
default:
|
|
139
|
-
throw new
|
|
130
|
+
throw new AIError.EventStreamFrameError(`unknown header value type ${type}`);
|
|
140
131
|
}
|
|
141
132
|
}
|
|
142
133
|
return out;
|
|
@@ -170,7 +161,7 @@ export async function* decodeEventStream(source: ReadableStream<Uint8Array>): As
|
|
|
170
161
|
while (buf.length - offset >= 4) {
|
|
171
162
|
const dv = new DataView(buf.buffer, buf.byteOffset + offset, buf.length - offset);
|
|
172
163
|
const total = dv.getUint32(0, false);
|
|
173
|
-
if (total < MIN_MESSAGE_LEN) throw new
|
|
164
|
+
if (total < MIN_MESSAGE_LEN) throw new AIError.EventStreamFrameError(`total length ${total} below minimum`);
|
|
174
165
|
if (buf.length - offset < total) break;
|
|
175
166
|
const frame = buf.subarray(offset, offset + total);
|
|
176
167
|
yield decodeMessage(frame);
|
|
@@ -179,7 +170,7 @@ export async function* decodeEventStream(source: ReadableStream<Uint8Array>): As
|
|
|
179
170
|
if (offset > 0) buf = buf.slice(offset);
|
|
180
171
|
if (done) break;
|
|
181
172
|
}
|
|
182
|
-
if (buf.length > 0) throw new
|
|
173
|
+
if (buf.length > 0) throw new AIError.EventStreamFrameError("truncated message at end of stream");
|
|
183
174
|
completed = true;
|
|
184
175
|
} finally {
|
|
185
176
|
// On abnormal exit (consumer threw/broke, decode error) cancel the body so the
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { $env
|
|
1
|
+
import { $env } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import * as AIError from "../error";
|
|
2
3
|
import { getEnvApiKey } from "../stream";
|
|
3
4
|
import type {
|
|
4
5
|
AssistantMessage,
|
|
@@ -12,7 +13,7 @@ import type {
|
|
|
12
13
|
} from "../types";
|
|
13
14
|
import { createAbortSourceTracker } from "../utils/abort";
|
|
14
15
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
15
|
-
import {
|
|
16
|
+
import type { RawHttpRequestDump } from "../utils/http-inspector";
|
|
16
17
|
import {
|
|
17
18
|
getOpenAIStreamFirstEventTimeoutMs,
|
|
18
19
|
getOpenAIStreamIdleTimeoutMs,
|
|
@@ -85,7 +86,7 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses"
|
|
|
85
86
|
|
|
86
87
|
// Start async processing
|
|
87
88
|
(async () => {
|
|
88
|
-
const startTime =
|
|
89
|
+
const startTime = performance.now();
|
|
89
90
|
let firstTokenTime: number | undefined;
|
|
90
91
|
const deploymentName = resolveDeploymentName(model, options);
|
|
91
92
|
|
|
@@ -96,7 +97,9 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses"
|
|
|
96
97
|
);
|
|
97
98
|
let rawRequestDump: RawHttpRequestDump | undefined;
|
|
98
99
|
const abortTracker = createAbortSourceTracker(options?.signal);
|
|
99
|
-
const firstEventTimeoutAbortError = new
|
|
100
|
+
const firstEventTimeoutAbortError = new AIError.StreamTimeoutError(
|
|
101
|
+
AZURE_OPENAI_RESPONSES_FIRST_EVENT_TIMEOUT_MESSAGE,
|
|
102
|
+
);
|
|
100
103
|
const { requestAbortController, requestSignal } = abortTracker;
|
|
101
104
|
const onSseEvent = options?.onSseEvent;
|
|
102
105
|
const rawSseObserver = onSseEvent
|
|
@@ -203,7 +206,7 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses"
|
|
|
203
206
|
let sawTerminalResponseEvent = false;
|
|
204
207
|
await processResponsesStream(timedOpenaiStream, output, stream, model, {
|
|
205
208
|
onFirstToken: () => {
|
|
206
|
-
if (!firstTokenTime) firstTokenTime =
|
|
209
|
+
if (!firstTokenTime) firstTokenTime = performance.now();
|
|
207
210
|
},
|
|
208
211
|
onCompleted: () => {
|
|
209
212
|
sawTerminalResponseEvent = true;
|
|
@@ -216,28 +219,34 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses"
|
|
|
216
219
|
}
|
|
217
220
|
|
|
218
221
|
if (abortTracker.wasCallerAbort()) {
|
|
219
|
-
throw new
|
|
222
|
+
throw new AIError.AbortError();
|
|
220
223
|
}
|
|
221
224
|
|
|
222
225
|
if (!sawTerminalResponseEvent) {
|
|
223
|
-
throw new
|
|
226
|
+
throw new AIError.ProviderResponseError(
|
|
227
|
+
"Azure OpenAI responses stream closed before a terminal response event was received",
|
|
228
|
+
{ provider: model.provider, kind: "incomplete-stream" },
|
|
229
|
+
);
|
|
224
230
|
}
|
|
225
231
|
|
|
226
232
|
if (output.stopReason === "aborted" || output.stopReason === "error") {
|
|
227
|
-
throw new
|
|
233
|
+
throw new AIError.ProviderResponseError(output.errorMessage ?? "An unknown error occurred", {
|
|
234
|
+
provider: model.provider,
|
|
235
|
+
kind: "output",
|
|
236
|
+
});
|
|
228
237
|
}
|
|
229
238
|
|
|
230
|
-
output.duration =
|
|
239
|
+
output.duration = performance.now() - startTime;
|
|
231
240
|
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
232
241
|
stream.push({ type: "done", reason: output.stopReason, message: output });
|
|
233
242
|
stream.end();
|
|
234
243
|
} catch (error) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
output.
|
|
238
|
-
output.
|
|
239
|
-
output.errorMessage =
|
|
240
|
-
output.duration =
|
|
244
|
+
const result = await AIError.finalize(error, { api: model.api, abortTracker, rawRequestDump });
|
|
245
|
+
output.stopReason = result.stopReason;
|
|
246
|
+
output.errorStatus = result.status;
|
|
247
|
+
output.errorId = result.id;
|
|
248
|
+
output.errorMessage = result.message;
|
|
249
|
+
output.duration = performance.now() - startTime;
|
|
241
250
|
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
242
251
|
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
243
252
|
stream.end();
|
|
@@ -267,7 +276,7 @@ function resolveAzureConfig(
|
|
|
267
276
|
}
|
|
268
277
|
|
|
269
278
|
if (!resolvedBaseUrl) {
|
|
270
|
-
throw new
|
|
279
|
+
throw new AIError.ConfigurationError(
|
|
271
280
|
"Azure OpenAI base URL is required. Set AZURE_OPENAI_BASE_URL or AZURE_OPENAI_RESOURCE_NAME, or pass azureBaseUrl, azureResourceName, or model.baseUrl.",
|
|
272
281
|
);
|
|
273
282
|
}
|
|
@@ -295,7 +304,8 @@ function buildAzureResponsesRequest(
|
|
|
295
304
|
if (!apiKey) {
|
|
296
305
|
const envKey = $env.AZURE_OPENAI_API_KEY;
|
|
297
306
|
if (!envKey) {
|
|
298
|
-
throw new
|
|
307
|
+
throw new AIError.MissingApiKeyError(
|
|
308
|
+
undefined,
|
|
299
309
|
"Azure OpenAI API key is required. Set AZURE_OPENAI_API_KEY environment variable or pass it as an argument.",
|
|
300
310
|
);
|
|
301
311
|
}
|
package/src/providers/cursor.ts
CHANGED
|
@@ -102,13 +102,8 @@ import {
|
|
|
102
102
|
WriteSuccessSchema,
|
|
103
103
|
} from "@oh-my-pi/pi-catalog/discovery/cursor-gen/agent_pb";
|
|
104
104
|
import { calculateCost } from "@oh-my-pi/pi-catalog/models";
|
|
105
|
-
import {
|
|
106
|
-
|
|
107
|
-
extractHttpStatusFromError,
|
|
108
|
-
parseJsonWithRepair,
|
|
109
|
-
parseStreamingJson,
|
|
110
|
-
sanitizeText,
|
|
111
|
-
} from "@oh-my-pi/pi-utils";
|
|
105
|
+
import { $env, parseJsonWithRepair, parseStreamingJson, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
106
|
+
import * as AIError from "../error";
|
|
112
107
|
import type {
|
|
113
108
|
Api,
|
|
114
109
|
AssistantMessage,
|
|
@@ -130,11 +125,16 @@ import type {
|
|
|
130
125
|
ToolResultMessage,
|
|
131
126
|
} from "../types";
|
|
132
127
|
import { normalizeSystemPrompts } from "../utils";
|
|
128
|
+
import {
|
|
129
|
+
clearStreamingPartialJson,
|
|
130
|
+
kStreamingBlockIndex,
|
|
131
|
+
kStreamingBlockKind,
|
|
132
|
+
kStreamingPartialJson,
|
|
133
|
+
} from "../utils/block-symbols";
|
|
133
134
|
import { deterministicUuid } from "../utils/deterministic-id";
|
|
134
135
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
135
136
|
import { connectProxiedSocket, getProxyForProvider, shouldBypassProxy } from "../utils/proxy";
|
|
136
137
|
import { createRequestDebugSession, isRequestDebugEnabled, type RequestDebugResponseLog } from "../utils/request-debug";
|
|
137
|
-
import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
|
|
138
138
|
import { toolWireSchema } from "../utils/schema/wire";
|
|
139
139
|
|
|
140
140
|
export const CURSOR_API_URL = "https://api2.cursor.sh";
|
|
@@ -194,11 +194,11 @@ function parseConnectEndStream(data: Uint8Array): Error | null {
|
|
|
194
194
|
if (error) {
|
|
195
195
|
const code = typeof error.code === "string" ? error.code : "unknown";
|
|
196
196
|
const message = typeof error.message === "string" ? error.message : "Unknown error";
|
|
197
|
-
return new
|
|
197
|
+
return new AIError.ProviderResponseError(`Connect error ${code}: ${message}`, { kind: "envelope" });
|
|
198
198
|
}
|
|
199
199
|
return null;
|
|
200
200
|
} catch {
|
|
201
|
-
return new
|
|
201
|
+
return new AIError.ProviderResponseError("Failed to parse Connect end stream", { kind: "envelope" });
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
|
|
@@ -315,7 +315,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
315
315
|
const stream = new AssistantMessageEventStream();
|
|
316
316
|
|
|
317
317
|
(async () => {
|
|
318
|
-
const startTime =
|
|
318
|
+
const startTime = performance.now();
|
|
319
319
|
let firstTokenTime: number | undefined;
|
|
320
320
|
|
|
321
321
|
const output: AssistantMessage = {
|
|
@@ -344,7 +344,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
344
344
|
try {
|
|
345
345
|
const apiKey = options?.apiKey;
|
|
346
346
|
if (!apiKey) {
|
|
347
|
-
throw new
|
|
347
|
+
throw new AIError.MissingApiKeyError(undefined, "Cursor API key (access token) is required");
|
|
348
348
|
}
|
|
349
349
|
|
|
350
350
|
const conversationId = options?.conversationId ?? options?.sessionId ?? crypto.randomUUID();
|
|
@@ -399,8 +399,8 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
399
399
|
|
|
400
400
|
let pendingBuffer = Buffer.alloc(0);
|
|
401
401
|
let endStreamError: Error | null = null;
|
|
402
|
-
let currentTextBlock: (TextContent & {
|
|
403
|
-
let currentThinkingBlock: (ThinkingContent & {
|
|
402
|
+
let currentTextBlock: (TextContent & { [kStreamingBlockIndex]: number }) | null = null;
|
|
403
|
+
let currentThinkingBlock: (ThinkingContent & { [kStreamingBlockIndex]: number }) | null = null;
|
|
404
404
|
let currentToolCall: ToolCallState | null = null;
|
|
405
405
|
const usageState: UsageState = { sawTokenDelta: false };
|
|
406
406
|
|
|
@@ -427,7 +427,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
427
427
|
currentToolCall = t;
|
|
428
428
|
},
|
|
429
429
|
setFirstTokenTime: () => {
|
|
430
|
-
if (!firstTokenTime) firstTokenTime =
|
|
430
|
+
if (!firstTokenTime) firstTokenTime = performance.now();
|
|
431
431
|
},
|
|
432
432
|
};
|
|
433
433
|
|
|
@@ -531,7 +531,12 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
531
531
|
const msg = trailers["grpc-message"];
|
|
532
532
|
if (status && status !== "0") {
|
|
533
533
|
void closeDebugLog().finally(() => {
|
|
534
|
-
reject(
|
|
534
|
+
reject(
|
|
535
|
+
new AIError.ProviderResponseError(
|
|
536
|
+
`gRPC error ${status}: ${decodeURIComponent(String(msg || ""))}`,
|
|
537
|
+
{ kind: "envelope" },
|
|
538
|
+
),
|
|
539
|
+
);
|
|
535
540
|
});
|
|
536
541
|
}
|
|
537
542
|
});
|
|
@@ -557,7 +562,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
557
562
|
options.signal.addEventListener("abort", () => {
|
|
558
563
|
h2Request?.close();
|
|
559
564
|
void closeDebugLog().finally(() => {
|
|
560
|
-
reject(new
|
|
565
|
+
reject(new AIError.AbortError());
|
|
561
566
|
});
|
|
562
567
|
});
|
|
563
568
|
}
|
|
@@ -567,9 +572,8 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
567
572
|
endCurrentThinkingBlock(output, stream, state);
|
|
568
573
|
if (state.currentToolCall) {
|
|
569
574
|
const idx = output.content.indexOf(state.currentToolCall);
|
|
570
|
-
state.currentToolCall.arguments = parseStreamingJson(state.currentToolCall
|
|
571
|
-
|
|
572
|
-
delete (state.currentToolCall as any).index;
|
|
575
|
+
state.currentToolCall.arguments = parseStreamingJson(state.currentToolCall[kStreamingPartialJson]);
|
|
576
|
+
clearStreamingPartialJson(state.currentToolCall);
|
|
573
577
|
stream.push({
|
|
574
578
|
type: "toolcall_end",
|
|
575
579
|
contentIndex: idx,
|
|
@@ -580,7 +584,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
580
584
|
|
|
581
585
|
calculateCost(model, output.usage);
|
|
582
586
|
|
|
583
|
-
output.duration =
|
|
587
|
+
output.duration = performance.now() - startTime;
|
|
584
588
|
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
585
589
|
stream.push({
|
|
586
590
|
type: "done",
|
|
@@ -589,10 +593,12 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
589
593
|
});
|
|
590
594
|
stream.end();
|
|
591
595
|
} catch (error) {
|
|
592
|
-
|
|
593
|
-
output.
|
|
594
|
-
output.
|
|
595
|
-
output.
|
|
596
|
+
const result = await AIError.finalize(error, { api: model.api, signal: options?.signal });
|
|
597
|
+
output.stopReason = result.stopReason;
|
|
598
|
+
output.errorStatus = result.status;
|
|
599
|
+
output.errorId = result.id;
|
|
600
|
+
output.errorMessage = result.message;
|
|
601
|
+
output.duration = performance.now() - startTime;
|
|
596
602
|
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
597
603
|
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
598
604
|
stream.end();
|
|
@@ -611,15 +617,19 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
611
617
|
return stream;
|
|
612
618
|
};
|
|
613
619
|
|
|
614
|
-
export type ToolCallState = ToolCall & {
|
|
620
|
+
export type ToolCallState = ToolCall & {
|
|
621
|
+
[kStreamingBlockIndex]: number;
|
|
622
|
+
[kStreamingPartialJson]?: string;
|
|
623
|
+
[kStreamingBlockKind]: "mcp" | "todo";
|
|
624
|
+
};
|
|
615
625
|
|
|
616
626
|
export interface BlockState {
|
|
617
|
-
currentTextBlock: (TextContent & {
|
|
618
|
-
currentThinkingBlock: (ThinkingContent & {
|
|
627
|
+
currentTextBlock: (TextContent & { [kStreamingBlockIndex]: number }) | null;
|
|
628
|
+
currentThinkingBlock: (ThinkingContent & { [kStreamingBlockIndex]: number }) | null;
|
|
619
629
|
currentToolCall: ToolCallState | null;
|
|
620
630
|
firstTokenTime: number | undefined;
|
|
621
|
-
setTextBlock: (b: (TextContent & {
|
|
622
|
-
setThinkingBlock: (b: (ThinkingContent & {
|
|
631
|
+
setTextBlock: (b: (TextContent & { [kStreamingBlockIndex]: number }) | null) => void;
|
|
632
|
+
setThinkingBlock: (b: (ThinkingContent & { [kStreamingBlockIndex]: number }) | null) => void;
|
|
623
633
|
setToolCall: (t: ToolCallState | null) => void;
|
|
624
634
|
setFirstTokenTime: () => void;
|
|
625
635
|
}
|
|
@@ -757,7 +767,7 @@ async function handleShellStreamArgs(
|
|
|
757
767
|
): Promise<void> {
|
|
758
768
|
const normalizedWorkingDirectory = args.workingDirectory || process.cwd();
|
|
759
769
|
const normalizedArgs: ShellArgs = { ...args, workingDirectory: normalizedWorkingDirectory };
|
|
760
|
-
const startTs =
|
|
770
|
+
const startTs = performance.now();
|
|
761
771
|
log("shellStream", "start", {
|
|
762
772
|
command: (args as any).command,
|
|
763
773
|
workingDirectory: normalizedWorkingDirectory,
|
|
@@ -895,7 +905,7 @@ async function handleShellStreamArgs(
|
|
|
895
905
|
sendExecClientMessage(h2Request, execMsg, "shellResult", sanitizedExecResult);
|
|
896
906
|
sendExecClientStreamClose(h2Request, execMsg);
|
|
897
907
|
|
|
898
|
-
log("shellStream", "done", { elapsed:
|
|
908
|
+
log("shellStream", "done", { elapsed: performance.now() - startTs });
|
|
899
909
|
}
|
|
900
910
|
|
|
901
911
|
function sendShellStreamExitFromResult(
|
|
@@ -1972,7 +1982,6 @@ function endCurrentTextBlock(output: AssistantMessage, stream: AssistantMessageE
|
|
|
1972
1982
|
const block = state.currentTextBlock;
|
|
1973
1983
|
if (!block) return;
|
|
1974
1984
|
const idx = output.content.indexOf(block);
|
|
1975
|
-
delete (block as { index?: number }).index;
|
|
1976
1985
|
stream.push({
|
|
1977
1986
|
type: "text_end",
|
|
1978
1987
|
contentIndex: idx,
|
|
@@ -1990,7 +1999,6 @@ function endCurrentThinkingBlock(
|
|
|
1990
1999
|
const block = state.currentThinkingBlock;
|
|
1991
2000
|
if (!block) return;
|
|
1992
2001
|
const idx = output.content.indexOf(block);
|
|
1993
|
-
delete (block as { index?: number }).index;
|
|
1994
2002
|
stream.push({
|
|
1995
2003
|
type: "thinking_end",
|
|
1996
2004
|
contentIndex: idx,
|
|
@@ -2016,10 +2024,10 @@ export function processInteractionUpdate(
|
|
|
2016
2024
|
state.setFirstTokenTime();
|
|
2017
2025
|
const delta = update.message.value.text || "";
|
|
2018
2026
|
if (!state.currentTextBlock) {
|
|
2019
|
-
const block: TextContent & {
|
|
2027
|
+
const block: TextContent & { [kStreamingBlockIndex]: number } = {
|
|
2020
2028
|
type: "text",
|
|
2021
2029
|
text: "",
|
|
2022
|
-
|
|
2030
|
+
[kStreamingBlockIndex]: output.content.length,
|
|
2023
2031
|
};
|
|
2024
2032
|
output.content.push(block);
|
|
2025
2033
|
state.setTextBlock(block);
|
|
@@ -2032,10 +2040,10 @@ export function processInteractionUpdate(
|
|
|
2032
2040
|
state.setFirstTokenTime();
|
|
2033
2041
|
const delta = update.message.value.text || "";
|
|
2034
2042
|
if (!state.currentThinkingBlock) {
|
|
2035
|
-
const block: ThinkingContent & {
|
|
2043
|
+
const block: ThinkingContent & { [kStreamingBlockIndex]: number } = {
|
|
2036
2044
|
type: "thinking",
|
|
2037
2045
|
thinking: "",
|
|
2038
|
-
|
|
2046
|
+
[kStreamingBlockIndex]: output.content.length,
|
|
2039
2047
|
};
|
|
2040
2048
|
output.content.push(block);
|
|
2041
2049
|
state.setThinkingBlock(block);
|
|
@@ -2059,9 +2067,9 @@ export function processInteractionUpdate(
|
|
|
2059
2067
|
id: args.toolCallId || crypto.randomUUID(),
|
|
2060
2068
|
name: args.name || args.toolName || "",
|
|
2061
2069
|
arguments: {},
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2070
|
+
[kStreamingBlockIndex]: output.content.length,
|
|
2071
|
+
[kStreamingPartialJson]: "",
|
|
2072
|
+
[kStreamingBlockKind]: "mcp",
|
|
2065
2073
|
};
|
|
2066
2074
|
output.content.push(block);
|
|
2067
2075
|
state.setToolCall(block);
|
|
@@ -2077,8 +2085,8 @@ export function processInteractionUpdate(
|
|
|
2077
2085
|
id: callId,
|
|
2078
2086
|
name: "todo",
|
|
2079
2087
|
arguments: todoArgs,
|
|
2080
|
-
|
|
2081
|
-
|
|
2088
|
+
[kStreamingBlockIndex]: output.content.length,
|
|
2089
|
+
[kStreamingBlockKind]: "todo",
|
|
2082
2090
|
};
|
|
2083
2091
|
output.content.push(block);
|
|
2084
2092
|
state.setToolCall(block);
|
|
@@ -2086,41 +2094,39 @@ export function processInteractionUpdate(
|
|
|
2086
2094
|
}
|
|
2087
2095
|
}
|
|
2088
2096
|
} else if (updateCase === "toolCallDelta" || updateCase === "partialToolCall") {
|
|
2089
|
-
if (state.currentToolCall?.
|
|
2097
|
+
if (state.currentToolCall?.[kStreamingBlockKind] === "mcp") {
|
|
2090
2098
|
// Cursor's `args_text_delta` is "aggregated args text so far" per agent.proto: each
|
|
2091
2099
|
// delta is a cumulative snapshot of the JSON-text args. Strip the prefix we already
|
|
2092
2100
|
// have to recover the new suffix; fall back to treating the value as an incremental
|
|
2093
2101
|
// fragment when it doesn't extend the buffer.
|
|
2094
2102
|
const snapshot: string = update.message.value.argsTextDelta || "";
|
|
2095
|
-
const current = state.currentToolCall
|
|
2103
|
+
const current = state.currentToolCall[kStreamingPartialJson] ?? "";
|
|
2096
2104
|
const chunk = snapshot.startsWith(current) ? snapshot.slice(current.length) : snapshot;
|
|
2097
2105
|
if (chunk.length === 0) {
|
|
2098
2106
|
return;
|
|
2099
2107
|
}
|
|
2100
|
-
state.currentToolCall
|
|
2101
|
-
state.currentToolCall.arguments = parseStreamingJson(state.currentToolCall
|
|
2108
|
+
state.currentToolCall[kStreamingPartialJson] = current + chunk;
|
|
2109
|
+
state.currentToolCall.arguments = parseStreamingJson(state.currentToolCall[kStreamingPartialJson]);
|
|
2102
2110
|
const idx = output.content.indexOf(state.currentToolCall);
|
|
2103
2111
|
stream.push({ type: "toolcall_delta", contentIndex: idx, delta: chunk, partial: output });
|
|
2104
2112
|
}
|
|
2105
2113
|
} else if (updateCase === "toolCallCompleted") {
|
|
2106
2114
|
if (state.currentToolCall) {
|
|
2107
2115
|
const toolCall = update.message.value.toolCall;
|
|
2108
|
-
if (state.currentToolCall
|
|
2116
|
+
if (state.currentToolCall[kStreamingBlockKind] === "mcp") {
|
|
2109
2117
|
const decodedArgs = decodeMcpArgsMap(toolCall?.mcpToolCall?.args?.args);
|
|
2110
2118
|
state.currentToolCall.arguments = mergeCursorMcpToolCallArgs(
|
|
2111
2119
|
state.currentToolCall.arguments as Record<string, unknown> | undefined,
|
|
2112
2120
|
decodedArgs,
|
|
2113
2121
|
);
|
|
2114
|
-
} else if (state.currentToolCall
|
|
2122
|
+
} else if (state.currentToolCall[kStreamingBlockKind] === "todo" && toolCall) {
|
|
2115
2123
|
const todoArgs = buildTodoArgs(toolCall);
|
|
2116
2124
|
if (todoArgs) {
|
|
2117
2125
|
state.currentToolCall.arguments = todoArgs;
|
|
2118
2126
|
}
|
|
2119
2127
|
}
|
|
2120
2128
|
const idx = output.content.indexOf(state.currentToolCall);
|
|
2121
|
-
|
|
2122
|
-
delete (state.currentToolCall as any).index;
|
|
2123
|
-
delete (state.currentToolCall as any).kind;
|
|
2129
|
+
clearStreamingPartialJson(state.currentToolCall);
|
|
2124
2130
|
stream.push({ type: "toolcall_end", contentIndex: idx, toolCall: state.currentToolCall, partial: output });
|
|
2125
2131
|
state.setToolCall(null);
|
|
2126
2132
|
}
|
|
@@ -2167,7 +2173,7 @@ function storeCursorBlob(blobStore: Map<string, Uint8Array>, data: Uint8Array):
|
|
|
2167
2173
|
function readCursorBlob(blobStore: Map<string, Uint8Array>, blobId: Uint8Array): Uint8Array {
|
|
2168
2174
|
const data = blobStore.get(Buffer.from(blobId).toString("hex"));
|
|
2169
2175
|
if (!data) {
|
|
2170
|
-
throw new
|
|
2176
|
+
throw new AIError.ValidationError("Cursor blob not found");
|
|
2171
2177
|
}
|
|
2172
2178
|
return data;
|
|
2173
2179
|
}
|
package/src/providers/devin.ts
CHANGED
|
@@ -28,7 +28,8 @@ import {
|
|
|
28
28
|
StopReason,
|
|
29
29
|
} from "@oh-my-pi/pi-catalog/discovery/devin-gen/exa/codeium_common_pb/codeium_common_pb";
|
|
30
30
|
import { calculateCost } from "@oh-my-pi/pi-catalog/models";
|
|
31
|
-
import {
|
|
31
|
+
import { logger, parseStreamingJson } from "@oh-my-pi/pi-utils";
|
|
32
|
+
import * as AIError from "../error";
|
|
32
33
|
import type {
|
|
33
34
|
Api,
|
|
34
35
|
AssistantMessage,
|
|
@@ -44,7 +45,6 @@ import type {
|
|
|
44
45
|
} from "../types";
|
|
45
46
|
import { deterministicUuid } from "../utils/deterministic-id";
|
|
46
47
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
47
|
-
import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
|
|
48
48
|
import { toolWireSchema } from "../utils/schema/wire";
|
|
49
49
|
|
|
50
50
|
/** Base host for Codeium/Windsurf's Cascade chat API (Connect protocol over HTTP/1.1). */
|
|
@@ -78,7 +78,7 @@ export const streamDevin: StreamFunction<"devin-agent"> = (
|
|
|
78
78
|
const stream = new AssistantMessageEventStream();
|
|
79
79
|
|
|
80
80
|
(async () => {
|
|
81
|
-
const startTime =
|
|
81
|
+
const startTime = performance.now();
|
|
82
82
|
let firstTokenTime: number | undefined;
|
|
83
83
|
|
|
84
84
|
const output: AssistantMessage = {
|
|
@@ -109,7 +109,7 @@ export const streamDevin: StreamFunction<"devin-agent"> = (
|
|
|
109
109
|
let latestStopReason = StopReason.UNSPECIFIED;
|
|
110
110
|
|
|
111
111
|
const markFirstToken = () => {
|
|
112
|
-
if (firstTokenTime === undefined) firstTokenTime =
|
|
112
|
+
if (firstTokenTime === undefined) firstTokenTime = performance.now();
|
|
113
113
|
};
|
|
114
114
|
|
|
115
115
|
const endTextBlock = () => {
|
|
@@ -169,12 +169,16 @@ export const streamDevin: StreamFunction<"devin-agent"> = (
|
|
|
169
169
|
|
|
170
170
|
if (!response.ok) {
|
|
171
171
|
const text = await response.text();
|
|
172
|
-
throw
|
|
173
|
-
status
|
|
174
|
-
|
|
172
|
+
throw new AIError.DevinApiError(
|
|
173
|
+
`Devin API error ${response.status} ${response.statusText}: ${text}`,
|
|
174
|
+
response.status,
|
|
175
|
+
);
|
|
175
176
|
}
|
|
176
177
|
if (!response.body) {
|
|
177
|
-
throw new
|
|
178
|
+
throw new AIError.ProviderResponseError("Devin API error: response body is empty", {
|
|
179
|
+
provider: model.provider,
|
|
180
|
+
kind: "empty-body",
|
|
181
|
+
});
|
|
178
182
|
}
|
|
179
183
|
const body = response.body;
|
|
180
184
|
|
|
@@ -199,7 +203,7 @@ export const streamDevin: StreamFunction<"devin-agent"> = (
|
|
|
199
203
|
if (flag & CONNECT_END_STREAM_FLAG) {
|
|
200
204
|
const trailerBytes = flag & CONNECT_COMPRESSED_FLAG ? gunzipSync(payload) : payload;
|
|
201
205
|
const trailerError = readConnectTrailerError(trailerBytes.toString("utf8").trim());
|
|
202
|
-
if (trailerError) throw new
|
|
206
|
+
if (trailerError) throw new AIError.ValidationError(trailerError);
|
|
203
207
|
continue;
|
|
204
208
|
}
|
|
205
209
|
|
|
@@ -318,20 +322,21 @@ export const streamDevin: StreamFunction<"devin-agent"> = (
|
|
|
318
322
|
output.stopReason = doneReason;
|
|
319
323
|
|
|
320
324
|
calculateCost(model, output.usage);
|
|
321
|
-
output.duration =
|
|
325
|
+
output.duration = performance.now() - startTime;
|
|
322
326
|
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
323
327
|
|
|
324
328
|
stream.push({ type: "done", reason: doneReason, message: output });
|
|
325
329
|
stream.end();
|
|
326
330
|
} catch (error) {
|
|
327
331
|
logger.error("devin: stream failed", { error: String(error) });
|
|
328
|
-
const
|
|
329
|
-
output.stopReason =
|
|
330
|
-
output.errorStatus =
|
|
331
|
-
output.
|
|
332
|
-
output.
|
|
332
|
+
const result = await AIError.finalize(error, { api: model.api, signal: options?.signal });
|
|
333
|
+
output.stopReason = result.stopReason;
|
|
334
|
+
output.errorStatus = result.status;
|
|
335
|
+
output.errorId = result.id;
|
|
336
|
+
output.errorMessage = result.message;
|
|
337
|
+
output.duration = performance.now() - startTime;
|
|
333
338
|
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
334
|
-
stream.push({ type: "error", reason:
|
|
339
|
+
stream.push({ type: "error", reason: result.stopReason, error: output });
|
|
335
340
|
stream.end();
|
|
336
341
|
}
|
|
337
342
|
})();
|
|
@@ -372,14 +377,17 @@ async function fetchDevinAuthMetadata(
|
|
|
372
377
|
});
|
|
373
378
|
const payload = new Uint8Array(await response.arrayBuffer());
|
|
374
379
|
if (!response.ok) {
|
|
375
|
-
throw
|
|
376
|
-
|
|
377
|
-
|
|
380
|
+
throw new AIError.DevinApiError(
|
|
381
|
+
`Devin auth error ${response.status} ${response.statusText}: ${new TextDecoder().decode(payload)}`,
|
|
382
|
+
response.status,
|
|
378
383
|
);
|
|
379
384
|
}
|
|
380
385
|
const decoded = decodeDevinUserJwtResponse(payload);
|
|
381
386
|
if (!decoded.userJwt) {
|
|
382
|
-
throw new
|
|
387
|
+
throw new AIError.ProviderResponseError("Devin auth error: GetUserJwt returned an empty user JWT", {
|
|
388
|
+
provider: "devin",
|
|
389
|
+
kind: "runtime",
|
|
390
|
+
});
|
|
383
391
|
}
|
|
384
392
|
const customBaseUrl = decoded.customApiServerUrl.trim();
|
|
385
393
|
return { userJwt: decoded.userJwt, ...(customBaseUrl ? { baseUrl: customBaseUrl.replace(/\/+$/, "") } : undefined) };
|