feat: allow injecting pre-built Anthropic client

Add optional `client` field to AnthropicOptions so callers can pass
alternative SDK clients (e.g. AnthropicVertex) that
share the same messages.stream() API. When omitted, behavior is unchanged.

Signed-off-by: sallyom <somalley@redhat.com>
This commit is contained in:
sallyom
2026-03-09 15:27:28 -04:00
Unverified
parent 4535415300
commit 0acb65fe1c
2 changed files with 88 additions and 18 deletions
+34 -18
View File
@@ -178,6 +178,12 @@ export interface AnthropicOptions extends StreamOptions {
effort?: AnthropicEffort;
interleavedThinking?: boolean;
toolChoice?: "auto" | "any" | "none" | { type: "tool"; name: string };
/**
* Pre-built Anthropic client instance. When provided, skips internal client
* construction entirely. Use this to inject alternative SDK clients such as
* `AnthropicVertex` that shares the same messaging API.
*/
client?: Anthropic;
}
function mergeHeaders(...headerSources: (Record<string, string> | undefined)[]): Record<string, string> {
@@ -217,25 +223,35 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOpti
};
try {
const apiKey = options?.apiKey ?? getEnvApiKey(model.provider) ?? "";
let client: Anthropic;
let isOAuth: boolean;
let copilotDynamicHeaders: Record<string, string> | undefined;
if (model.provider === "github-copilot") {
const hasImages = hasCopilotVisionInput(context.messages);
copilotDynamicHeaders = buildCopilotDynamicHeaders({
messages: context.messages,
hasImages,
});
if (options?.client) {
client = options.client;
isOAuth = false;
} else {
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 created = createClient(
model,
apiKey,
options?.interleavedThinking ?? true,
options?.headers,
copilotDynamicHeaders,
);
client = created.client;
isOAuth = created.isOAuthToken;
}
const { client, isOAuthToken } = createClient(
model,
apiKey,
options?.interleavedThinking ?? true,
options?.headers,
copilotDynamicHeaders,
);
let params = buildParams(model, context, isOAuthToken, options);
let params = buildParams(model, context, isOAuth, options);
const nextParams = await options?.onPayload?.(params, model);
if (nextParams !== undefined) {
params = nextParams as MessageCreateParamsStreaming;
@@ -290,7 +306,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOpti
const block: Block = {
type: "toolCall",
id: event.content_block.id,
name: isOAuthToken
name: isOAuth
? fromClaudeCodeName(event.content_block.name, context.tools)
: event.content_block.name,
arguments: (event.content_block.input as Record<string, any>) ?? {},
@@ -0,0 +1,54 @@
import { describe, expect, it } from "vitest";
import { getModel } from "../src/models.js";
import type { Context } from "../src/types.js";
describe("Anthropic client injection", () => {
const model = getModel("anthropic", "claude-sonnet-4-6");
const context: Context = {
systemPrompt: "Test",
messages: [{ role: "user", content: "Hello", timestamp: Date.now() }],
};
it("should use injected client and set isOAuth to false", async () => {
const { streamAnthropic } = await import("../src/providers/anthropic.js");
let capturedPayload: any = null;
const messagesStreamCalled = { value: false };
// Minimal mock: just enough to verify the injected client is used
const mockClient = {
messages: {
stream: () => {
messagesStreamCalled.value = true;
// Return async iterable that yields a stop event
const events = [
{ type: "message_start", message: { usage: { input_tokens: 0, output_tokens: 0 } } },
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: {} },
];
return {
[Symbol.asyncIterator]: async function* () {
yield* events;
},
};
},
},
};
const s = streamAnthropic(model, context, {
client: mockClient as any,
onPayload: (payload) => {
capturedPayload = payload;
},
});
for await (const _event of s) {
// consume
}
expect(messagesStreamCalled.value).toBe(true);
expect(capturedPayload).not.toBeNull();
// isOAuth=false means no Claude Code system prompt identity is prepended
expect(capturedPayload.system).toHaveLength(1);
expect(capturedPayload.system[0].text).toBe("Test");
});
});