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
5 changes: 5 additions & 0 deletions apps/web/src/lib/ai-gateway/kilo-auto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type AutoModel = {
input_cache_read_price: string | undefined;
input_cache_write_price: string | undefined;
supports_images: boolean;
supports_pdf: boolean;
opencode_settings: OpenCodeSettings | undefined;
};

Expand Down Expand Up @@ -109,6 +110,7 @@ export const KILO_AUTO_FRONTIER_MODEL: AutoModel = {
input_cache_read_price: '0.0000005',
input_cache_write_price: '0.00000625',
supports_images: true,
supports_pdf: true,
opencode_settings: {
family: 'claude',
prompt: 'anthropic',
Expand All @@ -127,6 +129,7 @@ export const KILO_AUTO_FREE_MODEL: AutoModel = {
input_cache_read_price: '0',
input_cache_write_price: '0',
supports_images: false,
supports_pdf: false,
opencode_settings: undefined,
};

Expand All @@ -141,6 +144,7 @@ export const KILO_AUTO_BALANCED_MODEL: AutoModel = {
input_cache_read_price: '0.0000000325',
input_cache_write_price: '0.00000040625',
supports_images: true,
supports_pdf: false,
opencode_settings: {
ai_sdk_provider: 'openai-compatible',
},
Expand All @@ -157,6 +161,7 @@ export const KILO_AUTO_SMALL_MODEL: AutoModel = {
input_cache_read_price: '0.000000005',
input_cache_write_price: undefined,
supports_images: true,
supports_pdf: false,
opencode_settings: undefined,
};

Expand Down
11 changes: 10 additions & 1 deletion apps/web/src/lib/ai-gateway/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ import { morph_warp_grep_free_model } from '@/lib/ai-gateway/providers/morph';
import { gemma_4_26b_a4b_it_free_model } from '@/lib/ai-gateway/providers/google';
import { qwen36_plus_model } from '@/lib/ai-gateway/providers/qwen';
import { stepfun_35_flash_free_model } from '@/lib/ai-gateway/providers/stepfun';
import { grok_code_fast_1_optimized_free_model } from '@/lib/ai-gateway/providers/xai';
import {
grok_code_fast_1_optimized_free_model,
isGrok4Model,
} from '@/lib/ai-gateway/providers/xai';
import { isAnthropicModel } from '@/lib/ai-gateway/providers/anthropic.constants';
import { isOpenAiModel } from '@/lib/ai-gateway/providers/openai';

export const PRIMARY_DEFAULT_MODEL = CLAUDE_SONNET_CURRENT_MODEL_ID;

Expand Down Expand Up @@ -63,6 +68,10 @@ export function isFreeModel(model: string): boolean {
);
}

export function isPdfSupportingModel(model: string): boolean {
return isAnthropicModel(model) || isOpenAiModel(model) || isGrok4Model(model);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does every OpenAI model support pdf?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably, I guess it's an API feature

}

export function isKiloExclusiveFreeModel(model: string): boolean {
return kiloExclusiveModels.some(
m => m.public_id === model && m.status !== 'disabled' && !m.pricing
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/lib/ai-gateway/processUsage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import {
} from '@/lib/ai-gateway/processUsage.messages';
import { OPENROUTER_BYOK_COST_MULTIPLIER } from '@/lib/ai-gateway/processUsage.constants';
import { computeOpenRouterCostFields, drainSseStream } from '@/lib/ai-gateway/processUsage.shared';
import { isAnthropicModel } from '@/lib/ai-gateway/providers/anthropic';
import { isAnthropicModel } from '@/lib/ai-gateway/providers/anthropic.constants';
import { isMinimaxModel } from '@/lib/ai-gateway/providers/minimax';
import type { KiloExclusiveModel } from '@/lib/ai-gateway/providers/kilo-exclusive-model';

Expand Down
9 changes: 9 additions & 0 deletions apps/web/src/lib/ai-gateway/providers/anthropic.constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { KiloExclusiveModel } from '@/lib/ai-gateway/providers/kilo-exclusive-model';
import { modelStartsWith } from '@/lib/ai-gateway/providers/model-prefix';

export const CLAUDE_SONNET_CURRENT_MODEL_ID = 'anthropic/claude-sonnet-4.6';

Expand All @@ -22,3 +23,11 @@ export const claude_sonnet_clawsetup_model: KiloExclusiveModel = {
pricing: null,
exclusive_to: [],
};

export function isAnthropicModel(requestedModel: string) {
return modelStartsWith(requestedModel, 'anthropic/');
}

export function isHaikuModel(requestedModel: string) {
return modelStartsWith(requestedModel, 'anthropic/claude-haiku');
}
9 changes: 0 additions & 9 deletions apps/web/src/lib/ai-gateway/providers/anthropic.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
import { modelStartsWith } from '@/lib/ai-gateway/providers/model-prefix';
import { addCacheBreakpoints } from '@/lib/ai-gateway/providers/openrouter/request-helpers';
import type { GatewayRequest } from '@/lib/ai-gateway/providers/openrouter/types';
import { normalizeToolCallIds } from '@/lib/ai-gateway/tool-calling';

export function isAnthropicModel(requestedModel: string) {
return modelStartsWith(requestedModel, 'anthropic/');
}

export function isHaikuModel(requestedModel: string) {
return modelStartsWith(requestedModel, 'anthropic/claude-haiku');
}

function appendAnthropicBetaHeader(extraHeaders: Record<string, string>, betaFlag: string) {
for (const header of ['anthropic-beta', 'x-anthropic-beta']) {
extraHeaders[header] = [extraHeaders[header], betaFlag].filter(Boolean).join(',');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ReasoningDetailType } from '@/lib/ai-gateway/custom-llm/reasoning-details';
import { isAnthropicModel } from '@/lib/ai-gateway/providers/anthropic';
import { isAnthropicModel } from '@/lib/ai-gateway/providers/anthropic.constants';
import type {
MessageWithReasoning,
OpenRouterChatCompletionRequest,
Expand Down
7 changes: 2 additions & 5 deletions apps/web/src/lib/ai-gateway/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,8 @@ import { applyMistralModelSettings, isMistralModel } from '@/lib/ai-gateway/prov
import { applyXaiModelSettings, isXaiModel } from '@/lib/ai-gateway/providers/xai';
import { shouldRouteToVercel } from '@/lib/ai-gateway/providers/vercel';
import { kiloExclusiveModels } from '@/lib/ai-gateway/models';
import {
applyAnthropicModelSettings,
isAnthropicModel,
isHaikuModel,
} from '@/lib/ai-gateway/providers/anthropic';
import { applyAnthropicModelSettings } from '@/lib/ai-gateway/providers/anthropic';
import { isAnthropicModel, isHaikuModel } from '@/lib/ai-gateway/providers/anthropic.constants';
import {
getBYOKforOrganization,
getBYOKforUser,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/lib/ai-gateway/providers/model-prefix.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { modelStartsWith, stripModelTilde } from './model-prefix';
import { isAnthropicModel, isHaikuModel } from './anthropic';
import { isAnthropicModel, isHaikuModel } from './anthropic.constants';
import { isOpenAiModel, isOpenAiOssModel } from './openai';
import { isGeminiModel, isGemmaModel, isGemini3Model } from './google';
import { isMoonshotModel } from './moonshotai';
Expand Down
15 changes: 12 additions & 3 deletions apps/web/src/lib/ai-gateway/providers/model-settings.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { isAnthropicModel } from '@/lib/ai-gateway/providers/anthropic.constants';
import { seed_20_pro_free_model } from '@/lib/ai-gateway/providers/bytedance';
import { isGemini3Model, isGemmaModel } from '@/lib/ai-gateway/providers/google';
import { modelStartsWith } from '@/lib/ai-gateway/providers/model-prefix';
import { isMoonshotModel } from '@/lib/ai-gateway/providers/moonshotai';
import { isOpenAiModel } from '@/lib/ai-gateway/providers/openai';
import { qwen36_plus_model } from '@/lib/ai-gateway/providers/qwen';
import { isXaiModel } from '@/lib/ai-gateway/providers/xai';
import { isGrok4Model, isXaiModel } from '@/lib/ai-gateway/providers/xai';
import { isZaiModel } from '@/lib/ai-gateway/providers/zai';
import type {
CustomLlmProvider,
Expand Down Expand Up @@ -36,7 +37,7 @@ export function getModelVariants(model: string): OpenCodeSettings['variants'] {
max: { reasoning: { enabled: true, effort: 'xhigh' }, verbosity: 'max' },
};
}
if (modelStartsWith(model, 'anthropic/')) {
if (isAnthropicModel(model)) {
return {
none: { reasoning: { enabled: false, effort: 'none' } },
low: { reasoning: { enabled: true, effort: 'low' }, verbosity: 'low' },
Expand Down Expand Up @@ -83,7 +84,7 @@ export function getModelVariants(model: string): OpenCodeSettings['variants'] {
high: { reasoning: { enabled: true, effort: 'high' } },
};
}
if (model.startsWith('x-ai/grok-4')) {
if (isGrok4Model(model)) {
return {
'non-reasoning': { reasoning: { enabled: false, effort: 'none' } },
reasoning: { reasoning: { enabled: true, effort: 'medium' } },
Expand All @@ -101,6 +102,10 @@ function getAiSdkProvider(model: string): CustomLlmProvider | undefined {
// with 'openai' a bunch of bugs in vercel ai sdk v5 get triggered
return 'openai-compatible';
}
if (isAnthropicModel(model)) {
// on Vercel AI Gateway, this is necessary to support document attachments
return 'anthropic';
}
if (isOpenAiModel(model) || isXaiModel(model)) {
// OpenAI: "While Chat Completions remains supported, Responses is recommended for all new projects.""
// xAI: "The Responses API is the recommended way to interact with xAI models."
Expand All @@ -116,6 +121,10 @@ export function getOpenCodeSettings(model: string): OpenCodeSettings | undefined
}

export function getOpenClawSettings(model: string): OpenClawModelSettings | undefined {
// 2026-04-28: this is aspirational, the OpenClaw Kilo provider does not respect this
if (isAnthropicModel(model)) {
return { api_adapter: 'anthropic-messages' };
}
if (isOpenAiModel(model) || isXaiModel(model)) {
return { api_adapter: 'openai-responses' };
}
Expand Down
86 changes: 57 additions & 29 deletions apps/web/src/lib/ai-gateway/providers/openrouter/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { isFreeModel, kiloExclusiveModels, preferredModels } from '@/lib/ai-gateway/models';
import {
isFreeModel,
isPdfSupportingModel,
kiloExclusiveModels,
preferredModels,
} from '@/lib/ai-gateway/models';
import PROVIDERS from '@/lib/ai-gateway/providers/provider-definitions';
import type { OpenRouterModel } from '@/lib/organizations/organization-types';
import {
Expand All @@ -19,35 +24,50 @@ import { AUTO_MODELS } from '@/lib/ai-gateway/kilo-auto';
export { normalizeModelId } from '@/lib/ai-gateway/model-utils';

function buildAutoModels(): OpenRouterModel[] {
return AUTO_MODELS.map(m => ({
id: m.id,
name: m.name,
created: 0,
description: m.description,
architecture: {
input_modalities: m.supports_images ? ['text', 'image'] : ['text'],
output_modalities: ['text'],
tokenizer: 'Other',
},
top_provider: {
is_moderated: false,
return AUTO_MODELS.map(m => {
const input_modalities = ['text'];
if (m.supports_images) {
input_modalities.push('image');
}
if (m.supports_pdf) {
input_modalities.push('pdf');
}
return {
id: m.id,
name: m.name,
created: 0,
description: m.description,
architecture: {
input_modalities: input_modalities,
output_modalities: ['text'],
tokenizer: 'Other',
},
top_provider: {
is_moderated: false,
context_length: m.context_length,
max_completion_tokens: m.max_completion_tokens,
},
pricing: {
prompt: m.prompt_price,
completion: m.completion_price,
input_cache_read: m.input_cache_read_price,
input_cache_write: m.input_cache_write_price,
request: '0',
image: '0',
web_search: '0',
internal_reasoning: '0',
},
context_length: m.context_length,
max_completion_tokens: m.max_completion_tokens,
},
pricing: {
prompt: m.prompt_price,
completion: m.completion_price,
input_cache_read: m.input_cache_read_price,
input_cache_write: m.input_cache_write_price,
request: '0',
image: '0',
web_search: '0',
internal_reasoning: '0',
},
context_length: m.context_length,
supported_parameters: ['max_tokens', 'temperature', 'tools', 'reasoning', 'include_reasoning'],
opencode: m.opencode_settings,
}));
supported_parameters: [
'max_tokens',
'temperature',
'tools',
'reasoning',
'include_reasoning',
],
opencode: m.opencode_settings,
};
});
}

function enhancedModelList(models: OpenRouterModel[]) {
Expand All @@ -68,13 +88,21 @@ function enhancedModelList(models: OpenRouterModel[]) {
const ageDays = (Date.now() / 1_000 - model.created) / (24 * 3600);
const isNew = preferredIndex >= 0 && ageDays >= 0 && ageDays < 7;
const skipSuffix = model.name.endsWith(')');
const addPdf =
isPdfSupportingModel(model.id) && !model.architecture.input_modalities.includes('pdf');
return {
...model,
name: skipSuffix ? model.name : isNew ? model.name + ' (new)' : model.name,
preferredIndex: preferredIndex >= 0 ? preferredIndex : undefined,
isFree: isFreeModel(model.id),
opencode: model.opencode ?? getOpenCodeSettings(model.id),
openclaw: model.openclaw ?? getOpenClawSettings(model.id),
architecture: addPdf
? {
...model.architecture,
input_modalities: model.architecture.input_modalities.concat(['pdf']),
}
: model.architecture,
};
});
const sortedModels = enhancedModels.sort((a, b) => {
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/lib/ai-gateway/providers/xai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export function isXaiModel(requestedModel: string) {
return requestedModel.startsWith('x-ai/');
}

export function isGrok4Model(model: string) {
return model.startsWith('x-ai/grok-4');
}

export function applyXaiModelSettings(
requestToMutate: GatewayRequest,
extraHeaders: Record<string, string>
Expand Down
Loading