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
32 changes: 10 additions & 22 deletions src/app/api/openrouter/[...path]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
isFreeModel,
isDataCollectionRequiredOnKiloCodeOnly,
isDeadFreeModel,
isSlackbotOnlyModel,
isRateLimitedModel,
} from '@/lib/models';
import {
Expand Down Expand Up @@ -68,29 +67,26 @@ import { isActiveReviewPromo } from '@/lib/code-reviews/core/constants';

const MAX_TOKENS_LIMIT = 99999999999; // GPT4.1 default is ~32k

const OPUS = CLAUDE_OPUS_CURRENT_MODEL_ID;
const SONNET = CLAUDE_SONNET_CURRENT_MODEL_ID;

const PAID_MODEL_AUTH_REQUIRED = 'PAID_MODEL_AUTH_REQUIRED';
const PROMOTION_MODEL_LIMIT_REACHED = 'PROMOTION_MODEL_LIMIT_REACHED';

// Mode → model mappings for kilo/auto routing.
// Add/remove/modify entries here to change routing behavior.
const MODE_TO_MODEL = new Map<string, string>([
// Opus modes (planning, reasoning, orchestration, debugging)
['plan', OPUS],
['general', OPUS],
['architect', OPUS],
['orchestrator', OPUS],
['ask', OPUS],
['debug', OPUS],
['plan', CLAUDE_OPUS_CURRENT_MODEL_ID],
['general', CLAUDE_OPUS_CURRENT_MODEL_ID],
['architect', CLAUDE_OPUS_CURRENT_MODEL_ID],
['orchestrator', CLAUDE_OPUS_CURRENT_MODEL_ID],
['ask', CLAUDE_OPUS_CURRENT_MODEL_ID],
['debug', CLAUDE_OPUS_CURRENT_MODEL_ID],
// Sonnet modes (implementation, exploration)
['build', SONNET],
['explore', SONNET],
['code', SONNET],
['build', CLAUDE_SONNET_CURRENT_MODEL_ID],
['explore', CLAUDE_SONNET_CURRENT_MODEL_ID],
['code', CLAUDE_SONNET_CURRENT_MODEL_ID],
]);

const DEFAULT_AUTO_MODEL = SONNET;
const DEFAULT_AUTO_MODEL = CLAUDE_SONNET_CURRENT_MODEL_ID;

function resolveAutoModel(modeHeader: string | null) {
const mode = modeHeader?.trim().toLowerCase() ?? 'build';
Expand Down Expand Up @@ -186,14 +182,12 @@ export async function POST(request: NextRequest): Promise<NextResponseType<unkno
user: maybeUser,
authFailedResponse,
organizationId: authOrganizationId,
internalApiUse: authInternalApiUse,
botId: authBotId,
} = await getUserFromAuth({ adminOnly: false });
authSpan.end();

let user: typeof maybeUser | AnonymousUserContext;
let organizationId: string | undefined = authOrganizationId;
let internalApiUse: boolean | undefined = authInternalApiUse;
let botId: string | undefined = authBotId;

if (authFailedResponse) {
Expand Down Expand Up @@ -237,7 +231,6 @@ export async function POST(request: NextRequest): Promise<NextResponseType<unkno
// Anonymous access for free model (already rate-limited above)
user = createAnonymousContext(ipAddress);
organizationId = undefined;
internalApiUse = false;
botId = undefined;
} else {
user = maybeUser;
Expand Down Expand Up @@ -291,11 +284,6 @@ export async function POST(request: NextRequest): Promise<NextResponseType<unkno
return modelDoesNotExistResponse();
}

// Slackbot-only models are only available through Kilo for Slack (internalApiUse)
if (isSlackbotOnlyModel(originalModelIdLowerCased) && !internalApiUse) {
return modelDoesNotExistResponse();
}

// Extract properties for usage context
const tokenEstimates = estimateChatTokens(requestBodyParsed);
const promptInfo = extractPromptInfo(requestBodyParsed);
Expand Down
8 changes: 0 additions & 8 deletions src/components/cloud-agent-next/hooks/useResumeConfigModal.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
import { isSlackbotOnlyModel } from '@/lib/models';
import { type DbSessionDetails, type IndexedDbSessionData } from '../store/db-session-atoms';
import { extractRepoFromGitUrl } from '../utils/git-utils';
import type { ResumeConfig, StreamResumeConfig } from '../types';
Expand Down Expand Up @@ -65,13 +64,6 @@ export function needsResumeConfigModal(params: {

if (!loadedDbSession) return false;

if (
loadedDbSession.last_model &&
isSlackbotOnlyModel(loadedDbSession.last_model) &&
!currentIndexedDbSession?.resumeConfig
)
return true;

const isCliSession = !loadedDbSession.cloud_agent_session_id;

// Sessions from cli_sessions_v2 table store mode/model in the cloud-agent DO,
Expand Down
8 changes: 0 additions & 8 deletions src/components/cloud-agent/hooks/useResumeConfigModal.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
import { isSlackbotOnlyModel } from '@/lib/models';
import { type DbSessionDetails, type IndexedDbSessionData } from '../store/db-session-atoms';
import { extractRepoFromGitUrl } from '../utils/git-utils';
import type { ResumeConfig, StreamResumeConfig } from '../types';
Expand Down Expand Up @@ -65,13 +64,6 @@ export function needsResumeConfigModal(params: {

if (!loadedDbSession) return false;

if (
loadedDbSession.last_model &&
isSlackbotOnlyModel(loadedDbSession.last_model) &&
!currentIndexedDbSession?.resumeConfig
)
return true;

const isCliSession = !loadedDbSession.cloud_agent_session_id;
const isLegacyWebSessionWithoutModel = Boolean(
loadedDbSession.cloud_agent_session_id && !loadedDbSession.last_model
Expand Down
9 changes: 5 additions & 4 deletions src/lib/integrations/slack-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@ import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET } from '@/lib/config.server';
import { APP_URL } from '@/lib/constants';
import { WebClient } from '@slack/web-api';
import type { OAuthV2Response } from '@slack/oauth';
import { opus_46_free_slackbot_model } from '@/lib/providers/anthropic';
import { getOrganizationById } from '@/lib/organizations/organizations';
import { getDefaultAllowedModel } from '@/lib/slack-bot/model-allow-list';
import { createProviderAwareModelAllowPredicate } from '@/lib/model-allow.server';
import { minimax_m25_free_model } from '@/lib/providers/minimax';
import { CLAUDE_OPUS_CURRENT_MODEL_ID } from '@/lib/providers/anthropic';

// Default model for Slack integrations - separate from the global platform default
const SLACK_DEFAULT_MODEL = opus_46_free_slackbot_model.is_enabled
? opus_46_free_slackbot_model.public_id
: 'minimax/minimax-m2.1';
const SLACK_DEFAULT_MODEL = minimax_m25_free_model.is_enabled
? minimax_m25_free_model.public_id
: CLAUDE_OPUS_CURRENT_MODEL_ID;

// Slack OAuth scopes for the integration
// These should be kept in sync with the scopes requested in the Slack app configuration
Expand Down
19 changes: 3 additions & 16 deletions src/lib/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@ import { KILO_AUTO_MODEL_ID } from '@/lib/kilo-auto-model';
import {
CLAUDE_OPUS_CURRENT_MODEL_ID,
CLAUDE_SONNET_CURRENT_MODEL_ID,
opus_46_free_slackbot_model,
} from '@/lib/providers/anthropic';
import { corethink_free_model } from '@/lib/providers/corethink';
import { giga_potato_model, giga_potato_thinking_model } from '@/lib/providers/gigapotato';
import type { KiloFreeModel } from '@/lib/providers/kilo-free-model';
import { minimax_m21_free_model, minimax_m25_free_model } from '@/lib/providers/minimax';
import { minimax_m25_free_model } from '@/lib/providers/minimax';
import { grok_code_fast_1_optimized_free_model } from '@/lib/providers/xai';
import { zai_glm47_free_model, zai_glm5_free_model } from '@/lib/providers/zai';
import { zai_glm5_free_model } from '@/lib/providers/zai';

export const DEFAULT_MODEL_CHOICES = [CLAUDE_SONNET_CURRENT_MODEL_ID, CLAUDE_OPUS_CURRENT_MODEL_ID];

Expand Down Expand Up @@ -51,7 +50,7 @@ export function isFreeModel(model: string): boolean {
}

export function isRateLimitedModel(model: string): boolean {
return kiloFreeModels.some(m => m.public_id === model && m.is_enabled && !m.slackbot_only);
return kiloFreeModels.some(m => m.public_id === model && m.is_enabled);
}

export function isDataCollectionRequiredOnKiloCodeOnly(model: string): boolean {
Expand All @@ -62,11 +61,8 @@ export const kiloFreeModels = [
corethink_free_model,
giga_potato_model,
giga_potato_thinking_model,
minimax_m21_free_model,
minimax_m25_free_model,
opus_46_free_slackbot_model,
grok_code_fast_1_optimized_free_model,
zai_glm47_free_model,
zai_glm5_free_model,
] as KiloFreeModel[];

Expand All @@ -87,12 +83,3 @@ export function extraRequiredProviders(model: string) {
export function isDeadFreeModel(model: string): boolean {
return !!kiloFreeModels.find(m => m.public_id === model && !m.is_enabled);
}

/**
* Check if a model is only available through Kilo for Slack (internalApiUse).
* These models are hidden from the public model list and return "model does not exist"
* when accessed outside of the Slack integration.
*/
export function isSlackbotOnlyModel(model: string): boolean {
return !!kiloFreeModels.find(m => m.public_id === model && m.slackbot_only);
}
6 changes: 3 additions & 3 deletions src/lib/providerHash.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ describe('generateProviderSpecificHash', () => {

it('should generate different hashes for different providers', () => {
const openRouterHash = generateProviderSpecificHash(testUserId, PROVIDERS.OPENROUTER);
const grokHash = generateProviderSpecificHash(testUserId, PROVIDERS.XAI);
const vercelHash = generateProviderSpecificHash(testUserId, PROVIDERS.VERCEL_AI_GATEWAY);

expect(openRouterHash).not.toBe(grokHash);
expect(openRouterHash).not.toBe(vercelHash);
});

it('should generate consistent hashes for the same provider and user', () => {
Expand All @@ -26,7 +26,7 @@ describe('generateProviderSpecificHash', () => {
});

it('should return a base64 encoded string', () => {
const hash = generateProviderSpecificHash(testUserId, PROVIDERS.XAI);
const hash = generateProviderSpecificHash(testUserId, PROVIDERS.VERCEL_AI_GATEWAY);

// Base64 pattern check
expect(hash).toMatch(/^[A-Za-z0-9+/]+=*$/);
Expand Down
80 changes: 1 addition & 79 deletions src/lib/providers/anthropic.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { KiloFreeModel } from '@/lib/providers/kilo-free-model';
import type { OpenRouterChatCompletionRequest } from '@/lib/providers/openrouter/types';
import { normalizeToolCallIds } from '@/lib/tool-calling';
import type OpenAI from 'openai';
Expand All @@ -7,26 +6,8 @@ export const CLAUDE_SONNET_CURRENT_MODEL_ID = 'anthropic/claude-sonnet-4.6';

export const CLAUDE_OPUS_CURRENT_MODEL_ID = 'anthropic/claude-opus-4.6';

export const opus_46_free_slackbot_model = {
public_id: 'anthropic/claude-opus-4.6:slackbot',
display_name: 'Anthropic: Claude Opus 4.6 (Free for Kilo for Slack)',
description: 'Free version of Claude Opus 4.6 for use in Kilo for Slack only',
context_length: 1_000_000,
max_completion_tokens: 32000,
is_enabled: false,
flags: ['reasoning', 'prompt_cache', 'vision'],
gateway: 'openrouter',
internal_id: 'anthropic/claude-opus-4.6',
inference_providers: [],
slackbot_only: true,
} as KiloFreeModel;

const ENABLE_ANTHROPIC_STRICT_TOOL_USE = false;

const ENABLE_ANTHROPIC_AUTOMATIC_CACHING = true;

const ENABLE_ANTHROPIC_FINE_GRAINED_TOOL_STREAMING = true;

export function isAnthropicModel(requestedModel: string) {
return requestedModel.startsWith('anthropic/');
}
Expand All @@ -35,62 +16,12 @@ export function isHaikuModel(requestedModel: string) {
return requestedModel.startsWith('anthropic/claude-haiku');
}

export function isOpusModel(requestedModel: string) {
return requestedModel.startsWith('anthropic/claude-opus');
}

type ReadFileParametersSchema = {
properties?: {
files?: {
items?: {
properties?: { line_ranges?: { items?: { minItems?: number; maxItems?: number } } };
};
};
};
};

function patchReadFileTool(func: OpenAI.FunctionDefinition) {
try {
const lineRangesItems = (func.parameters as ReadFileParametersSchema | undefined)?.properties
?.files?.items?.properties?.line_ranges?.items;
if (lineRangesItems) {
delete lineRangesItems.minItems;
delete lineRangesItems.maxItems;
}
func.strict = true;
return true;
} catch (e) {
console.error('[patchReadFileTool]', e);
return false;
}
}

function appendAnthropicBetaHeader(extraHeaders: Record<string, string>, betaFlag: string) {
extraHeaders['x-anthropic-beta'] = [extraHeaders['x-anthropic-beta'], betaFlag]
.filter(Boolean)
.join(',');
}

function applyAnthropicStrictToolUse(
requestToMutate: OpenRouterChatCompletionRequest,
extraHeaders: Record<string, string>
) {
let supportedToolFound = false;
for (const tool of requestToMutate.tools ?? []) {
if (tool.type === 'function') {
if (tool.function.name === 'read_file' && patchReadFileTool(tool.function)) {
supportedToolFound = true;
} else {
delete tool.function.strict;
}
}
}
if (supportedToolFound) {
console.debug('[applyAnthropicStrictToolUse] setting structured-outputs beta header');
appendAnthropicBetaHeader(extraHeaders, 'structured-outputs-2025-11-13');
}
}

function hasCacheControl(message: OpenAI.ChatCompletionMessageParam) {
return (
'cache_control' in message ||
Expand Down Expand Up @@ -163,16 +94,7 @@ export function applyAnthropicModelSettings(
requestToMutate: OpenRouterChatCompletionRequest,
extraHeaders: Record<string, string>
) {
if (ENABLE_ANTHROPIC_STRICT_TOOL_USE) {
applyAnthropicStrictToolUse(requestToMutate, extraHeaders);
}

if (ENABLE_ANTHROPIC_FINE_GRAINED_TOOL_STREAMING) {
console.debug(
'[applyAnthropicModelSettings] setting fine-grained-tool-streaming-2025-05-14 beta header'
);
appendAnthropicBetaHeader(extraHeaders, 'fine-grained-tool-streaming-2025-05-14');
}
appendAnthropicBetaHeader(extraHeaders, 'fine-grained-tool-streaming-2025-05-14');

if (ENABLE_ANTHROPIC_AUTOMATIC_CACHING) {
// kilo/auto doesn't get cache breakpoints, because clients don't know it's a Claude model
Expand Down
33 changes: 0 additions & 33 deletions src/lib/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
import { applyXaiModelSettings, isXaiModel } from '@/lib/providers/xai';
import { applyVercelSettings, shouldRouteToVercel } from '@/lib/providers/vercel';
import { kiloFreeModels } from '@/lib/models';
import { applyMinimaxProviderSettings } from '@/lib/providers/minimax';
import {
applyAnthropicModelSettings,
isAnthropicModel,
Expand Down Expand Up @@ -71,13 +70,6 @@ export const PROVIDERS = {
hasGenerationEndpoint: false,
requiresResponseRewrite: true,
},
INCEPTION: {
id: 'inception',
apiUrl: 'https://api.inceptionlabs.ai/v1',
apiKey: getEnvVariable('INCEPTION_API_KEY'),
hasGenerationEndpoint: false,
requiresResponseRewrite: false,
},
MARTIAN: {
id: 'martian',
apiUrl: 'https://api.withmartian.com/v1',
Expand All @@ -92,34 +84,13 @@ export const PROVIDERS = {
hasGenerationEndpoint: false,
requiresResponseRewrite: false,
},
MINIMAX: {
id: 'minimax',
apiUrl: 'https://api.minimax.io/v1',
apiKey: getEnvVariable('MINIMAX_API_KEY'),
hasGenerationEndpoint: false,
requiresResponseRewrite: false,
},
STREAMLAKE: {
id: 'streamlake',
apiUrl: 'https://vanchin.streamlake.ai/api/gateway/v1/endpoints',
apiKey: getEnvVariable('STREAMLAKE_API_KEY'),
hasGenerationEndpoint: false,
requiresResponseRewrite: false,
},
VERCEL_AI_GATEWAY: {
id: 'vercel',
apiUrl: 'https://ai-gateway.vercel.sh/v1',
apiKey: getEnvVariable('VERCEL_AI_GATEWAY_API_KEY'),
hasGenerationEndpoint: true,
requiresResponseRewrite: false,
},
XAI: {
id: 'x-ai',
apiUrl: 'https://api.x.ai/v1',
apiKey: getEnvVariable('XAI_API_KEY'),
hasGenerationEndpoint: false,
requiresResponseRewrite: false,
},
} as const satisfies Record<string, Provider>;

export async function getProvider(
Expand Down Expand Up @@ -305,10 +276,6 @@ export function applyProviderSpecificLogic(
applyCoreThinkProviderSettings(requestToMutate);
}

if (provider.id === 'minimax') {
applyMinimaxProviderSettings(requestToMutate);
}

if (provider.id === 'mistral') {
applyMistralProviderSettings(requestToMutate, extraHeaders);
} else if (isMistralModel(requestedModel)) {
Expand Down
Loading