mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-20 10:01:21 +00:00
feat(ai): route copilot claude via anthropic messages api
This commit is contained in:
parent
cf1353b8e7
commit
0a132a30a1
9 changed files with 196 additions and 76 deletions
|
|
@ -519,13 +519,21 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
|
||||||
if (m.tool_call !== true) continue;
|
if (m.tool_call !== true) continue;
|
||||||
if (m.status === "deprecated") continue;
|
if (m.status === "deprecated") continue;
|
||||||
|
|
||||||
|
// Claude 4.x models route to Anthropic Messages API
|
||||||
|
const isCopilotClaude4 = /^claude-(haiku|sonnet|opus)-4([.\-]|$)/.test(modelId);
|
||||||
// gpt-5 models require responses API, others use completions
|
// gpt-5 models require responses API, others use completions
|
||||||
const needsResponsesApi = modelId.startsWith("gpt-5") || modelId.startsWith("oswe");
|
const needsResponsesApi = modelId.startsWith("gpt-5") || modelId.startsWith("oswe");
|
||||||
|
|
||||||
|
const api: Api = isCopilotClaude4
|
||||||
|
? "anthropic-messages"
|
||||||
|
: needsResponsesApi
|
||||||
|
? "openai-responses"
|
||||||
|
: "openai-completions";
|
||||||
|
|
||||||
const copilotModel: Model<any> = {
|
const copilotModel: Model<any> = {
|
||||||
id: modelId,
|
id: modelId,
|
||||||
name: m.name || modelId,
|
name: m.name || modelId,
|
||||||
api: needsResponsesApi ? "openai-responses" : "openai-completions",
|
api,
|
||||||
provider: "github-copilot",
|
provider: "github-copilot",
|
||||||
baseUrl: "https://api.individual.githubcopilot.com",
|
baseUrl: "https://api.individual.githubcopilot.com",
|
||||||
reasoning: m.reasoning === true,
|
reasoning: m.reasoning === true,
|
||||||
|
|
@ -540,13 +548,13 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
|
||||||
maxTokens: m.limit?.output || 8192,
|
maxTokens: m.limit?.output || 8192,
|
||||||
headers: { ...COPILOT_STATIC_HEADERS },
|
headers: { ...COPILOT_STATIC_HEADERS },
|
||||||
// compat only applies to openai-completions
|
// compat only applies to openai-completions
|
||||||
...(needsResponsesApi ? {} : {
|
...(api === "openai-completions" ? {
|
||||||
compat: {
|
compat: {
|
||||||
supportsStore: false,
|
supportsStore: false,
|
||||||
supportsDeveloperRole: false,
|
supportsDeveloperRole: false,
|
||||||
supportsReasoningEffort: false,
|
supportsReasoningEffort: false,
|
||||||
},
|
},
|
||||||
}),
|
} : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
models.push(copilotModel);
|
models.push(copilotModel);
|
||||||
|
|
|
||||||
|
|
@ -2260,11 +2260,10 @@ export const MODELS = {
|
||||||
"claude-haiku-4.5": {
|
"claude-haiku-4.5": {
|
||||||
id: "claude-haiku-4.5",
|
id: "claude-haiku-4.5",
|
||||||
name: "Claude Haiku 4.5",
|
name: "Claude Haiku 4.5",
|
||||||
api: "openai-completions",
|
api: "anthropic-messages",
|
||||||
provider: "github-copilot",
|
provider: "github-copilot",
|
||||||
baseUrl: "https://api.individual.githubcopilot.com",
|
baseUrl: "https://api.individual.githubcopilot.com",
|
||||||
headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"},
|
headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"},
|
||||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false},
|
|
||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text", "image"],
|
input: ["text", "image"],
|
||||||
cost: {
|
cost: {
|
||||||
|
|
@ -2275,15 +2274,14 @@ export const MODELS = {
|
||||||
},
|
},
|
||||||
contextWindow: 128000,
|
contextWindow: 128000,
|
||||||
maxTokens: 16000,
|
maxTokens: 16000,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"anthropic-messages">,
|
||||||
"claude-opus-4.5": {
|
"claude-opus-4.5": {
|
||||||
id: "claude-opus-4.5",
|
id: "claude-opus-4.5",
|
||||||
name: "Claude Opus 4.5",
|
name: "Claude Opus 4.5",
|
||||||
api: "openai-completions",
|
api: "anthropic-messages",
|
||||||
provider: "github-copilot",
|
provider: "github-copilot",
|
||||||
baseUrl: "https://api.individual.githubcopilot.com",
|
baseUrl: "https://api.individual.githubcopilot.com",
|
||||||
headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"},
|
headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"},
|
||||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false},
|
|
||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text", "image"],
|
input: ["text", "image"],
|
||||||
cost: {
|
cost: {
|
||||||
|
|
@ -2294,15 +2292,14 @@ export const MODELS = {
|
||||||
},
|
},
|
||||||
contextWindow: 128000,
|
contextWindow: 128000,
|
||||||
maxTokens: 16000,
|
maxTokens: 16000,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"anthropic-messages">,
|
||||||
"claude-opus-4.6": {
|
"claude-opus-4.6": {
|
||||||
id: "claude-opus-4.6",
|
id: "claude-opus-4.6",
|
||||||
name: "Claude Opus 4.6",
|
name: "Claude Opus 4.6",
|
||||||
api: "openai-completions",
|
api: "anthropic-messages",
|
||||||
provider: "github-copilot",
|
provider: "github-copilot",
|
||||||
baseUrl: "https://api.individual.githubcopilot.com",
|
baseUrl: "https://api.individual.githubcopilot.com",
|
||||||
headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"},
|
headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"},
|
||||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false},
|
|
||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text", "image"],
|
input: ["text", "image"],
|
||||||
cost: {
|
cost: {
|
||||||
|
|
@ -2313,15 +2310,14 @@ export const MODELS = {
|
||||||
},
|
},
|
||||||
contextWindow: 128000,
|
contextWindow: 128000,
|
||||||
maxTokens: 64000,
|
maxTokens: 64000,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"anthropic-messages">,
|
||||||
"claude-sonnet-4": {
|
"claude-sonnet-4": {
|
||||||
id: "claude-sonnet-4",
|
id: "claude-sonnet-4",
|
||||||
name: "Claude Sonnet 4",
|
name: "Claude Sonnet 4",
|
||||||
api: "openai-completions",
|
api: "anthropic-messages",
|
||||||
provider: "github-copilot",
|
provider: "github-copilot",
|
||||||
baseUrl: "https://api.individual.githubcopilot.com",
|
baseUrl: "https://api.individual.githubcopilot.com",
|
||||||
headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"},
|
headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"},
|
||||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false},
|
|
||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text", "image"],
|
input: ["text", "image"],
|
||||||
cost: {
|
cost: {
|
||||||
|
|
@ -2332,15 +2328,14 @@ export const MODELS = {
|
||||||
},
|
},
|
||||||
contextWindow: 128000,
|
contextWindow: 128000,
|
||||||
maxTokens: 16000,
|
maxTokens: 16000,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"anthropic-messages">,
|
||||||
"claude-sonnet-4.5": {
|
"claude-sonnet-4.5": {
|
||||||
id: "claude-sonnet-4.5",
|
id: "claude-sonnet-4.5",
|
||||||
name: "Claude Sonnet 4.5",
|
name: "Claude Sonnet 4.5",
|
||||||
api: "openai-completions",
|
api: "anthropic-messages",
|
||||||
provider: "github-copilot",
|
provider: "github-copilot",
|
||||||
baseUrl: "https://api.individual.githubcopilot.com",
|
baseUrl: "https://api.individual.githubcopilot.com",
|
||||||
headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"},
|
headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"},
|
||||||
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false},
|
|
||||||
reasoning: true,
|
reasoning: true,
|
||||||
input: ["text", "image"],
|
input: ["text", "image"],
|
||||||
cost: {
|
cost: {
|
||||||
|
|
@ -2351,7 +2346,7 @@ export const MODELS = {
|
||||||
},
|
},
|
||||||
contextWindow: 128000,
|
contextWindow: 128000,
|
||||||
maxTokens: 16000,
|
maxTokens: 16000,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"anthropic-messages">,
|
||||||
"gemini-2.5-pro": {
|
"gemini-2.5-pro": {
|
||||||
id: "gemini-2.5-pro",
|
id: "gemini-2.5-pro",
|
||||||
name: "Gemini 2.5 Pro",
|
name: "Gemini 2.5 Pro",
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
||||||
import { parseStreamingJson } from "../utils/json-parse.js";
|
import { parseStreamingJson } from "../utils/json-parse.js";
|
||||||
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
||||||
|
|
||||||
|
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
|
||||||
import { adjustMaxTokensForThinking, buildBaseOptions } from "./simple-options.js";
|
import { adjustMaxTokensForThinking, buildBaseOptions } from "./simple-options.js";
|
||||||
import { transformMessages } from "./transform-messages.js";
|
import { transformMessages } from "./transform-messages.js";
|
||||||
|
|
||||||
|
|
@ -217,11 +218,22 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOpti
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const apiKey = options?.apiKey ?? getEnvApiKey(model.provider) ?? "";
|
const apiKey = options?.apiKey ?? getEnvApiKey(model.provider) ?? "";
|
||||||
|
|
||||||
|
let copilotDynamicHeaders: Record<string, string> | undefined;
|
||||||
|
if (model.provider === "github-copilot") {
|
||||||
|
const hasImages = hasCopilotVisionInput(context.messages);
|
||||||
|
copilotDynamicHeaders = buildCopilotDynamicHeaders({
|
||||||
|
messages: context.messages,
|
||||||
|
hasImages,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const { client, isOAuthToken } = createClient(
|
const { client, isOAuthToken } = createClient(
|
||||||
model,
|
model,
|
||||||
apiKey,
|
apiKey,
|
||||||
options?.interleavedThinking ?? true,
|
options?.interleavedThinking ?? true,
|
||||||
options?.headers,
|
options?.headers,
|
||||||
|
copilotDynamicHeaders,
|
||||||
);
|
);
|
||||||
const params = buildParams(model, context, isOAuthToken, options);
|
const params = buildParams(model, context, isOAuthToken, options);
|
||||||
options?.onPayload?.(params);
|
options?.onPayload?.(params);
|
||||||
|
|
@ -471,12 +483,54 @@ function isOAuthToken(apiKey: string): boolean {
|
||||||
return apiKey.includes("sk-ant-oat");
|
return apiKey.includes("sk-ant-oat");
|
||||||
}
|
}
|
||||||
|
|
||||||
function createClient(
|
export interface BuildAnthropicClientOptionsParams {
|
||||||
model: Model<"anthropic-messages">,
|
model: Model<"anthropic-messages">;
|
||||||
apiKey: string,
|
apiKey: string;
|
||||||
interleavedThinking: boolean,
|
interleavedThinking: boolean;
|
||||||
optionsHeaders?: Record<string, string>,
|
dynamicHeaders?: Record<string, string>;
|
||||||
): { client: Anthropic; isOAuthToken: boolean } {
|
optionsHeaders?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnthropicClientConfig {
|
||||||
|
apiKey: string | null;
|
||||||
|
authToken?: string;
|
||||||
|
baseURL: string;
|
||||||
|
defaultHeaders: Record<string, string>;
|
||||||
|
dangerouslyAllowBrowser: boolean;
|
||||||
|
isOAuthToken: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAnthropicClientOptions(params: BuildAnthropicClientOptionsParams): AnthropicClientConfig {
|
||||||
|
const { model, apiKey, interleavedThinking, dynamicHeaders, optionsHeaders } = params;
|
||||||
|
|
||||||
|
// Copilot: Bearer auth, selective betas
|
||||||
|
if (model.provider === "github-copilot") {
|
||||||
|
const betaFeatures: string[] = [];
|
||||||
|
if (interleavedThinking) {
|
||||||
|
betaFeatures.push("interleaved-thinking-2025-05-14");
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultHeaders = mergeHeaders(
|
||||||
|
{
|
||||||
|
accept: "application/json",
|
||||||
|
"anthropic-dangerous-direct-browser-access": "true",
|
||||||
|
...(betaFeatures.length > 0 ? { "anthropic-beta": betaFeatures.join(",") } : {}),
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
dynamicHeaders,
|
||||||
|
model.headers,
|
||||||
|
optionsHeaders,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiKey: null,
|
||||||
|
baseURL: model.baseUrl,
|
||||||
|
defaultHeaders,
|
||||||
|
dangerouslyAllowBrowser: true,
|
||||||
|
isOAuthToken: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const betaFeatures = ["fine-grained-tool-streaming-2025-05-14"];
|
const betaFeatures = ["fine-grained-tool-streaming-2025-05-14"];
|
||||||
if (interleavedThinking) {
|
if (interleavedThinking) {
|
||||||
betaFeatures.push("interleaved-thinking-2025-05-14");
|
betaFeatures.push("interleaved-thinking-2025-05-14");
|
||||||
|
|
@ -484,7 +538,6 @@ function createClient(
|
||||||
|
|
||||||
const oauthToken = isOAuthToken(apiKey);
|
const oauthToken = isOAuthToken(apiKey);
|
||||||
if (oauthToken) {
|
if (oauthToken) {
|
||||||
// Stealth mode: Mimic Claude Code's headers exactly
|
|
||||||
const defaultHeaders = mergeHeaders(
|
const defaultHeaders = mergeHeaders(
|
||||||
{
|
{
|
||||||
accept: "application/json",
|
accept: "application/json",
|
||||||
|
|
@ -497,15 +550,14 @@ function createClient(
|
||||||
optionsHeaders,
|
optionsHeaders,
|
||||||
);
|
);
|
||||||
|
|
||||||
const client = new Anthropic({
|
return {
|
||||||
apiKey: null,
|
apiKey: null,
|
||||||
authToken: apiKey,
|
authToken: apiKey,
|
||||||
baseURL: model.baseUrl,
|
baseURL: model.baseUrl,
|
||||||
defaultHeaders,
|
defaultHeaders,
|
||||||
dangerouslyAllowBrowser: true,
|
dangerouslyAllowBrowser: true,
|
||||||
});
|
isOAuthToken: true,
|
||||||
|
};
|
||||||
return { client, isOAuthToken: true };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultHeaders = mergeHeaders(
|
const defaultHeaders = mergeHeaders(
|
||||||
|
|
@ -518,14 +570,39 @@ function createClient(
|
||||||
optionsHeaders,
|
optionsHeaders,
|
||||||
);
|
);
|
||||||
|
|
||||||
const client = new Anthropic({
|
return {
|
||||||
apiKey,
|
apiKey,
|
||||||
baseURL: model.baseUrl,
|
baseURL: model.baseUrl,
|
||||||
dangerouslyAllowBrowser: true,
|
|
||||||
defaultHeaders,
|
defaultHeaders,
|
||||||
|
dangerouslyAllowBrowser: true,
|
||||||
|
isOAuthToken: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createClient(
|
||||||
|
model: Model<"anthropic-messages">,
|
||||||
|
apiKey: string,
|
||||||
|
interleavedThinking: boolean,
|
||||||
|
optionsHeaders?: Record<string, string>,
|
||||||
|
dynamicHeaders?: Record<string, string>,
|
||||||
|
): { client: Anthropic; isOAuthToken: boolean } {
|
||||||
|
const config = buildAnthropicClientOptions({
|
||||||
|
model,
|
||||||
|
apiKey,
|
||||||
|
interleavedThinking,
|
||||||
|
dynamicHeaders,
|
||||||
|
optionsHeaders,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { client, isOAuthToken: false };
|
const client = new Anthropic({
|
||||||
|
apiKey: config.apiKey,
|
||||||
|
...(config.authToken ? { authToken: config.authToken } : {}),
|
||||||
|
baseURL: config.baseURL,
|
||||||
|
defaultHeaders: config.defaultHeaders,
|
||||||
|
dangerouslyAllowBrowser: config.dangerouslyAllowBrowser,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { client, isOAuthToken: config.isOAuthToken };
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildParams(
|
function buildParams(
|
||||||
|
|
|
||||||
59
packages/ai/src/providers/github-copilot-headers.ts
Normal file
59
packages/ai/src/providers/github-copilot-headers.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import type { Message } from "../types.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Infer whether the current request to Copilot is user-initiated or agent-initiated.
|
||||||
|
* Accepts `unknown[]` because providers may pass pre-converted message shapes.
|
||||||
|
*/
|
||||||
|
export function inferCopilotInitiator(messages: unknown[]): "user" | "agent" {
|
||||||
|
if (messages.length === 0) return "user";
|
||||||
|
|
||||||
|
const last = messages[messages.length - 1] as Record<string, unknown>;
|
||||||
|
const role = last.role as string | undefined;
|
||||||
|
if (!role) return "user";
|
||||||
|
|
||||||
|
if (role !== "user") return "agent";
|
||||||
|
|
||||||
|
// Check if last content block is a tool_result (Anthropic-converted shape)
|
||||||
|
const content = last.content;
|
||||||
|
if (Array.isArray(content) && content.length > 0) {
|
||||||
|
const lastBlock = content[content.length - 1] as Record<string, unknown>;
|
||||||
|
if (lastBlock.type === "tool_result") {
|
||||||
|
return "agent";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "user";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check whether any message in the conversation contains image content. */
|
||||||
|
export function hasCopilotVisionInput(messages: Message[]): boolean {
|
||||||
|
return messages.some((msg) => {
|
||||||
|
if (msg.role === "user" && Array.isArray(msg.content)) {
|
||||||
|
return msg.content.some((c) => c.type === "image");
|
||||||
|
}
|
||||||
|
if (msg.role === "toolResult" && Array.isArray(msg.content)) {
|
||||||
|
return msg.content.some((c) => c.type === "image");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build dynamic Copilot headers that vary per-request.
|
||||||
|
* Static headers (User-Agent, Editor-Version, etc.) come from model.headers.
|
||||||
|
*/
|
||||||
|
export function buildCopilotDynamicHeaders(params: {
|
||||||
|
messages: unknown[];
|
||||||
|
hasImages: boolean;
|
||||||
|
}): Record<string, string> {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
"X-Initiator": inferCopilotInitiator(params.messages),
|
||||||
|
"Openai-Intent": "conversation-edits",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (params.hasImages) {
|
||||||
|
headers["Copilot-Vision-Request"] = "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
@ -29,6 +29,7 @@ import type {
|
||||||
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
||||||
import { parseStreamingJson } from "../utils/json-parse.js";
|
import { parseStreamingJson } from "../utils/json-parse.js";
|
||||||
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
||||||
|
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
|
||||||
import { buildBaseOptions, clampReasoning } from "./simple-options.js";
|
import { buildBaseOptions, clampReasoning } from "./simple-options.js";
|
||||||
import { transformMessages } from "./transform-messages.js";
|
import { transformMessages } from "./transform-messages.js";
|
||||||
|
|
||||||
|
|
@ -359,28 +360,12 @@ function createClient(
|
||||||
|
|
||||||
const headers = { ...model.headers };
|
const headers = { ...model.headers };
|
||||||
if (model.provider === "github-copilot") {
|
if (model.provider === "github-copilot") {
|
||||||
// Copilot expects X-Initiator to indicate whether the request is user-initiated
|
const hasImages = hasCopilotVisionInput(context.messages);
|
||||||
// or agent-initiated (e.g. follow-up after assistant/tool messages). If there is
|
const copilotHeaders = buildCopilotDynamicHeaders({
|
||||||
// no prior message, default to user-initiated.
|
messages: context.messages,
|
||||||
const messages = context.messages || [];
|
hasImages,
|
||||||
const lastMessage = messages[messages.length - 1];
|
|
||||||
const isAgentCall = lastMessage ? lastMessage.role !== "user" : false;
|
|
||||||
headers["X-Initiator"] = isAgentCall ? "agent" : "user";
|
|
||||||
headers["Openai-Intent"] = "conversation-edits";
|
|
||||||
|
|
||||||
// Copilot requires this header when sending images
|
|
||||||
const hasImages = messages.some((msg) => {
|
|
||||||
if (msg.role === "user" && Array.isArray(msg.content)) {
|
|
||||||
return msg.content.some((c) => c.type === "image");
|
|
||||||
}
|
|
||||||
if (msg.role === "toolResult" && Array.isArray(msg.content)) {
|
|
||||||
return msg.content.some((c) => c.type === "image");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
});
|
||||||
if (hasImages) {
|
Object.assign(headers, copilotHeaders);
|
||||||
headers["Copilot-Vision-Request"] = "true";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge options headers last so they can override defaults
|
// Merge options headers last so they can override defaults
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import type {
|
||||||
Usage,
|
Usage,
|
||||||
} from "../types.js";
|
} from "../types.js";
|
||||||
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
||||||
|
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
|
||||||
import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.js";
|
import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.js";
|
||||||
import { buildBaseOptions, clampReasoning } from "./simple-options.js";
|
import { buildBaseOptions, clampReasoning } from "./simple-options.js";
|
||||||
|
|
||||||
|
|
@ -159,28 +160,12 @@ function createClient(
|
||||||
|
|
||||||
const headers = { ...model.headers };
|
const headers = { ...model.headers };
|
||||||
if (model.provider === "github-copilot") {
|
if (model.provider === "github-copilot") {
|
||||||
// Copilot expects X-Initiator to indicate whether the request is user-initiated
|
const hasImages = hasCopilotVisionInput(context.messages);
|
||||||
// or agent-initiated (e.g. follow-up after assistant/tool messages). If there is
|
const copilotHeaders = buildCopilotDynamicHeaders({
|
||||||
// no prior message, default to user-initiated.
|
messages: context.messages,
|
||||||
const messages = context.messages || [];
|
hasImages,
|
||||||
const lastMessage = messages[messages.length - 1];
|
|
||||||
const isAgentCall = lastMessage ? lastMessage.role !== "user" : false;
|
|
||||||
headers["X-Initiator"] = isAgentCall ? "agent" : "user";
|
|
||||||
headers["Openai-Intent"] = "conversation-edits";
|
|
||||||
|
|
||||||
// Copilot requires this header when sending images
|
|
||||||
const hasImages = messages.some((msg) => {
|
|
||||||
if (msg.role === "user" && Array.isArray(msg.content)) {
|
|
||||||
return msg.content.some((c) => c.type === "image");
|
|
||||||
}
|
|
||||||
if (msg.role === "toolResult" && Array.isArray(msg.content)) {
|
|
||||||
return msg.content.some((c) => c.type === "image");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
});
|
||||||
if (hasImages) {
|
Object.assign(headers, copilotHeaders);
|
||||||
headers["Copilot-Vision-Request"] = "true";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge options headers last so they can override defaults
|
// Merge options headers last so they can override defaults
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ describe("Anthropic Copilot auth config", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(options.apiKey).toBeNull();
|
expect(options.apiKey).toBeNull();
|
||||||
expect(options.defaultHeaders?.["Authorization"]).toBe(`Bearer ${token}`);
|
expect(options.defaultHeaders?.Authorization).toBe(`Bearer ${token}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes Copilot static headers from model.headers", () => {
|
it("includes Copilot static headers from model.headers", () => {
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ describe("Copilot Claude model routing", () => {
|
||||||
|
|
||||||
it("does not have compat block on Claude models (completions-API-specific)", () => {
|
it("does not have compat block on Claude models (completions-API-specific)", () => {
|
||||||
const sonnet = getModel("github-copilot", "claude-sonnet-4");
|
const sonnet = getModel("github-copilot", "claude-sonnet-4");
|
||||||
expect((sonnet as any).compat).toBeUndefined();
|
expect("compat" in sonnet).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("preserves static Copilot headers on Claude models", () => {
|
it("preserves static Copilot headers on Claude models", () => {
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,17 @@ describe("inferCopilotInitiator", () => {
|
||||||
];
|
];
|
||||||
expect(inferCopilotInitiator(messages)).toBe("agent");
|
expect(inferCopilotInitiator(messages)).toBe("agent");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns 'agent' for any non-user role (e.g. 'tool' in OpenAI format)", () => {
|
||||||
|
const messages: unknown[] = [
|
||||||
|
{
|
||||||
|
role: "tool",
|
||||||
|
tool_call_id: "call_abc123",
|
||||||
|
content: "tool output",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expect(inferCopilotInitiator(messages)).toBe("agent");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("hasCopilotVisionInput", () => {
|
describe("hasCopilotVisionInput", () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue