Skip to content

Commit f07b9ca

Browse files
AmintaCCCPopenclaw
andauthored
feat(ai): add optional OpenAI Responses API support (EvanLi#44)
Co-authored-by: openclaw <openclaw@users.noreply.github.com>
1 parent e7e9a03 commit f07b9ca

4 files changed

Lines changed: 45 additions & 23 deletions

File tree

server/src/routes/proxy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ router.post('/api/proxy/ai', async (req, res) => {
100100
'Accept': 'application/json',
101101
};
102102

103-
if (apiType === 'openai') {
104-
targetUrl = buildApiUrl(baseUrl, 'v1/chat/completions');
103+
if (apiType === 'openai' || apiType === 'openai-responses') {
104+
targetUrl = buildApiUrl(baseUrl, apiType === 'openai-responses' ? 'v1/responses' : 'v1/chat/completions');
105105
headers['Authorization'] = `Bearer ${apiKey}`;
106106
} else if (apiType === 'claude') {
107107
targetUrl = buildApiUrl(baseUrl, 'v1/messages');

src/components/SettingsPanel.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export const SettingsPanel: React.FC = () => {
9494

9595
type AIFormState = {
9696
name: string;
97-
apiType: 'openai' | 'claude' | 'gemini';
97+
apiType: 'openai' | 'openai-responses' | 'claude' | 'gemini';
9898
baseUrl: string;
9999
apiKey: string;
100100
model: string;
@@ -728,10 +728,11 @@ Focus on practicality and accurate categorization to help users quickly understa
728728
</label>
729729
<select
730730
value={aiForm.apiType}
731-
onChange={(e) => setAIForm(prev => ({ ...prev, apiType: e.target.value as 'openai' | 'claude' | 'gemini' }))}
731+
onChange={(e) => setAIForm(prev => ({ ...prev, apiType: e.target.value as 'openai' | 'openai-responses' | 'claude' | 'gemini' }))}
732732
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
733733
>
734-
<option value="openai">OpenAI</option>
734+
<option value="openai">OpenAI (Chat Completions)</option>
735+
<option value="openai-responses">OpenAI (Responses)</option>
735736
<option value="claude">Claude</option>
736737
<option value="gemini">Gemini</option>
737738
</select>
@@ -747,7 +748,7 @@ Focus on practicality and accurate categorization to help users quickly understa
747748
onChange={(e) => setAIForm(prev => ({ ...prev, baseUrl: e.target.value }))}
748749
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
749750
placeholder={
750-
aiForm.apiType === 'openai'
751+
aiForm.apiType === 'openai' || aiForm.apiType === 'openai-responses'
751752
? 'https://api.openai.com/v1'
752753
: aiForm.apiType === 'claude'
753754
? 'https://api.anthropic.com/v1'
@@ -756,8 +757,8 @@ Focus on practicality and accurate categorization to help users quickly understa
756757
/>
757758
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
758759
{t(
759-
'只填到版本号即可(如 .../v1 或 .../v1beta),不要包含 /chat/completions、/messages 或 :generateContent',
760-
'Only include the version prefix (e.g. .../v1 or .../v1beta). Do not include /chat/completions, /messages, or :generateContent.'
760+
'只填到版本号即可(如 .../v1 或 .../v1beta),不要包含 /chat/completions、/responses、/messages 或 :generateContent',
761+
'Only include the version prefix (e.g. .../v1 or .../v1beta). Do not include /chat/completions, /responses, /messages, or :generateContent.'
761762
)}
762763
</p>
763764
</div>

src/services/aiService.ts

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ export class AIService {
1010
this.language = language;
1111
}
1212

13-
private getApiType(): 'openai' | 'claude' | 'gemini' {
14-
return this.config.apiType || 'openai';
13+
private getApiType(): 'openai' | 'openai-responses' | 'claude' | 'gemini' {
14+
return (this.config.apiType as 'openai' | 'openai-responses' | 'claude' | 'gemini') || 'openai';
1515
}
1616

1717
private buildApiUrl(pathWithVersion: string): string {
@@ -50,25 +50,33 @@ export class AIService {
5050
}): Promise<string> {
5151
const apiType = this.getApiType();
5252

53-
if (apiType === 'openai') {
53+
if (apiType === 'openai' || apiType === 'openai-responses') {
5454
const messages = [
5555
...(options.system.trim()
5656
? [{ role: 'system', content: options.system }]
5757
: []),
5858
{ role: 'user', content: options.user },
5959
];
60-
const requestBody = {
61-
model: this.config.model,
62-
messages,
63-
temperature: options.temperature,
64-
max_tokens: options.maxTokens,
65-
};
60+
61+
const requestBody = apiType === 'openai-responses'
62+
? {
63+
model: this.config.model,
64+
input: messages,
65+
temperature: options.temperature,
66+
max_output_tokens: options.maxTokens,
67+
}
68+
: {
69+
model: this.config.model,
70+
messages,
71+
temperature: options.temperature,
72+
max_tokens: options.maxTokens,
73+
};
6674

6775
let data: any;
6876
if (backend.isAvailable && this.config.id) {
6977
data = await backend.proxyAIRequest(this.config.id, requestBody);
7078
} else {
71-
const url = this.buildApiUrl('v1/chat/completions');
79+
const url = this.buildApiUrl(apiType === 'openai-responses' ? 'v1/responses' : 'v1/chat/completions');
7280
const response = await fetch(url, {
7381
method: 'POST',
7482
headers: {
@@ -85,11 +93,24 @@ export class AIService {
8593
data = await response.json();
8694
}
8795

88-
const content = data.choices?.[0]?.message?.content;
89-
if (!content) {
90-
throw new Error('No content received from AI service');
96+
if (apiType === 'openai-responses') {
97+
const outputText = typeof data?.output_text === 'string' ? data.output_text : '';
98+
if (outputText) return outputText;
99+
100+
const output = data?.output;
101+
if (Array.isArray(output)) {
102+
const text = output
103+
.flatMap((item: any) => Array.isArray(item?.content) ? item.content : [])
104+
.map((part: any) => (typeof part?.text === 'string' ? part.text : ''))
105+
.join('');
106+
if (text) return text;
107+
}
108+
} else {
109+
const content = data?.choices?.[0]?.message?.content;
110+
if (content) return content;
91111
}
92-
return content;
112+
113+
throw new Error('No content received from AI service');
93114
}
94115

95116
if (apiType === 'claude') {

src/types/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export interface GitHubUser {
6969
export interface AIConfig {
7070
id: string;
7171
name: string;
72-
apiType?: 'openai' | 'claude' | 'gemini'; // API 格式/兼容协议(默认 openai)
72+
apiType?: 'openai' | 'openai-responses' | 'claude' | 'gemini'; // API 格式/兼容协议(默认 openai)
7373
baseUrl: string;
7474
apiKey: string;
7575
model: string;

0 commit comments

Comments
 (0)