Skip to content

Commit e0ff879

Browse files
committed
Add native Anthropic Messages and OpenAI Responses request formatting with provider-specific reasoning effort and streaming support
- for anthropic, use its native api, not an openai compatibility layer (this way we can enable thinking effort and receive reasoning summaries) - when using anthropic, we get [Pasted text] - similarly to anthropic, openai should use the native api, so that 'thinking' is reported to the user - 'thinking' summaries are never received/reported to the user when using openai, though responses do arrive - remove redundant comments fom edits: [Changes] ├── apps │ └── editor │ ├── package.json │ └── src │ ├── commands │ │ ├── code-at-cursor-command │ │ │ └── utils │ │ │ └── perform-code-at-cursor.ts │ │ └── generate-commit-message-commands │ │ └── utils │ │ └── generate-commit-message-with-api.ts │ ├── constants │ │ └── providers.ts │ ├── features │ │ └── search-files │ │ └── utils │ │ └── search-files-by-intelligent.ts │ ├── i18n │ │ └── translations │ │ └── utils │ │ └── send-llm-message.ts │ ├── services │ │ ├── model-fetcher.ts │ │ ├── prompt-view-api-calls-manager.ts │ │ └── providers-manager.ts │ ├── utils │ │ ├── apply-reasoning-effort.ts │ │ ├── build-chat-request.ts │ │ ├── build-request-headers.ts │ │ ├── build-user-content.ts │ │ └── send-llm-message.ts │ └── views │ ├── chats │ │ ├── backend │ │ │ ├── chats-view-provider.ts │ │ │ └── message-handlers │ │ │ └── handle-get-chats.ts │ │ ├── frontend │ │ │ └── Chats.tsx │ │ └── types │ │ └── messages.ts │ ├── prompt │ │ └── backend │ │ └── message-handlers │ │ ├── configurations │ │ │ └── handle-pick-api-reasoning-effort.ts │ │ ├── handle-make-api-call.ts │ │ ├── handle-patch-repair.ts │ │ ├── handle-voice-input.ts │ │ └── utils │ │ └── patch-repair-utils.ts │ ├── settings │ │ ├── backend │ │ │ └── message-handlers │ │ │ └── handle-pick-api-reasoning-effort.ts │ │ └── frontend │ │ └── forms │ │ └── EditProviderForm │ │ └── EditProviderForm.tsx │ └── shared │ └── actions │ └── api │ ├── create │ │ └── interactions │ │ ├── verify-model.ts │ │ └── verify-reasoning-effort.ts │ └── update │ └── interactions │ └── edit-reasoning-effort-for-api-config.ts └── packages └── ui └── src └── components └── editor └── chats-view ├── Chats │ └── Chats.tsx └── Progress └── Progress.tsx
1 parent b2346cf commit e0ff879

7 files changed

Lines changed: 341 additions & 70 deletions

File tree

‎apps/editor/src/services/prompt-view-api-calls-manager.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export class PromptViewApiCallsManager {
4343
const body_to_hash = JSON.parse(JSON.stringify(params.body))
4444
delete body_to_hash.reasoning_effort
4545
delete body_to_hash.reasoning
46+
delete body_to_hash.thinking
47+
delete body_to_hash.output_config
4648
if (body_to_hash.extra_body?.google?.thinking_config) {
4749
delete body_to_hash.extra_body.google.thinking_config
4850
}

‎apps/editor/src/utils/apply-reasoning-effort.ts‎

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ApiConfiguration, Provider } from '@/services/providers-manager'
2+
import { PROVIDERS } from '@/constants/providers'
23

34
export const apply_reasoning_effort = (params: {
45
body: { [key: string]: any }
@@ -17,14 +18,30 @@ export const apply_reasoning_effort = (params: {
1718
params.provider.base_url ==
1819
'https://generativelanguage.googleapis.com/v1beta/openai/v1'
1920
) {
20-
params.body.extra_body = {
21-
google: {
22-
thinking_config: {
23-
thinking_level: params.reasoning_effort,
24-
include_thoughts: true
21+
if (params.reasoning_effort) {
22+
params.body.extra_body = {
23+
google: {
24+
thinking_config: {
25+
thinking_level: params.reasoning_effort,
26+
include_thoughts: true
27+
}
2528
}
2629
}
2730
}
31+
} else if (params.provider.base_url === PROVIDERS.Anthropic.base_url) {
32+
if (params.reasoning_effort === 'none') {
33+
params.body.thinking = { type: 'disabled' }
34+
} else if (params.reasoning_effort) {
35+
params.body.thinking = { type: 'adaptive', display: 'summarized' }
36+
params.body.output_config = { effort: params.reasoning_effort }
37+
}
38+
} else if (params.provider.base_url === PROVIDERS.OpenAI.base_url) {
39+
if (params.reasoning_effort) {
40+
params.body.reasoning =
41+
params.reasoning_effort === 'none'
42+
? { effort: 'none' }
43+
: { effort: params.reasoning_effort, summary: 'auto' }
44+
}
2845
} else {
2946
if (params.reasoning_effort) {
3047
params.body.reasoning_effort = params.reasoning_effort
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { PROVIDERS } from '@/constants/providers'
2+
3+
const ANTHROPIC_MAX_TOKENS = 64000
4+
5+
export type ChatApiFormat =
6+
| 'chat-completions'
7+
| 'anthropic-messages'
8+
| 'openai-responses'
9+
10+
export const is_anthropic_base_url = (base_url: string): boolean =>
11+
base_url === PROVIDERS.Anthropic.base_url
12+
13+
export const is_openai_base_url = (base_url: string): boolean =>
14+
base_url === PROVIDERS.OpenAI.base_url
15+
16+
const get_text_from_content = (content: any): string => {
17+
if (typeof content === 'string') return content
18+
if (Array.isArray(content)) {
19+
return content
20+
.filter((part) => part?.type === 'text' && typeof part.text === 'string')
21+
.map((part) => part.text)
22+
.join('\n\n')
23+
}
24+
return ''
25+
}
26+
27+
const has_audio_input = (messages: any[]): boolean =>
28+
messages.some(
29+
(message) =>
30+
Array.isArray(message?.content) &&
31+
message.content.some((part: any) => part?.type === 'input_audio')
32+
)
33+
34+
const to_responses_content = (content: any): any => {
35+
if (!Array.isArray(content)) return content
36+
return content.map((part) => {
37+
if (part?.type === 'text') {
38+
return { ...part, type: 'input_text' }
39+
}
40+
if (part?.type === 'image_url') {
41+
const { image_url, ...rest } = part
42+
return {
43+
...rest,
44+
type: 'input_image',
45+
image_url: typeof image_url === 'string' ? image_url : image_url?.url
46+
}
47+
}
48+
return part
49+
})
50+
}
51+
52+
const build_openai_responses_request = (params: {
53+
base_url: string
54+
body: { [key: string]: any }
55+
}): { url: string; body: { [key: string]: any }; format: ChatApiFormat } => {
56+
const { messages = [], ...rest } = params.body
57+
const instructions_parts: string[] = []
58+
const input: any[] = []
59+
60+
for (const message of messages) {
61+
if (message.role === 'system') {
62+
const text = get_text_from_content(message.content)
63+
if (text) instructions_parts.push(text)
64+
} else {
65+
input.push({
66+
role: message.role,
67+
content: to_responses_content(message.content)
68+
})
69+
}
70+
}
71+
72+
return {
73+
url: `${params.base_url}/responses`,
74+
body: {
75+
store: false,
76+
...rest,
77+
...(instructions_parts.length > 0
78+
? { instructions: instructions_parts.join('\n\n') }
79+
: {}),
80+
input
81+
},
82+
format: 'openai-responses'
83+
}
84+
}
85+
86+
export const build_chat_request = (params: {
87+
base_url: string
88+
body: { [key: string]: any }
89+
}): { url: string; body: { [key: string]: any }; format: ChatApiFormat } => {
90+
if (is_openai_base_url(params.base_url)) {
91+
if (!has_audio_input(params.body.messages ?? [])) {
92+
return build_openai_responses_request(params)
93+
}
94+
95+
const { reasoning, ...rest } = params.body
96+
return {
97+
url: `${params.base_url}/chat/completions`,
98+
body: reasoning?.effort
99+
? { ...rest, reasoning_effort: reasoning.effort }
100+
: rest,
101+
format: 'chat-completions'
102+
}
103+
}
104+
105+
if (!is_anthropic_base_url(params.base_url)) {
106+
return {
107+
url: `${params.base_url}/chat/completions`,
108+
body: params.body,
109+
format: 'chat-completions'
110+
}
111+
}
112+
113+
const { messages = [], ...rest } = params.body
114+
const system_parts: string[] = []
115+
const native_messages: any[] = []
116+
117+
for (const message of messages) {
118+
if (message.role === 'system') {
119+
const text = get_text_from_content(message.content)
120+
if (text) system_parts.push(text)
121+
} else {
122+
native_messages.push(message)
123+
}
124+
}
125+
126+
return {
127+
url: `${params.base_url}/messages`,
128+
body: {
129+
max_tokens: ANTHROPIC_MAX_TOKENS,
130+
...rest,
131+
...(system_parts.length > 0 ? { system: system_parts.join('\n\n') } : {}),
132+
messages: native_messages
133+
},
134+
format: 'anthropic-messages'
135+
}
136+
}

‎apps/editor/src/utils/build-user-content.ts‎

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ export const build_user_content = (params: {
66
part1: string
77
part2: string
88
}): any => {
9+
const is_anthropic = params.provider.base_url === PROVIDERS.Anthropic.base_url
10+
const is_openai = params.provider.base_url === PROVIDERS.OpenAI.base_url
11+
912
const parse_text_with_images = (text: string) => {
1013
if (!text.includes('<cwc-image>')) {
1114
return [{ type: 'text', text }]
@@ -19,20 +22,28 @@ export const build_user_content = (params: {
1922
parsed.push({ type: 'text', text: part.trim() })
2023
}
2124
} else {
22-
parsed.push({
23-
type: 'image_url',
24-
image_url: {
25-
url: `data:image/png;base64,${part}`
26-
}
27-
})
25+
parsed.push(
26+
is_anthropic
27+
? {
28+
type: 'image',
29+
source: {
30+
type: 'base64',
31+
media_type: 'image/png',
32+
data: part
33+
}
34+
}
35+
: {
36+
type: 'image_url',
37+
image_url: {
38+
url: `data:image/png;base64,${part}`
39+
}
40+
}
41+
)
2842
}
2943
}
3044
return parsed
3145
}
3246

33-
const is_anthropic = params.provider.base_url === PROVIDERS.Anthropic.base_url
34-
const is_openai = params.provider.base_url === PROVIDERS.OpenAI.base_url
35-
3647
if (is_anthropic || is_openai) {
3748
const cache_config: any = {}
3849

@@ -48,18 +59,26 @@ export const build_user_content = (params: {
4859
}
4960
}
5061

51-
const user_content: any[] = [
52-
{
62+
const user_content: any[] = []
63+
64+
if (params.part1.trim()) {
65+
user_content.push({
5366
type: 'text',
5467
text: params.part1,
5568
...cache_config
56-
}
57-
]
69+
})
70+
}
5871

5972
if (!params.part2.includes('<cwc-image>')) {
60-
user_content.push({ type: 'text', text: params.part2 })
73+
if (params.part2.trim()) {
74+
user_content.push({ type: 'text', text: params.part2 })
75+
}
6176
} else {
62-
user_content.push(...parse_text_with_images(params.part2))
77+
user_content.push(
78+
...parse_text_with_images(params.part2).filter(
79+
(part) => part.type !== 'text' || part.text.trim()
80+
)
81+
)
6382
}
6483

6584
return user_content

0 commit comments

Comments
 (0)