Skip to content

Commit 0e7fd0e

Browse files
authored
Allow document/pdf upload for Claude, GPT and Grok 4 models (#2848)
* Allow document/pdf upload for Claude, GPT and Grok 4 models * Move
1 parent c1aeb7b commit 0e7fd0e

11 files changed

Lines changed: 102 additions & 50 deletions

File tree

apps/web/src/lib/ai-gateway/kilo-auto/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type AutoModel = {
1919
input_cache_read_price: string | undefined;
2020
input_cache_write_price: string | undefined;
2121
supports_images: boolean;
22+
supports_pdf: boolean;
2223
opencode_settings: OpenCodeSettings | undefined;
2324
};
2425

@@ -109,6 +110,7 @@ export const KILO_AUTO_FRONTIER_MODEL: AutoModel = {
109110
input_cache_read_price: '0.0000005',
110111
input_cache_write_price: '0.00000625',
111112
supports_images: true,
113+
supports_pdf: true,
112114
opencode_settings: {
113115
family: 'claude',
114116
prompt: 'anthropic',
@@ -127,6 +129,7 @@ export const KILO_AUTO_FREE_MODEL: AutoModel = {
127129
input_cache_read_price: '0',
128130
input_cache_write_price: '0',
129131
supports_images: false,
132+
supports_pdf: false,
130133
opencode_settings: undefined,
131134
};
132135

@@ -141,6 +144,7 @@ export const KILO_AUTO_BALANCED_MODEL: AutoModel = {
141144
input_cache_read_price: '0.0000000325',
142145
input_cache_write_price: '0.00000040625',
143146
supports_images: true,
147+
supports_pdf: false,
144148
opencode_settings: {
145149
ai_sdk_provider: 'openai-compatible',
146150
},
@@ -157,6 +161,7 @@ export const KILO_AUTO_SMALL_MODEL: AutoModel = {
157161
input_cache_read_price: '0.000000005',
158162
input_cache_write_price: undefined,
159163
supports_images: true,
164+
supports_pdf: false,
160165
opencode_settings: undefined,
161166
};
162167

apps/web/src/lib/ai-gateway/models.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ import { morph_warp_grep_free_model } from '@/lib/ai-gateway/providers/morph';
2525
import { gemma_4_26b_a4b_it_free_model } from '@/lib/ai-gateway/providers/google';
2626
import { qwen36_plus_model } from '@/lib/ai-gateway/providers/qwen';
2727
import { stepfun_35_flash_free_model } from '@/lib/ai-gateway/providers/stepfun';
28-
import { grok_code_fast_1_optimized_free_model } from '@/lib/ai-gateway/providers/xai';
28+
import {
29+
grok_code_fast_1_optimized_free_model,
30+
isGrok4Model,
31+
} from '@/lib/ai-gateway/providers/xai';
32+
import { isAnthropicModel } from '@/lib/ai-gateway/providers/anthropic.constants';
33+
import { isOpenAiModel } from '@/lib/ai-gateway/providers/openai';
2934

3035
export const PRIMARY_DEFAULT_MODEL = CLAUDE_SONNET_CURRENT_MODEL_ID;
3136

@@ -63,6 +68,10 @@ export function isFreeModel(model: string): boolean {
6368
);
6469
}
6570

71+
export function isPdfSupportingModel(model: string): boolean {
72+
return isAnthropicModel(model) || isOpenAiModel(model) || isGrok4Model(model);
73+
}
74+
6675
export function isKiloExclusiveFreeModel(model: string): boolean {
6776
return kiloExclusiveModels.some(
6877
m => m.public_id === model && m.status !== 'disabled' && !m.pricing

apps/web/src/lib/ai-gateway/processUsage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ import {
5454
} from '@/lib/ai-gateway/processUsage.messages';
5555
import { OPENROUTER_BYOK_COST_MULTIPLIER } from '@/lib/ai-gateway/processUsage.constants';
5656
import { computeOpenRouterCostFields, drainSseStream } from '@/lib/ai-gateway/processUsage.shared';
57-
import { isAnthropicModel } from '@/lib/ai-gateway/providers/anthropic';
57+
import { isAnthropicModel } from '@/lib/ai-gateway/providers/anthropic.constants';
5858
import { isMinimaxModel } from '@/lib/ai-gateway/providers/minimax';
5959
import type { KiloExclusiveModel } from '@/lib/ai-gateway/providers/kilo-exclusive-model';
6060

apps/web/src/lib/ai-gateway/providers/anthropic.constants.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { KiloExclusiveModel } from '@/lib/ai-gateway/providers/kilo-exclusive-model';
2+
import { modelStartsWith } from '@/lib/ai-gateway/providers/model-prefix';
23

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

@@ -22,3 +23,11 @@ export const claude_sonnet_clawsetup_model: KiloExclusiveModel = {
2223
pricing: null,
2324
exclusive_to: [],
2425
};
26+
27+
export function isAnthropicModel(requestedModel: string) {
28+
return modelStartsWith(requestedModel, 'anthropic/');
29+
}
30+
31+
export function isHaikuModel(requestedModel: string) {
32+
return modelStartsWith(requestedModel, 'anthropic/claude-haiku');
33+
}

apps/web/src/lib/ai-gateway/providers/anthropic.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,7 @@
1-
import { modelStartsWith } from '@/lib/ai-gateway/providers/model-prefix';
21
import { addCacheBreakpoints } from '@/lib/ai-gateway/providers/openrouter/request-helpers';
32
import type { GatewayRequest } from '@/lib/ai-gateway/providers/openrouter/types';
43
import { normalizeToolCallIds } from '@/lib/ai-gateway/tool-calling';
54

6-
export function isAnthropicModel(requestedModel: string) {
7-
return modelStartsWith(requestedModel, 'anthropic/');
8-
}
9-
10-
export function isHaikuModel(requestedModel: string) {
11-
return modelStartsWith(requestedModel, 'anthropic/claude-haiku');
12-
}
13-
145
function appendAnthropicBetaHeader(extraHeaders: Record<string, string>, betaFlag: string) {
156
for (const header of ['anthropic-beta', 'x-anthropic-beta']) {
167
extraHeaders[header] = [extraHeaders[header], betaFlag].filter(Boolean).join(',');

apps/web/src/lib/ai-gateway/providers/fixOpenCodeDuplicateReasoning.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ReasoningDetailType } from '@/lib/ai-gateway/custom-llm/reasoning-details';
2-
import { isAnthropicModel } from '@/lib/ai-gateway/providers/anthropic';
2+
import { isAnthropicModel } from '@/lib/ai-gateway/providers/anthropic.constants';
33
import type {
44
MessageWithReasoning,
55
OpenRouterChatCompletionRequest,

apps/web/src/lib/ai-gateway/providers/index.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,8 @@ import { applyMistralModelSettings, isMistralModel } from '@/lib/ai-gateway/prov
1212
import { applyXaiModelSettings, isXaiModel } from '@/lib/ai-gateway/providers/xai';
1313
import { shouldRouteToVercel } from '@/lib/ai-gateway/providers/vercel';
1414
import { kiloExclusiveModels } from '@/lib/ai-gateway/models';
15-
import {
16-
applyAnthropicModelSettings,
17-
isAnthropicModel,
18-
isHaikuModel,
19-
} from '@/lib/ai-gateway/providers/anthropic';
15+
import { applyAnthropicModelSettings } from '@/lib/ai-gateway/providers/anthropic';
16+
import { isAnthropicModel, isHaikuModel } from '@/lib/ai-gateway/providers/anthropic.constants';
2017
import {
2118
getBYOKforOrganization,
2219
getBYOKforUser,

apps/web/src/lib/ai-gateway/providers/model-prefix.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { modelStartsWith, stripModelTilde } from './model-prefix';
2-
import { isAnthropicModel, isHaikuModel } from './anthropic';
2+
import { isAnthropicModel, isHaikuModel } from './anthropic.constants';
33
import { isOpenAiModel, isOpenAiOssModel } from './openai';
44
import { isGeminiModel, isGemmaModel, isGemini3Model } from './google';
55
import { isMoonshotModel } from './moonshotai';

apps/web/src/lib/ai-gateway/providers/model-settings.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import { isAnthropicModel } from '@/lib/ai-gateway/providers/anthropic.constants';
12
import { seed_20_pro_free_model } from '@/lib/ai-gateway/providers/bytedance';
23
import { isGemini3Model, isGemmaModel } from '@/lib/ai-gateway/providers/google';
34
import { modelStartsWith } from '@/lib/ai-gateway/providers/model-prefix';
45
import { isMoonshotModel } from '@/lib/ai-gateway/providers/moonshotai';
56
import { isOpenAiModel } from '@/lib/ai-gateway/providers/openai';
67
import { qwen36_plus_model } from '@/lib/ai-gateway/providers/qwen';
7-
import { isXaiModel } from '@/lib/ai-gateway/providers/xai';
8+
import { isGrok4Model, isXaiModel } from '@/lib/ai-gateway/providers/xai';
89
import { isZaiModel } from '@/lib/ai-gateway/providers/zai';
910
import type {
1011
CustomLlmProvider,
@@ -36,7 +37,7 @@ export function getModelVariants(model: string): OpenCodeSettings['variants'] {
3637
max: { reasoning: { enabled: true, effort: 'xhigh' }, verbosity: 'max' },
3738
};
3839
}
39-
if (modelStartsWith(model, 'anthropic/')) {
40+
if (isAnthropicModel(model)) {
4041
return {
4142
none: { reasoning: { enabled: false, effort: 'none' } },
4243
low: { reasoning: { enabled: true, effort: 'low' }, verbosity: 'low' },
@@ -83,7 +84,7 @@ export function getModelVariants(model: string): OpenCodeSettings['variants'] {
8384
high: { reasoning: { enabled: true, effort: 'high' } },
8485
};
8586
}
86-
if (model.startsWith('x-ai/grok-4')) {
87+
if (isGrok4Model(model)) {
8788
return {
8889
'non-reasoning': { reasoning: { enabled: false, effort: 'none' } },
8990
reasoning: { reasoning: { enabled: true, effort: 'medium' } },
@@ -101,6 +102,10 @@ function getAiSdkProvider(model: string): CustomLlmProvider | undefined {
101102
// with 'openai' a bunch of bugs in vercel ai sdk v5 get triggered
102103
return 'openai-compatible';
103104
}
105+
if (isAnthropicModel(model)) {
106+
// on Vercel AI Gateway, this is necessary to support document attachments
107+
return 'anthropic';
108+
}
104109
if (isOpenAiModel(model) || isXaiModel(model)) {
105110
// OpenAI: "While Chat Completions remains supported, Responses is recommended for all new projects.""
106111
// xAI: "The Responses API is the recommended way to interact with xAI models."
@@ -116,6 +121,10 @@ export function getOpenCodeSettings(model: string): OpenCodeSettings | undefined
116121
}
117122

118123
export function getOpenClawSettings(model: string): OpenClawModelSettings | undefined {
124+
// 2026-04-28: this is aspirational, the OpenClaw Kilo provider does not respect this
125+
if (isAnthropicModel(model)) {
126+
return { api_adapter: 'anthropic-messages' };
127+
}
119128
if (isOpenAiModel(model) || isXaiModel(model)) {
120129
return { api_adapter: 'openai-responses' };
121130
}

apps/web/src/lib/ai-gateway/providers/openrouter/index.ts

Lines changed: 57 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { isFreeModel, kiloExclusiveModels, preferredModels } from '@/lib/ai-gateway/models';
1+
import {
2+
isFreeModel,
3+
isPdfSupportingModel,
4+
kiloExclusiveModels,
5+
preferredModels,
6+
} from '@/lib/ai-gateway/models';
27
import PROVIDERS from '@/lib/ai-gateway/providers/provider-definitions';
38
import type { OpenRouterModel } from '@/lib/organizations/organization-types';
49
import {
@@ -19,35 +24,50 @@ import { AUTO_MODELS } from '@/lib/ai-gateway/kilo-auto';
1924
export { normalizeModelId } from '@/lib/ai-gateway/model-utils';
2025

2126
function buildAutoModels(): OpenRouterModel[] {
22-
return AUTO_MODELS.map(m => ({
23-
id: m.id,
24-
name: m.name,
25-
created: 0,
26-
description: m.description,
27-
architecture: {
28-
input_modalities: m.supports_images ? ['text', 'image'] : ['text'],
29-
output_modalities: ['text'],
30-
tokenizer: 'Other',
31-
},
32-
top_provider: {
33-
is_moderated: false,
27+
return AUTO_MODELS.map(m => {
28+
const input_modalities = ['text'];
29+
if (m.supports_images) {
30+
input_modalities.push('image');
31+
}
32+
if (m.supports_pdf) {
33+
input_modalities.push('pdf');
34+
}
35+
return {
36+
id: m.id,
37+
name: m.name,
38+
created: 0,
39+
description: m.description,
40+
architecture: {
41+
input_modalities: input_modalities,
42+
output_modalities: ['text'],
43+
tokenizer: 'Other',
44+
},
45+
top_provider: {
46+
is_moderated: false,
47+
context_length: m.context_length,
48+
max_completion_tokens: m.max_completion_tokens,
49+
},
50+
pricing: {
51+
prompt: m.prompt_price,
52+
completion: m.completion_price,
53+
input_cache_read: m.input_cache_read_price,
54+
input_cache_write: m.input_cache_write_price,
55+
request: '0',
56+
image: '0',
57+
web_search: '0',
58+
internal_reasoning: '0',
59+
},
3460
context_length: m.context_length,
35-
max_completion_tokens: m.max_completion_tokens,
36-
},
37-
pricing: {
38-
prompt: m.prompt_price,
39-
completion: m.completion_price,
40-
input_cache_read: m.input_cache_read_price,
41-
input_cache_write: m.input_cache_write_price,
42-
request: '0',
43-
image: '0',
44-
web_search: '0',
45-
internal_reasoning: '0',
46-
},
47-
context_length: m.context_length,
48-
supported_parameters: ['max_tokens', 'temperature', 'tools', 'reasoning', 'include_reasoning'],
49-
opencode: m.opencode_settings,
50-
}));
61+
supported_parameters: [
62+
'max_tokens',
63+
'temperature',
64+
'tools',
65+
'reasoning',
66+
'include_reasoning',
67+
],
68+
opencode: m.opencode_settings,
69+
};
70+
});
5171
}
5272

5373
function enhancedModelList(models: OpenRouterModel[]) {
@@ -68,13 +88,21 @@ function enhancedModelList(models: OpenRouterModel[]) {
6888
const ageDays = (Date.now() / 1_000 - model.created) / (24 * 3600);
6989
const isNew = preferredIndex >= 0 && ageDays >= 0 && ageDays < 7;
7090
const skipSuffix = model.name.endsWith(')');
91+
const addPdf =
92+
isPdfSupportingModel(model.id) && !model.architecture.input_modalities.includes('pdf');
7193
return {
7294
...model,
7395
name: skipSuffix ? model.name : isNew ? model.name + ' (new)' : model.name,
7496
preferredIndex: preferredIndex >= 0 ? preferredIndex : undefined,
7597
isFree: isFreeModel(model.id),
7698
opencode: model.opencode ?? getOpenCodeSettings(model.id),
7799
openclaw: model.openclaw ?? getOpenClawSettings(model.id),
100+
architecture: addPdf
101+
? {
102+
...model.architecture,
103+
input_modalities: model.architecture.input_modalities.concat(['pdf']),
104+
}
105+
: model.architecture,
78106
};
79107
});
80108
const sortedModels = enhancedModels.sort((a, b) => {

0 commit comments

Comments
 (0)