574a83f5f5
The onPayload hook contract (README, docs/extensions.md) is that a non-undefined return replaces the provider request payload, and every provider except these three implements it (anthropic, openai-responses family, google, ollama — see the earlier fix for the responses providers). openai-completions, amazon-bedrock and cursor invoked the hook fire-and-forget and sent the original payload, so extensions hooking before_provider_request could never transform the wire body on these providers. - openai-completions: await the hook and apply a non-undefined replacement to the params used for the request body, raw request dump and error-path fallback state - amazon-bedrock: same for the ConverseStream command input - cursor: await the hook for the AgentRunRequest; buildGrpcRequest becomes async and is exported for direct testing (transport is HTTP/2) - devin-agent intentionally unchanged: it does not fire the hook at all (its payload is a protobuf object), which is a feature gap rather than a dropped replacement; documented in README/docs instead - regression tests: captured wire body reflects async/sync replacement, and an undefined return keeps the original payload (completions + bedrock over a mocked fetch; cursor by decoding the serialized run request)
68 lines
2.4 KiB
TypeScript
68 lines
2.4 KiB
TypeScript
// Regression: cursor ignored the onPayload replacement return value
|
|
// (fire-and-forget), so the hook could never change the request actually sent
|
|
// upstream. The replacement contract matches anthropic / openai-responses /
|
|
// google: await the hook and use its non-undefined return as the request.
|
|
// buildGrpcRequest is exercised directly (the transport is HTTP/2), and the
|
|
// serialized run request is decoded back from the wire bytes.
|
|
import { describe, expect, it } from "bun:test";
|
|
import { fromBinary } from "@bufbuild/protobuf";
|
|
import { buildGrpcRequest } from "@oh-my-pi/pi-ai/providers/cursor";
|
|
import { AgentClientMessageSchema } from "@oh-my-pi/pi-catalog/discovery/cursor-gen/agent_pb";
|
|
import type { Context, Model } from "@oh-my-pi/pi-ai/types";
|
|
import { buildModel } from "@oh-my-pi/pi-catalog/build";
|
|
|
|
const model: Model<"cursor-agent"> = buildModel({
|
|
id: "cursor-composer-2.5",
|
|
name: "Cursor Composer 2.5",
|
|
api: "cursor-agent",
|
|
provider: "cursor",
|
|
baseUrl: "https://api2.cursor.sh",
|
|
reasoning: false,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 200_000,
|
|
maxTokens: 32_000,
|
|
});
|
|
|
|
const context: Context = {
|
|
messages: [{ role: "user", content: "Say hello", timestamp: 0 }],
|
|
};
|
|
|
|
function decodeRunRequest(requestBytes: Uint8Array): { case: string; value: Record<string, any> } {
|
|
const decoded = fromBinary(AgentClientMessageSchema, requestBytes);
|
|
return decoded.message as unknown as { case: string; value: Record<string, any> };
|
|
}
|
|
|
|
describe("cursor onPayload replacement", () => {
|
|
it("sends an async onPayload replacement body", async () => {
|
|
const { requestBytes } = await buildGrpcRequest(
|
|
model,
|
|
context,
|
|
{
|
|
onPayload: async payload => ({
|
|
...(payload as Record<string, unknown>),
|
|
customSystemPrompt: "replacement",
|
|
}),
|
|
},
|
|
{ conversationId: "conv-1", blobStore: new Map() },
|
|
);
|
|
|
|
const message = decodeRunRequest(requestBytes);
|
|
expect(message.case).toBe("runRequest");
|
|
expect(message.value.customSystemPrompt).toBe("replacement");
|
|
});
|
|
|
|
it("keeps the original body when onPayload returns undefined", async () => {
|
|
const { requestBytes } = await buildGrpcRequest(
|
|
model,
|
|
context,
|
|
{ onPayload: async () => undefined },
|
|
{ conversationId: "conv-1", blobStore: new Map() },
|
|
);
|
|
|
|
const message = decodeRunRequest(requestBytes);
|
|
expect(message.case).toBe("runRequest");
|
|
expect(message.value.customSystemPrompt).toBeUndefined();
|
|
});
|
|
});
|