Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c765eb1
feat: add vertex-anthropic provider
analogpvt May 7, 2026
a15874f
fix: address CodeRabbit review feedback
analogpvt May 7, 2026
78c3d04
fix: vertex-anthropic response transform and schema
analogpvt May 7, 2026
408f54c
fix: vertex auth uses Bearer for non-lite models
analogpvt May 7, 2026
13c0d22
chore: merge main and resolve conflicts
analogpvt May 7, 2026
ac04397
feat: auto-refresh GCP OAuth2 tokens from service account
analogpvt May 7, 2026
27edcb3
Merge remote-tracking branch 'origin/main' into fix/vertex-anthropic-…
analogpvt May 7, 2026
7c2e3b6
docs: add Vertex AI Anthropic setup guide
analogpvt May 7, 2026
8a6adec
docs: simplify vertex-anthropic .env.example
analogpvt May 7, 2026
bbcf503
Merge remote-tracking branch 'origin/main' into fix/vertex-anthropic-…
analogpvt May 7, 2026
1b18cfd
feat: inline SA JSON for vertex-anthropic config
analogpvt May 7, 2026
42934ec
docs: improve vertex-anthropic setup guide
analogpvt May 7, 2026
95fd269
Merge remote-tracking branch 'origin/main' into fix/vertex-anthropic-…
analogpvt May 7, 2026
294d7a3
chore: sync branch with base
analogpvt May 7, 2026
4ac56ec
Merge remote-tracking branch 'origin/main' into fix/vertex-anthropic-…
analogpvt May 7, 2026
426c835
chore: merge main and resolve conflicts
analogpvt May 14, 2026
345456f
fix: use string price format for vertex-anthropic
analogpvt May 14, 2026
7121ddd
fix: sync pnpm-lock.yaml with main
analogpvt May 14, 2026
d0cb12f
fix: update vision test for vertex-anthropic entry
analogpvt May 14, 2026
6449e0e
fix: restore key query param for google-vertex
analogpvt May 14, 2026
585d188
Merge remote-tracking branch 'origin/main' into fix/vertex-anthropic-…
analogpvt May 14, 2026
cf9b730
Delete docs/superpowers/plans/2026-05-07-vertex-anthropic-inline-sa-j…
steebchen May 17, 2026
e727d1b
docs: move vertex-anthropic to integrations; ci: add env vars
steebchen May 17, 2026
381f830
Merge remote-tracking branch 'origin/main' into feat/vertex-anthropic…
steebchen May 17, 2026
7df770f
fix(models): add terms/privacy links to vertex-anthropic
steebchen May 17, 2026
43e6f87
fix(vertex-anthropic): default region to global
steebchen May 17, 2026
1c1919a
fix(vertex-anthropic): use plain host for global endpoint
steebchen May 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 30 additions & 10 deletions apps/gateway/src/chat/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,12 @@ function usesGoogleQueryToken(provider: string): boolean {
}

function isGoogleCompatibleProvider(provider: string): boolean {
return usesGoogleQueryToken(provider);
return (
provider === "google-ai-studio" ||
provider === "glacier" ||
provider === "google-vertex" ||
provider === "quartz"
);
}

// Pre-compiled regex pattern to avoid recompilation per request
Expand Down Expand Up @@ -3916,7 +3921,7 @@ chat.openapi(completions, async (c) => {
}

// Anthropic does not allow temperature and top_p to be set simultaneously
if (usedProvider === "anthropic") {
if (usedProvider === "anthropic" || usedProvider === "vertex-anthropic") {
if (temperature !== undefined && top_p !== undefined) {
top_p = undefined;
}
Expand Down Expand Up @@ -5707,7 +5712,8 @@ chat.openapi(completions, async (c) => {
const shouldBufferForHealing =
streamingIsJsonResponseFormat &&
(streamingResponseHealingEnabled === true ||
(usedProvider === "anthropic" &&
((usedProvider === "anthropic" ||
usedProvider === "vertex-anthropic") &&
response_format?.type === "json_object") ||
(usedProvider === "aws-bedrock" &&
response_format?.type === "json_object") ||
Expand Down Expand Up @@ -6489,7 +6495,11 @@ chat.openapi(completions, async (c) => {
}

// For Anthropic, if we have partial usage data, complete it
if (usedProvider === "anthropic" && transformedData.usage) {
if (
(usedProvider === "anthropic" ||
usedProvider === "vertex-anthropic") &&
transformedData.usage
) {
const usage = transformedData.usage;
if (
usage.output_tokens !== undefined &&
Expand Down Expand Up @@ -6559,7 +6569,10 @@ chat.openapi(completions, async (c) => {

// For Anthropic streaming tool calls, enrich delta chunks with id/type/name
// from the initial content_block_start event. This ensures OpenAI SDK compatibility.
if (usedProvider === "anthropic") {
if (
usedProvider === "anthropic" ||
usedProvider === "vertex-anthropic"
) {
const toolCalls =
transformedData.choices?.[0]?.delta?.tool_calls;
if (toolCalls && toolCalls.length > 0) {
Expand Down Expand Up @@ -6686,7 +6699,8 @@ chat.openapi(completions, async (c) => {
// use raw data. For others (like aws-bedrock), use transformed OpenAI format.
const contentChunk = extractContent(
isGoogleCompatibleProvider(usedProvider) ||
usedProvider === "anthropic"
usedProvider === "anthropic" ||
usedProvider === "vertex-anthropic"
? data
: transformedData,
usedProvider,
Expand Down Expand Up @@ -6717,7 +6731,10 @@ chat.openapi(completions, async (c) => {

// Track web search calls for cost calculation
// Check for web search results based on provider-specific data
if (usedProvider === "anthropic") {
if (
usedProvider === "anthropic" ||
usedProvider === "vertex-anthropic"
) {
// For Anthropic, count web_search_tool_result blocks
if (
data.type === "content_block_start" &&
Expand Down Expand Up @@ -6758,7 +6775,8 @@ chat.openapi(completions, async (c) => {
// use raw data. For others, use transformed OpenAI format.
const reasoningContentChunk = extractReasoning(
isGoogleCompatibleProvider(usedProvider) ||
usedProvider === "anthropic"
usedProvider === "anthropic" ||
usedProvider === "vertex-anthropic"
? data
: transformedData,
usedProvider,
Expand Down Expand Up @@ -6786,7 +6804,8 @@ chat.openapi(completions, async (c) => {

// For Anthropic content_block_delta events, match by content block index
if (
usedProvider === "anthropic" &&
(usedProvider === "anthropic" ||
usedProvider === "vertex-anthropic") &&
newCall._contentBlockIndex !== undefined
) {
existingCall =
Expand Down Expand Up @@ -6831,6 +6850,7 @@ chat.openapi(completions, async (c) => {
}
break;
case "anthropic":
case "vertex-anthropic":
if (
data.type === "message_delta" &&
data.delta?.stop_reason
Expand Down Expand Up @@ -9226,7 +9246,7 @@ chat.openapi(completions, async (c) => {
const shouldHealNonStreaming =
isJsonResponseFormat &&
(responseHealingEnabled === true ||
(usedProvider === "anthropic" &&
((usedProvider === "anthropic" || usedProvider === "vertex-anthropic") &&
response_format?.type === "json_object") ||
(usedProvider === "aws-bedrock" &&
response_format?.type === "json_object") ||
Expand Down
1 change: 1 addition & 0 deletions apps/gateway/src/chat/tools/extract-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export function extractContent(data: any, provider: Provider): string {
return contentParts.map((part: any) => part.text).join("") ?? "";
}
case "anthropic":
case "vertex-anthropic":
if (data.type === "content_block_delta" && data.delta?.text) {
return data.delta.text;
} else if (data.delta?.text) {
Expand Down
65 changes: 47 additions & 18 deletions apps/gateway/src/chat/tools/extract-reasoning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,68 @@ import { extractReasoningDetailsText } from "./reasoning-details.js";

import type { Provider } from "@llmgateway/models";

/**
* Extracts reasoning content from streaming data based on provider format
*/
export function extractReasoning(data: any, provider: Provider): string {
interface AnthropicStreamChunk {
type?: string;
delta?: { type?: string; thinking?: string };
}

interface GoogleStreamChunk {
candidates?: Array<{
content?: { parts?: Array<{ thought?: boolean; text?: string }> };
}>;
}

interface OpenAIStreamChunk {
choices?: Array<{
delta?: {
reasoning?: string;
reasoning_content?: string;
reasoning_details?: unknown;
};
}>;
}

type StreamingChunk =
| AnthropicStreamChunk
| GoogleStreamChunk
| OpenAIStreamChunk;

export function extractReasoning(
data: StreamingChunk,
provider: Provider,
): string {
switch (provider) {
case "anthropic": {
// Handle Anthropic thinking content blocks in streaming format
case "anthropic":
case "vertex-anthropic": {
const chunk = data as AnthropicStreamChunk;
if (
data.type === "content_block_delta" &&
data.delta?.type === "thinking_delta" &&
data.delta?.thinking
chunk.type === "content_block_delta" &&
chunk.delta?.type === "thinking_delta" &&
chunk.delta?.thinking
) {
// This is a thinking delta - return the thinking content
return data.delta.thinking;
return chunk.delta.thinking;
}
return "";
}
case "google-ai-studio":
case "glacier":
case "google-vertex":
case "quartz": {
const parts = data.candidates?.[0]?.content?.parts ?? [];
const reasoningParts = parts.filter((part: any) => part.thought);
return reasoningParts.map((part: any) => part.text).join("") ?? "";
const chunk = data as GoogleStreamChunk;
const parts = chunk.candidates?.[0]?.content?.parts ?? [];
const reasoningParts = parts.filter((part) => part.thought);
return reasoningParts.map((part) => part.text).join("") ?? "";
}
default: // OpenAI format (includes GLM/ZAI which use reasoning_content)
default: {
const chunk = data as OpenAIStreamChunk;
return (
data.choices?.[0]?.delta?.reasoning ??
data.choices?.[0]?.delta?.reasoning_content ??
chunk.choices?.[0]?.delta?.reasoning ??
chunk.choices?.[0]?.delta?.reasoning_content ??
extractReasoningDetailsText(
data.choices?.[0]?.delta?.reasoning_details,
chunk.choices?.[0]?.delta?.reasoning_details,
) ??
""
);
}
}
}
1 change: 1 addition & 0 deletions apps/gateway/src/chat/tools/extract-tool-calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export function extractToolCalls(
): any[] | null {
switch (provider) {
case "anthropic":
case "vertex-anthropic":
// Anthropic streaming tool calls come as content_block_start with tool_use type
if (
data.type === "content_block_start" &&
Expand Down
3 changes: 2 additions & 1 deletion apps/gateway/src/chat/tools/parse-provider-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ export function parseProviderResponse(

break;
}
case "anthropic": {
case "anthropic":
case "vertex-anthropic": {
// Extract content and reasoning content from Anthropic response
const contentBlocks = json.content ?? [];
const textBlocks = contentBlocks.filter(
Expand Down
5 changes: 3 additions & 2 deletions apps/gateway/src/chat/tools/resolve-provider-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,8 @@ export async function resolveProviderContext(
usedProvider === "google-ai-studio" ||
usedProvider === "glacier" ||
usedProvider === "google-vertex" ||
usedProvider === "quartz"
usedProvider === "quartz" ||
usedProvider === "vertex-anthropic"
? usedToken
: undefined,
options.stream,
Expand Down Expand Up @@ -390,7 +391,7 @@ export async function resolveProviderContext(
}

// Anthropic does not allow temperature and top_p simultaneously
if (usedProvider === "anthropic") {
if (usedProvider === "anthropic" || usedProvider === "vertex-anthropic") {
if (temperature !== undefined && top_p !== undefined) {
top_p = undefined;
}
Expand Down
3 changes: 2 additions & 1 deletion apps/gateway/src/chat/tools/transform-streaming-to-openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ export function transformStreamingToOpenai(
};

switch (usedProvider) {
case "anthropic": {
case "anthropic":
case "vertex-anthropic": {
if (data.type === "content_block_delta" && data.delta?.text) {
transformedData = {
id: data.id ?? `chatcmpl-${Date.now()}`,
Expand Down
42 changes: 42 additions & 0 deletions packages/actions/src/get-provider-endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,24 @@ export function getProviderEndpoint(
"https://aiplatform.googleapis.com",
) ?? "https://aiplatform.googleapis.com";
break;
case "vertex-anthropic": {
const vaDefaultRegion =
providerKeyOptions?.vertex_anthropic_region ??
getProviderEnvValue(
"vertex-anthropic",
"region",
configIndex,
"us-east5",
) ??
"us-east5";
url =
envValueOrDefault(
"vertex-anthropic",
"baseUrl",
`https://${vaDefaultRegion}-aiplatform.googleapis.com`,
) ?? `https://${vaDefaultRegion}-aiplatform.googleapis.com`;
break;
}
case "quartz":
url = skipEnvVars
? undefined
Expand Down Expand Up @@ -338,6 +356,30 @@ export function getProviderEndpoint(
configIndex,
providerKeyOptions,
);
case "vertex-anthropic": {
const vaProjectId =
providerKeyOptions?.vertex_anthropic_project_id ??
getProviderEnvValue("vertex-anthropic", "project", configIndex);
const vaRegion =
providerKeyOptions?.vertex_anthropic_region ??
getProviderEnvValue(
"vertex-anthropic",
"region",
configIndex,
"us-east5",
) ??
"us-east5";

if (!vaProjectId) {
throw new Error(
"LLM_VERTEX_ANTHROPIC_PROJECT environment variable is required for vertex-anthropic provider",
);
}

const vaModel = modelName ?? "claude-sonnet-4-6";
const vaEndpoint = stream ? "streamRawPredict" : "rawPredict";
return `${url}/v1/projects/${vaProjectId}/locations/${vaRegion}/publishers/anthropic/models/${vaModel}:${vaEndpoint}`;
}
Comment thread
analogpvt marked this conversation as resolved.
Resolved
case "perplexity":
return `${url}/chat/completions`;
case "novita":
Expand Down
5 changes: 5 additions & 0 deletions packages/actions/src/get-provider-headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export function getProviderHeaders(
case "google-vertex":
case "quartz":
return requestIdHeader;
Comment thread
analogpvt marked this conversation as resolved.
Resolved
case "vertex-anthropic":
return {
...requestIdHeader,
Authorization: `Bearer ${token}`,
};
case "avalanche":
return {
...requestIdHeader,
Expand Down
12 changes: 10 additions & 2 deletions packages/actions/src/prepare-request-body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -928,7 +928,9 @@ export async function prepareRequestBody(
// otherwise pass an unknown field through to OpenAI/Google/etc., risking a
// 400 from strict providers and confusing logs from lenient ones.
const providerHandlesCacheControl =
usedProvider === "anthropic" || usedProvider === "aws-bedrock";
usedProvider === "anthropic" ||
usedProvider === "vertex-anthropic" ||
usedProvider === "aws-bedrock";
if (!providerHandlesCacheControl) {
processedMessages = processedMessages.map((m) => {
if (!Array.isArray(m.content)) {
Expand Down Expand Up @@ -1322,7 +1324,8 @@ export async function prepareRequestBody(
}
break;
}
case "anthropic": {
case "anthropic":
case "vertex-anthropic": {
// Remove generic tool_choice that was added earlier
delete requestBody.tool_choice;

Expand Down Expand Up @@ -1611,6 +1614,11 @@ export async function prepareRequestBody(
// Note: The model capability (jsonOutput) should ensure the prompt guides JSON output.
}
}

if (usedProvider === "vertex-anthropic") {
requestBody.anthropic_version = "vertex-2023-10-16";
delete requestBody.model;
}
break;
}
case "aws-bedrock": {
Expand Down
2 changes: 2 additions & 0 deletions packages/db/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,8 @@ export interface ProviderKeyOptions {
azure_ai_foundry_api_version?: string;
alibaba_region?: "singapore" | "us-virginia" | "cn-beijing";
google_vertex_project_id?: string;
vertex_anthropic_project_id?: string;
vertex_anthropic_region?: string;
}

export const providerKey = pgTable(
Expand Down
Loading
Loading