Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ LLM_OPENAI_API_KEY=sk-your_openai_key_here
# Anthropic
LLM_ANTHROPIC_API_KEY=sk-ant-your_anthropic_key_here

# Anthropic Discount (Anthropic-compatible endpoint via custom base URL + API key)
LLM_ANTHROPIC_DISCOUNT_BASE_URL=https://your-anthropic-discount-endpoint
LLM_ANTHROPIC_DISCOUNT_API_KEY=your_anthropic_discount_key_here

# Google AI Studio
LLM_GOOGLE_AI_STUDIO_API_KEY=your_google_ai_studio_key_here

Expand Down
36 changes: 30 additions & 6 deletions apps/gateway/src/chat/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4585,7 +4585,11 @@ chat.openapi(completions, async (c) => {
}

// Anthropic does not allow temperature and top_p to be set simultaneously
if (usedProvider === "anthropic" || usedProvider === "vertex-anthropic") {
if (
usedProvider === "anthropic" ||
usedProvider === "anthropic-discount" ||
usedProvider === "vertex-anthropic"
) {
if (temperature !== undefined && top_p !== undefined) {
top_p = undefined;
}
Expand Down Expand Up @@ -5270,7 +5274,11 @@ chat.openapi(completions, async (c) => {
headers["Content-Type"] = "application/json";

// Add effort beta header for Anthropic if effort parameter is specified
if (usedProvider === "anthropic" && effort !== undefined) {
if (
(usedProvider === "anthropic" ||
usedProvider === "anthropic-discount") &&
effort !== undefined
) {
const currentBeta = headers["anthropic-beta"];
headers["anthropic-beta"] = currentBeta
? `${currentBeta},effort-2025-11-24`
Expand All @@ -5279,7 +5287,8 @@ chat.openapi(completions, async (c) => {

// Add structured outputs beta header for Anthropic if json_schema response_format is specified
if (
usedProvider === "anthropic" &&
(usedProvider === "anthropic" ||
usedProvider === "anthropic-discount") &&
response_format?.type === "json_schema"
) {
const currentBeta = headers["anthropic-beta"];
Expand Down Expand Up @@ -6556,6 +6565,7 @@ chat.openapi(completions, async (c) => {
streamingIsJsonResponseFormat &&
(streamingResponseHealingEnabled === true ||
((usedProvider === "anthropic" ||
usedProvider === "anthropic-discount" ||
usedProvider === "vertex-anthropic") &&
response_format?.type === "json_object") ||
(usedProvider === "aws-bedrock" &&
Expand Down Expand Up @@ -7374,6 +7384,7 @@ chat.openapi(completions, async (c) => {
// For Anthropic, if we have partial usage data, complete it
if (
(usedProvider === "anthropic" ||
usedProvider === "anthropic-discount" ||
usedProvider === "vertex-anthropic") &&
transformedData.usage
) {
Expand Down Expand Up @@ -7448,6 +7459,7 @@ chat.openapi(completions, async (c) => {
// from the initial content_block_start event. This ensures OpenAI SDK compatibility.
if (
usedProvider === "anthropic" ||
usedProvider === "anthropic-discount" ||
usedProvider === "vertex-anthropic"
) {
const toolCalls =
Expand Down Expand Up @@ -7577,6 +7589,7 @@ chat.openapi(completions, async (c) => {
const contentChunk = extractContent(
isGoogleCompatibleProvider(usedProvider) ||
usedProvider === "anthropic" ||
usedProvider === "anthropic-discount" ||
usedProvider === "vertex-anthropic"
? data
: transformedData,
Expand Down Expand Up @@ -7610,6 +7623,7 @@ chat.openapi(completions, async (c) => {
// Check for web search results based on provider-specific data
if (
usedProvider === "anthropic" ||
usedProvider === "anthropic-discount" ||
usedProvider === "vertex-anthropic"
) {
// For Anthropic, count web_search_tool_result blocks
Expand Down Expand Up @@ -7653,6 +7667,7 @@ chat.openapi(completions, async (c) => {
const reasoningContentChunk = extractReasoning(
isGoogleCompatibleProvider(usedProvider) ||
usedProvider === "anthropic" ||
usedProvider === "anthropic-discount" ||
usedProvider === "vertex-anthropic"
? data
: transformedData,
Expand Down Expand Up @@ -7682,6 +7697,7 @@ chat.openapi(completions, async (c) => {
// For Anthropic content_block_delta events, match by content block index
if (
(usedProvider === "anthropic" ||
usedProvider === "anthropic-discount" ||
usedProvider === "vertex-anthropic") &&
newCall._contentBlockIndex !== undefined
) {
Expand Down Expand Up @@ -7727,6 +7743,7 @@ chat.openapi(completions, async (c) => {
}
break;
case "anthropic":
case "anthropic-discount":
case "vertex-anthropic":
if (
data.type === "message_delta" &&
Expand Down Expand Up @@ -8975,7 +8992,11 @@ chat.openapi(completions, async (c) => {
}

// Add effort beta header for Anthropic if effort parameter is specified
if (usedProvider === "anthropic" && effort !== undefined) {
if (
(usedProvider === "anthropic" ||
usedProvider === "anthropic-discount") &&
effort !== undefined
) {
const currentBeta = headers["anthropic-beta"];
headers["anthropic-beta"] = currentBeta
? `${currentBeta},effort-2025-11-24`
Expand All @@ -8984,7 +9005,8 @@ chat.openapi(completions, async (c) => {

// Add structured outputs beta header for Anthropic if json_schema response_format is specified
if (
usedProvider === "anthropic" &&
(usedProvider === "anthropic" ||
usedProvider === "anthropic-discount") &&
response_format?.type === "json_schema"
) {
const currentBeta = headers["anthropic-beta"];
Expand Down Expand Up @@ -10337,7 +10359,9 @@ chat.openapi(completions, async (c) => {
const shouldHealNonStreaming =
isJsonResponseFormat &&
(responseHealingEnabled === true ||
((usedProvider === "anthropic" || usedProvider === "vertex-anthropic") &&
((usedProvider === "anthropic" ||
usedProvider === "anthropic-discount" ||
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 "anthropic-discount":
case "vertex-anthropic":
if (data.type === "content_block_delta" && data.delta?.text) {
return data.delta.text;
Expand Down
1 change: 1 addition & 0 deletions apps/gateway/src/chat/tools/extract-reasoning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export function extractReasoning(
): string {
switch (provider) {
case "anthropic":
case "anthropic-discount":
case "vertex-anthropic": {
const chunk = data as AnthropicStreamChunk;
if (
Expand Down
1 change: 1 addition & 0 deletions apps/gateway/src/chat/tools/extract-token-usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ export function extractTokenUsage(
}
break;
case "anthropic":
case "anthropic-discount":
{
const usage = data.message?.usage ?? data.usage;
if (!usage) {
Expand Down
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 "anthropic-discount":
case "vertex-anthropic":
// Anthropic streaming tool calls come as content_block_start with tool_use type
if (
Expand Down
1 change: 1 addition & 0 deletions apps/gateway/src/chat/tools/map-finish-reason-to-openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export function mapFinishReasonToOpenai(
return "stop";
}
case "anthropic":
case "anthropic-discount":
case "vertex-anthropic":
if (!finishReason) {
return hasToolCalls ? "tool_calls" : "stop";
Expand Down
1 change: 1 addition & 0 deletions apps/gateway/src/chat/tools/parse-provider-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export function parseProviderResponse(
break;
}
case "anthropic":
case "anthropic-discount":
case "vertex-anthropic": {
// Extract content and reasoning content from Anthropic response
const contentBlocks = json.content ?? [];
Expand Down
13 changes: 10 additions & 3 deletions apps/gateway/src/chat/tools/resolve-provider-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,11 @@ export async function resolveProviderContext(
}

// Anthropic does not allow temperature and top_p simultaneously
if (usedProvider === "anthropic" || usedProvider === "vertex-anthropic") {
if (
usedProvider === "anthropic" ||
usedProvider === "anthropic-discount" ||
usedProvider === "vertex-anthropic"
) {
if (temperature !== undefined && top_p !== undefined) {
top_p = undefined;
}
Expand Down Expand Up @@ -516,15 +520,18 @@ export async function resolveProviderContext(
});
headers["Content-Type"] = "application/json";

if (usedProvider === "anthropic" && options.effort !== undefined) {
if (
(usedProvider === "anthropic" || usedProvider === "anthropic-discount") &&
options.effort !== undefined
) {
const currentBeta = headers["anthropic-beta"];
headers["anthropic-beta"] = currentBeta
? `${currentBeta},effort-2025-11-24`
: "effort-2025-11-24";
}

if (
usedProvider === "anthropic" &&
(usedProvider === "anthropic" || usedProvider === "anthropic-discount") &&
options.response_format?.type === "json_schema"
) {
const currentBeta = headers["anthropic-beta"];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ export function transformResponseToOpenai(
break;
}
case "anthropic":
case "anthropic-discount":
case "vertex-anthropic": {
transformedResponse = {
id: `chatcmpl-${Date.now()}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export function transformStreamingToOpenai(

switch (usedProvider) {
case "anthropic":
case "anthropic-discount":
case "vertex-anthropic": {
const usage = data.message?.usage ?? data.usage;
if (data.type === "message_start") {
Expand Down
1 change: 1 addition & 0 deletions apps/gateway/src/lib/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export function getUnifiedFinishReason(

switch (provider) {
case "anthropic":
case "anthropic-discount":
case "vertex-anthropic":
if (finishReason === "stop_sequence") {
return UnifiedFinishReason.COMPLETED;
Expand Down
11 changes: 11 additions & 0 deletions packages/actions/src/get-provider-endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@ export function getProviderEndpoint(
case "anthropic":
url = "https://api.anthropic.com";
break;
case "anthropic-discount":
url = skipEnvVars
? undefined
: getProviderEnvValue("anthropic-discount", "baseUrl", configIndex);
if (!url) {
throw new Error(
"Anthropic Discount provider requires LLM_ANTHROPIC_DISCOUNT_BASE_URL environment variable",
);
}
break;
case "google-ai-studio":
url =
envValueOrDefault(
Expand Down Expand Up @@ -337,6 +347,7 @@ export function getProviderEndpoint(

switch (provider) {
case "anthropic":
case "anthropic-discount":
return `${url}/v1/messages`;
case "google-ai-studio": {
const endpoint = stream ? "streamGenerateContent" : "generateContent";
Expand Down
3 changes: 2 additions & 1 deletion packages/actions/src/get-provider-headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ export function getProviderHeaders(
}

switch (provider) {
case "anthropic": {
case "anthropic":
case "anthropic-discount": {
const betaFeatures = ["tools-2024-04-04", "prompt-caching-2024-07-31"];
if (options?.webSearchEnabled) {
betaFeatures.push("web-search-2025-03-05");
Expand Down
2 changes: 2 additions & 0 deletions packages/actions/src/prepare-request-body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1040,6 +1040,7 @@ export async function prepareRequestBody(
// Anthropic-style `ttl: "1h"` must be normalized away before forwarding.
const providerHandlesCacheControl =
usedProvider === "anthropic" ||
usedProvider === "anthropic-discount" ||
usedProvider === "vertex-anthropic" ||
usedProvider === "aws-bedrock" ||
usedProvider === "alibaba";
Expand Down Expand Up @@ -1506,6 +1507,7 @@ export async function prepareRequestBody(
break;
}
case "anthropic":
case "anthropic-discount":
case "vertex-anthropic": {
// Remove generic tool_choice that was added earlier
delete requestBody.tool_choice;
Expand Down
3 changes: 2 additions & 1 deletion packages/actions/src/transform-anthropic-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ export async function transformAnthropicMessages(
// Apply for anthropic provider only, and only when the project hasn't
// opted out of auto-injection.
const shouldApplyCacheControl =
provider === "anthropic" && providerCacheControlEnabled;
(provider === "anthropic" || provider === "anthropic-discount") &&
providerCacheControlEnabled;

// Track cache_control usage to limit to maximum of 4 blocks total (including system messages)
let cacheControlCount = initialCacheControlCount;
Expand Down
48 changes: 48 additions & 0 deletions packages/models/src/models/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,30 @@ export const anthropicModels = [
webSearch: true,
webSearchPrice: "0.01", // $10 per 1000 searches
},
{
providerId: "anthropic-discount",
externalId: "claude-opus-4-7",
inputPrice: "5.0e-6",
outputPrice: "25.0e-6",
cachedInputPrice: "0.5e-6",
cacheWriteInputPrice: "6.25e-6",
cacheWriteInputPrice1h: "10.0e-6",
discount: "0.1",
minCacheableTokens: 4096,
requestPrice: "0",
contextSize: 1000000,
maxOutput: 128000,
reasoning: true,
reasoningMode: "adaptive",
reasoningOutput: "omit",
streaming: true,
vision: true,
tools: true,
jsonOutputSchema: true,
supportedParameters: ["temperature", "max_tokens", "top_p", "effort"],
webSearch: true,
webSearchPrice: "0.01",
},
{
providerId: "aws-bedrock",
externalId: "anthropic.claude-opus-4-7",
Expand Down Expand Up @@ -1130,6 +1154,30 @@ export const anthropicModels = [
webSearch: true,
webSearchPrice: "0.01",
},
{
providerId: "anthropic-discount",
externalId: "claude-opus-4-8",
inputPrice: "5.0e-6",
outputPrice: "25.0e-6",
cachedInputPrice: "0.5e-6",
cacheWriteInputPrice: "6.25e-6",
cacheWriteInputPrice1h: "10.0e-6",
discount: "0.1",
minCacheableTokens: 4096,
requestPrice: "0",
contextSize: 1000000,
maxOutput: 128000,
reasoning: true,
reasoningMode: "adaptive",
reasoningOutput: "omit",
streaming: true,
vision: true,
tools: true,
jsonOutputSchema: true,
supportedParameters: ["temperature", "max_tokens", "top_p", "effort"],
webSearch: true,
webSearchPrice: "0.01",
},
],
},
] as const satisfies ModelDefinition[];
Loading
Loading