Skip to content

Commit 748e354

Browse files
committed
add support for the OpenAI Responses API compatible provider in LLM providers. OpenAI providers now use the Responses API protocol
1 parent e666a6c commit 748e354

12 files changed

Lines changed: 664 additions & 86 deletions

conf/ezbookkeeping.ini

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -172,12 +172,12 @@ transaction_from_ai_image_recognition = false
172172
max_ai_recognition_picture_size = 10485760
173173

174174
[llm_text_recognition]
175-
# Large Language Model (LLM) provider for transaction text recognition, supports the following types: "openai", "openai_compatible", "anthropic", "anthropic_compatible", "openrouter", "ollama", "lm_studio", "google_ai"
175+
# Large Language Model (LLM) provider for transaction text recognition, supports the following types: "openai", "openai_compatible", "openai_responses_compatible", "anthropic", "anthropic_compatible", "openrouter", "ollama", "lm_studio", "google_ai"
176176
llm_provider =
177177

178178
# Whether to enable thinking for the large language model, supports the following levels:
179179
# "off": disable thinking (supports all providers)
180-
# "low" / "medium" / "high" / "xhigh": enable thinking with the corresponding reasoning effort (supports "openai", "openai_compatible", "openrouter", "ollama", "lm_studio" and "google_ai" providers)
180+
# "low" / "medium" / "high" / "xhigh": enable thinking with the corresponding reasoning effort (supports "openai", "openai_compatible", "openai_responses_compatible", "openrouter", "ollama", "lm_studio" and "google_ai" providers)
181181
# "on": enable thinking with the default reasoning effort (supports "anthropic", "anthropic_compatible", "ollama" and "lm_studio" providers)
182182
# Leave blank to use the default setting of the LLM provider
183183
enable_thinking =
@@ -188,13 +188,13 @@ openai_api_key =
188188
# For "openai" llm provider only, transaction text recognition model for creating transactions from text
189189
openai_model_id =
190190

191-
# For "openai_compatible" llm provider only, OpenAI compatible API base url, e.g. "https://api.openai.com/v1/"
191+
# For "openai_compatible" and "openai_responses_compatible" llm provider only, OpenAI compatible API base url, e.g. "https://api.openai.com/v1/"
192192
openai_compatible_base_url =
193193

194-
# For "openai_compatible" llm provider only, OpenAI compatible API secret key
194+
# For "openai_compatible" and "openai_responses_compatible" llm provider only, OpenAI compatible API secret key
195195
openai_compatible_api_key =
196196

197-
# For "openai_compatible" llm provider only, transaction text recognition model for creating transactions from text
197+
# For "openai_compatible" and "openai_responses_compatible" llm provider only, transaction text recognition model for creating transactions from text
198198
openai_compatible_model_id =
199199

200200
# For "anthropic" llm provider only, Anthropic API key, please visit https://platform.claude.com/settings/keys for more information
@@ -265,12 +265,12 @@ proxy = system
265265
skip_tls_verify = false
266266

267267
[llm_image_recognition]
268-
# Large Language Model (LLM) provider for receipt image recognition, supports the following types: "openai", "openai_compatible", "anthropic", "anthropic_compatible", "openrouter", "ollama", "lm_studio", "google_ai"
268+
# Large Language Model (LLM) provider for receipt image recognition, supports the following types: "openai", "openai_compatible", "openai_responses_compatible", "anthropic", "anthropic_compatible", "openrouter", "ollama", "lm_studio", "google_ai"
269269
llm_provider =
270270

271271
# Whether to enable thinking for the large language model, supports the following levels:
272272
# "off": disable thinking (supports all providers)
273-
# "low" / "medium" / "high" / "xhigh": enable thinking with the corresponding reasoning effort (supports "openai", "openai_compatible", "openrouter", "ollama", "lm_studio" and "google_ai" providers)
273+
# "low" / "medium" / "high" / "xhigh": enable thinking with the corresponding reasoning effort (supports "openai", "openai_compatible", "openai_responses_compatible", "openrouter", "ollama", "lm_studio" and "google_ai" providers)
274274
# "on": enable thinking with the default reasoning effort (supports "anthropic", "anthropic_compatible", "ollama" and "lm_studio" providers)
275275
# Leave blank to use the default setting of the LLM provider
276276
enable_thinking =
@@ -281,13 +281,13 @@ openai_api_key =
281281
# For "openai" llm provider only, receipt image recognition model for creating transactions from images
282282
openai_model_id =
283283

284-
# For "openai_compatible" llm provider only, OpenAI compatible API base url, e.g. "https://api.openai.com/v1/"
284+
# For "openai_compatible" and "openai_responses_compatible" llm provider only, OpenAI compatible API base url, e.g. "https://api.openai.com/v1/"
285285
openai_compatible_base_url =
286286

287-
# For "openai_compatible" llm provider only, OpenAI compatible API secret key
287+
# For "openai_compatible" and "openai_responses_compatible" llm provider only, OpenAI compatible API secret key
288288
openai_compatible_api_key =
289289

290-
# For "openai_compatible" llm provider only, receipt image recognition model for creating transactions from images
290+
# For "openai_compatible" and "openai_responses_compatible" llm provider only, receipt image recognition model for creating transactions from images
291291
openai_compatible_model_id =
292292

293293
# For "anthropic" llm provider only, Anthropic API key, please visit https://platform.claude.com/settings/keys for more information

pkg/llm/large_language_model_provider_container.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ func initializeLargeLanguageModelProvider(llmConfig *settings.LLMConfig, enableR
5252
return openai.NewOpenAILargeLanguageModelProvider(llmConfig, enableResponseLog), nil
5353
} else if llmConfig.LLMProvider == settings.OpenAICompatibleLLMProvider {
5454
return openai.NewOpenAICompatibleLargeLanguageModelProvider(llmConfig, enableResponseLog), nil
55+
} else if llmConfig.LLMProvider == settings.OpenAIResponsesCompatibleLLMProvider {
56+
return openai.NewOpenAIResponsesCompatibleLargeLanguageModelProvider(llmConfig, enableResponseLog), nil
5557
} else if llmConfig.LLMProvider == settings.AnthropicLLMProvider {
5658
return anthropic.NewAnthropicLargeLanguageModelProvider(llmConfig, enableResponseLog), nil
5759
} else if llmConfig.LLMProvider == settings.AnthropicCompatibleLLMProvider {

pkg/llm/provider/openai/openai_chat_completions_api_provider.go

Lines changed: 0 additions & 44 deletions
This file was deleted.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package openai
2+
3+
// OpenAIMessageRole defines the role of OpenAI chat completions message
4+
type OpenAIMessageRole string
5+
6+
// OpenAI Message Roles
7+
const (
8+
OpenAIMessageRoleSystem OpenAIMessageRole = "system"
9+
OpenAIMessageRoleUser OpenAIMessageRole = "user"
10+
)

pkg/llm/provider/openai/openai_common_compatible_large_language_model_adapter.go

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,6 @@ type CommonOpenAIChatCompletionsAPILargeLanguageModelAdapter struct {
3333
ThinkingLevel settings.LLMThinkingLevel
3434
}
3535

36-
// OpenAIMessageRole defines the role of OpenAI chat completions message
37-
type OpenAIMessageRole string
38-
39-
// OpenAI Message Roles
40-
const (
41-
OpenAIMessageRoleSystem OpenAIMessageRole = "system"
42-
OpenAIMessageRoleUser OpenAIMessageRole = "user"
43-
)
44-
4536
// OpenAIChatCompletionsRequestResponseFormatType defines the type of OpenAI chat completions request response format
4637
type OpenAIChatCompletionsRequestResponseFormatType string
4738

@@ -79,8 +70,14 @@ type OpenAIChatCompletionsRequestImageContent struct {
7970

8071
// OpenAIChatCompletionsRequestResponseFormat defines the structure of OpenAI chat completions request response format
8172
type OpenAIChatCompletionsRequestResponseFormat struct {
82-
Type OpenAIChatCompletionsRequestResponseFormatType `json:"type"`
83-
JsonSchema *jsonschema.Schema `json:"json_schema,omitempty"`
73+
Type OpenAIChatCompletionsRequestResponseFormatType `json:"type"`
74+
JsonSchema *OpenAIChatCompletionsRequestResponseJsonSchema `json:"json_schema,omitempty"`
75+
}
76+
77+
type OpenAIChatCompletionsRequestResponseJsonSchema struct {
78+
Name string `json:"name"`
79+
Strict bool `json:"strict"`
80+
Schema *jsonschema.Schema `json:"schema"`
8481
}
8582

8683
// OpenAIChatCompletionsRequestImageUrl defines the structure of OpenAI image url
@@ -215,8 +212,12 @@ func (p *CommonOpenAIChatCompletionsAPILargeLanguageModelAdapter) buildJsonReque
215212
schema.Version = ""
216213

217214
chatCompletionsRequest.ResponseFormat = &OpenAIChatCompletionsRequestResponseFormat{
218-
Type: OpenAIChatCompletionsRequestResponseFormatTypeJsonSchema,
219-
JsonSchema: schema,
215+
Type: OpenAIChatCompletionsRequestResponseFormatTypeJsonSchema,
216+
JsonSchema: &OpenAIChatCompletionsRequestResponseJsonSchema{
217+
Name: "response",
218+
Strict: true,
219+
Schema: schema,
220+
},
220221
}
221222
} else {
222223
chatCompletionsRequest.ResponseFormat = &OpenAIChatCompletionsRequestResponseFormat{

pkg/llm/provider/openai/openai_common_compatible_large_language_model_adapter_test.go

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package openai
22

33
import (
44
"encoding/json"
5+
"reflect"
56
"testing"
67

78
"github.com/stretchr/testify/assert"
@@ -13,8 +14,8 @@ import (
1314

1415
func TestCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter_buildJsonRequestBody_TextualUserPrompt(t *testing.T) {
1516
adapter := &CommonOpenAIChatCompletionsAPILargeLanguageModelAdapter{
16-
apiProvider: &OpenAIOfficialChatCompletionsAPIProvider{
17-
OpenAIModelID: "test",
17+
apiProvider: &OpenAICompatibleChatCompletionsAPIProvider{
18+
OpenAICompatibleModelID: "test",
1819
},
1920
}
2021

@@ -35,8 +36,8 @@ func TestCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter_buildJsonReques
3536

3637
func TestCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter_buildJsonRequestBody_ImageUserPrompt(t *testing.T) {
3738
adapter := &CommonOpenAIChatCompletionsAPILargeLanguageModelAdapter{
38-
apiProvider: &OpenAIOfficialChatCompletionsAPIProvider{
39-
OpenAIModelID: "test",
39+
apiProvider: &OpenAICompatibleChatCompletionsAPIProvider{
40+
OpenAICompatibleModelID: "test",
4041
},
4142
}
4243

@@ -59,8 +60,8 @@ func TestCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter_buildJsonReques
5960

6061
func TestCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter_buildJsonRequestBody_ThinkingHighReasoningEffort(t *testing.T) {
6162
adapter := &CommonOpenAIChatCompletionsAPILargeLanguageModelAdapter{
62-
apiProvider: &OpenAIOfficialChatCompletionsAPIProvider{
63-
OpenAIModelID: "test",
63+
apiProvider: &OpenAICompatibleChatCompletionsAPIProvider{
64+
OpenAICompatibleModelID: "test",
6465
},
6566
ThinkingLevel: settings.LLMThinkingHigh,
6667
}
@@ -75,9 +76,38 @@ func TestCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter_buildJsonReques
7576
assert.Equal(t, "{\"model\":\"test\",\"stream\":false,\"messages\":[{\"role\":\"user\",\"content\":\"Hello, how are you?\"}],\"reasoning\":{\"effort\":\"high\"},\"response_format\":{\"type\":\"json_object\"}}", string(bodyBytes))
7677
}
7778

79+
func TestCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter_buildJsonRequestBody_JsonSchema(t *testing.T) {
80+
adapter := &CommonOpenAIChatCompletionsAPILargeLanguageModelAdapter{
81+
apiProvider: &OpenAICompatibleChatCompletionsAPIProvider{
82+
OpenAICompatibleModelID: "test",
83+
},
84+
ThinkingLevel: settings.LLMThinkingHigh,
85+
}
86+
87+
request := &data.LargeLanguageModelRequest{
88+
UserPrompt: []byte("Hello"),
89+
ResponseJsonObjectType: reflect.TypeOf(openAIResponsesTestResponse{}),
90+
}
91+
92+
bodyBytes, err := adapter.buildJsonRequestBody(core.NewNullContext(), 0, request, data.LARGE_LANGUAGE_MODEL_RESPONSE_FORMAT_JSON)
93+
assert.Nil(t, err)
94+
95+
var body map[string]any
96+
err = json.Unmarshal(bodyBytes, &body)
97+
assert.Nil(t, err)
98+
99+
responseFormat := body["response_format"].(map[string]any)
100+
responseType := responseFormat["type"]
101+
jsonScheme := responseFormat["json_schema"].(map[string]any)
102+
assert.Equal(t, "json_schema", responseType)
103+
assert.Equal(t, "response", jsonScheme["name"])
104+
assert.Equal(t, true, jsonScheme["strict"])
105+
assert.NotNil(t, jsonScheme["schema"])
106+
}
107+
78108
func TestCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter_ParseTextualResponse_ValidJsonResponse(t *testing.T) {
79109
adapter := &CommonOpenAIChatCompletionsAPILargeLanguageModelAdapter{
80-
apiProvider: &OpenAIOfficialChatCompletionsAPIProvider{},
110+
apiProvider: &OpenAICompatibleChatCompletionsAPIProvider{},
81111
}
82112

83113
response := `{
@@ -109,7 +139,7 @@ func TestCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter_ParseTextualRes
109139

110140
func TestCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter_ParseTextualResponse_EmptyResponse(t *testing.T) {
111141
adapter := &CommonOpenAIChatCompletionsAPILargeLanguageModelAdapter{
112-
apiProvider: &OpenAIOfficialChatCompletionsAPIProvider{},
142+
apiProvider: &OpenAICompatibleChatCompletionsAPIProvider{},
113143
}
114144

115145
response := `{
@@ -134,7 +164,7 @@ func TestCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter_ParseTextualRes
134164

135165
func TestCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter_ParseTextualResponse_EmptyChoices(t *testing.T) {
136166
adapter := &CommonOpenAIChatCompletionsAPILargeLanguageModelAdapter{
137-
apiProvider: &OpenAIOfficialChatCompletionsAPIProvider{},
167+
apiProvider: &OpenAICompatibleChatCompletionsAPIProvider{},
138168
}
139169

140170
response := `{
@@ -149,7 +179,7 @@ func TestCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter_ParseTextualRes
149179

150180
func TestCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter_ParseTextualResponse_NoChoiceContent(t *testing.T) {
151181
adapter := &CommonOpenAIChatCompletionsAPILargeLanguageModelAdapter{
152-
apiProvider: &OpenAIOfficialChatCompletionsAPIProvider{},
182+
apiProvider: &OpenAICompatibleChatCompletionsAPIProvider{},
153183
}
154184

155185
response := `{
@@ -172,7 +202,7 @@ func TestCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter_ParseTextualRes
172202

173203
func TestCommonOpenAIChatCompletionsAPILargeLanguageModelAdapter_ParseTextualResponse_InvalidJson(t *testing.T) {
174204
adapter := &CommonOpenAIChatCompletionsAPILargeLanguageModelAdapter{
175-
apiProvider: &OpenAIOfficialChatCompletionsAPIProvider{},
205+
apiProvider: &OpenAICompatibleChatCompletionsAPIProvider{},
176206
}
177207

178208
response := "error"

0 commit comments

Comments
 (0)