feat(ai): route copilot claude via anthropic messages api

This commit is contained in:
Nate Smyth
2026-02-06 05:09:52 -05:00
committed by Mario Zechner
parent cf1353b8e7
commit 0a132a30a1
9 changed files with 196 additions and 76 deletions

View File

@@ -28,6 +28,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream.js";
import { parseStreamingJson } from "../utils/json-parse.js";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
import { adjustMaxTokensForThinking, buildBaseOptions } from "./simple-options.js";
import { transformMessages } from "./transform-messages.js";
@@ -217,11 +218,22 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOpti
try {
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(
model,
apiKey,
options?.interleavedThinking ?? true,
options?.headers,
copilotDynamicHeaders,
);
const params = buildParams(model, context, isOAuthToken, options);
options?.onPayload?.(params);
@@ -471,12 +483,54 @@ function isOAuthToken(apiKey: string): boolean {
return apiKey.includes("sk-ant-oat");
}
function createClient(
model: Model<"anthropic-messages">,
apiKey: string,
interleavedThinking: boolean,
optionsHeaders?: Record<string, string>,
): { client: Anthropic; isOAuthToken: boolean } {
export interface BuildAnthropicClientOptionsParams {
model: Model<"anthropic-messages">;
apiKey: string;
interleavedThinking: boolean;
dynamicHeaders?: Record<string, string>;
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"];
if (interleavedThinking) {
betaFeatures.push("interleaved-thinking-2025-05-14");
@@ -484,7 +538,6 @@ function createClient(
const oauthToken = isOAuthToken(apiKey);
if (oauthToken) {
// Stealth mode: Mimic Claude Code's headers exactly
const defaultHeaders = mergeHeaders(
{
accept: "application/json",
@@ -497,15 +550,14 @@ function createClient(
optionsHeaders,
);
const client = new Anthropic({
return {
apiKey: null,
authToken: apiKey,
baseURL: model.baseUrl,
defaultHeaders,
dangerouslyAllowBrowser: true,
});
return { client, isOAuthToken: true };
isOAuthToken: true,
};
}
const defaultHeaders = mergeHeaders(
@@ -518,14 +570,39 @@ function createClient(
optionsHeaders,
);
const client = new Anthropic({
return {
apiKey,
baseURL: model.baseUrl,
dangerouslyAllowBrowser: true,
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(