Fix X-Initiator header logic for GitHub Copilot

Check last message role instead of any message in history.
This matches the original correct implementation from PR #200.

fixes #209
This commit is contained in:
Mario Zechner 2025-12-19 05:08:28 +01:00
parent 13b8af1f36
commit 575dcb2676
5 changed files with 18 additions and 14 deletions

2
package-lock.json generated
View file

@ -6555,7 +6555,7 @@
"chalk": "^5.5.0"
},
"bin": {
"pi": "dist/cli.js"
"pi-pods": "dist/cli.js"
},
"devDependencies": {},
"engines": {

View file

@ -3225,7 +3225,7 @@ export const MODELS = {
cost: {
input: 0.24,
output: 0.38,
cacheRead: 0.19,
cacheRead: 0.11,
cacheWrite: 0,
},
contextWindow: 163840,

View file

@ -302,9 +302,11 @@ function createClient(model: Model<"openai-completions">, context: Context, apiK
const headers = { ...model.headers };
if (model.provider === "github-copilot") {
// Copilot expects X-Initiator to indicate whether the request is user-initiated
// or agent-initiated. It's an agent call if ANY message in history has assistant/tool role.
// or agent-initiated (e.g. follow-up after assistant/tool messages). If there is
// no prior message, default to user-initiated.
const messages = context.messages || [];
const isAgentCall = messages.some((msg) => msg.role === "assistant" || msg.role === "toolResult");
const lastMessage = messages[messages.length - 1];
const isAgentCall = lastMessage ? lastMessage.role !== "user" : false;
headers["X-Initiator"] = isAgentCall ? "agent" : "user";
headers["Openai-Intent"] = "conversation-edits";
}

View file

@ -310,9 +310,11 @@ function createClient(model: Model<"openai-responses">, context: Context, apiKey
const headers = { ...model.headers };
if (model.provider === "github-copilot") {
// Copilot expects X-Initiator to indicate whether the request is user-initiated
// or agent-initiated. It's an agent call if ANY message in history has assistant/tool role.
// or agent-initiated (e.g. follow-up after assistant/tool messages). If there is
// no prior message, default to user-initiated.
const messages = context.messages || [];
const isAgentCall = messages.some((msg) => msg.role === "assistant" || msg.role === "toolResult");
const lastMessage = messages[messages.length - 1];
const isAgentCall = lastMessage ? lastMessage.role !== "user" : false;
headers["X-Initiator"] = isAgentCall ? "agent" : "user";
headers["Openai-Intent"] = "conversation-edits";
}

View file

@ -175,7 +175,7 @@ describe("GitHub Copilot Headers", () => {
expect(lastOpenAIConfig?.defaultHeaders?.["X-Initiator"]).toBe("user");
});
it("sets X-Initiator: agent when assistant message exists in history", async () => {
it("sets X-Initiator: agent when last message is assistant", async () => {
const context: Context = {
messages: [{ role: "user", content: "Hello", timestamp: Date.now() }, assistantMessage],
};
@ -186,7 +186,7 @@ describe("GitHub Copilot Headers", () => {
expect(lastOpenAIConfig?.defaultHeaders?.["X-Initiator"]).toBe("agent");
});
it("sets X-Initiator: agent when toolResult exists in history", async () => {
it("sets X-Initiator: agent when last message is toolResult", async () => {
const context: Context = {
messages: [{ role: "user", content: "Hello", timestamp: Date.now() }, toolResultMessage],
};
@ -197,7 +197,7 @@ describe("GitHub Copilot Headers", () => {
expect(lastOpenAIConfig?.defaultHeaders?.["X-Initiator"]).toBe("agent");
});
it("sets X-Initiator: agent for multi-turn conversation (last is user, but assistant in history)", async () => {
it("sets X-Initiator: user for multi-turn conversation when last message is user", async () => {
const context: Context = {
messages: [
{ role: "user", content: "Hello", timestamp: Date.now() },
@ -209,7 +209,7 @@ describe("GitHub Copilot Headers", () => {
const stream = streamOpenAICompletions(copilotCompletionsModel, context, { apiKey: "test-key" });
await consumeStream(stream);
expect(lastOpenAIConfig?.defaultHeaders?.["X-Initiator"]).toBe("agent");
expect(lastOpenAIConfig?.defaultHeaders?.["X-Initiator"]).toBe("user");
});
it("sets X-Initiator: user when there are no messages", async () => {
@ -259,7 +259,7 @@ describe("GitHub Copilot Headers", () => {
expect(lastOpenAIConfig?.defaultHeaders?.["X-Initiator"]).toBe("user");
});
it("sets X-Initiator: agent when assistant message exists in history", async () => {
it("sets X-Initiator: agent when last message is assistant", async () => {
const context: Context = {
messages: [
{ role: "user", content: "Hello", timestamp: Date.now() },
@ -273,7 +273,7 @@ describe("GitHub Copilot Headers", () => {
expect(lastOpenAIConfig?.defaultHeaders?.["X-Initiator"]).toBe("agent");
});
it("sets X-Initiator: agent when toolResult exists in history", async () => {
it("sets X-Initiator: agent when last message is toolResult", async () => {
const context: Context = {
messages: [{ role: "user", content: "Hello", timestamp: Date.now() }, toolResultMessage],
};
@ -284,7 +284,7 @@ describe("GitHub Copilot Headers", () => {
expect(lastOpenAIConfig?.defaultHeaders?.["X-Initiator"]).toBe("agent");
});
it("sets X-Initiator: agent for multi-turn conversation (last is user, but assistant in history)", async () => {
it("sets X-Initiator: user for multi-turn conversation when last message is user", async () => {
const context: Context = {
messages: [
{ role: "user", content: "Hello", timestamp: Date.now() },
@ -296,7 +296,7 @@ describe("GitHub Copilot Headers", () => {
const stream = streamOpenAIResponses(copilotResponsesModel, context, { apiKey: "test-key" });
await consumeStream(stream);
expect(lastOpenAIConfig?.defaultHeaders?.["X-Initiator"]).toBe("agent");
expect(lastOpenAIConfig?.defaultHeaders?.["X-Initiator"]).toBe("user");
});
it("sets X-Initiator: user when there are no messages", async () => {