Skip to content

Commit b598e93

Browse files
authored
Merge branch 'master' into master
2 parents 6396904 + f7d6ece commit b598e93

31 files changed

+2047
-168
lines changed

.codecov.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
coverage:
2+
ignore:
3+
- "examples/**"
4+
- "internal/test/**"

.github/workflows/pr.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
go-version: '1.24'
1717
- name: Run vet
1818
run: |
19-
go vet .
19+
go vet -stdversion ./...
2020
- name: Run golangci-lint
2121
uses: golangci/golangci-lint-action@v7
2222
with:

chat.go

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"encoding/json"
66
"errors"
77
"net/http"
8+
9+
"github.com/sashabaranov/go-openai/jsonschema"
810
)
911

1012
// Chat message role defined by the OpenAI API.
@@ -221,13 +223,49 @@ type ChatCompletionResponseFormatJSONSchema struct {
221223
Strict bool `json:"strict"`
222224
}
223225

226+
func (r *ChatCompletionResponseFormatJSONSchema) UnmarshalJSON(data []byte) error {
227+
type rawJSONSchema struct {
228+
Name string `json:"name"`
229+
Description string `json:"description,omitempty"`
230+
Schema json.RawMessage `json:"schema"`
231+
Strict bool `json:"strict"`
232+
}
233+
var raw rawJSONSchema
234+
if err := json.Unmarshal(data, &raw); err != nil {
235+
return err
236+
}
237+
r.Name = raw.Name
238+
r.Description = raw.Description
239+
r.Strict = raw.Strict
240+
if len(raw.Schema) > 0 && string(raw.Schema) != "null" {
241+
var d jsonschema.Definition
242+
err := json.Unmarshal(raw.Schema, &d)
243+
if err != nil {
244+
return err
245+
}
246+
r.Schema = &d
247+
}
248+
return nil
249+
}
250+
251+
// ChatCompletionRequestExtensions contains third-party OpenAI API extensions
252+
// (e.g., vendor-specific implementations like vLLM).
253+
type ChatCompletionRequestExtensions struct {
254+
// GuidedChoice is a vLLM-specific extension that restricts the model's output
255+
// to one of the predefined string choices provided in this field. This feature
256+
// is used to constrain the model's responses to a controlled set of options,
257+
// ensuring predictable and consistent outputs in scenarios where specific
258+
// choices are required.
259+
GuidedChoice []string `json:"guided_choice,omitempty"`
260+
}
261+
224262
// ChatCompletionRequest represents a request structure for chat completion API.
225263
type ChatCompletionRequest struct {
226264
Model string `json:"model"`
227265
Messages []ChatCompletionMessage `json:"messages"`
228266
// MaxTokens The maximum number of tokens that can be generated in the chat completion.
229267
// This value can be used to control costs for text generated via API.
230-
// This value is now deprecated in favor of max_completion_tokens, and is not compatible with o1 series models.
268+
// Deprecated: use MaxCompletionTokens. Not compatible with o1-series models.
231269
// refs: https://platform.openai.com/docs/api-reference/chat/create#chat-create-max_tokens
232270
MaxTokens int `json:"max_tokens,omitempty"`
233271
// MaxCompletionTokens An upper bound for the number of tokens that can be generated for a completion,
@@ -282,12 +320,22 @@ type ChatCompletionRequest struct {
282320
ChatTemplateKwargs map[string]any `json:"chat_template_kwargs,omitempty"`
283321
ExtraHeaders ExtraHeaders `json:"extra_headers,omitempty"`
284322
Thinking *Thinking `json:"thinking,omitempty"`
323+
// Specifies the latency tier to use for processing the request.
324+
ServiceTier ServiceTier `json:"service_tier,omitempty"`
325+
// Embedded struct for non-OpenAI extensions
326+
ChatCompletionRequestExtensions
327+
TranslationOptions *TranslationOptions `json:"translation_options,omitempty"`
285328
}
286329

287330
type Thinking struct {
288331
Type string `json:"type"`
289332
}
290333

334+
type TranslationOptions struct {
335+
SourceLang string `json:"source_lang"`
336+
TargetLang string `json:"target_lang"`
337+
}
338+
291339
type StreamOptions struct {
292340
// If set, an additional chunk will be streamed before the data: [DONE] message.
293341
// The usage field on this chunk shows the token usage statistics for the entire request,
@@ -369,6 +417,15 @@ const (
369417
FinishReasonNull FinishReason = "null"
370418
)
371419

420+
type ServiceTier string
421+
422+
const (
423+
ServiceTierAuto ServiceTier = "auto"
424+
ServiceTierDefault ServiceTier = "default"
425+
ServiceTierFlex ServiceTier = "flex"
426+
ServiceTierPriority ServiceTier = "priority"
427+
)
428+
372429
func (r FinishReason) MarshalJSON() ([]byte, error) {
373430
if r == FinishReasonNull || r == "" {
374431
return []byte("null"), nil
@@ -401,6 +458,7 @@ type ChatCompletionResponse struct {
401458
Usage Usage `json:"usage"`
402459
SystemFingerprint string `json:"system_fingerprint"`
403460
PromptFilterResults []PromptFilterResult `json:"prompt_filter_results,omitempty"`
461+
ServiceTier ServiceTier `json:"service_tier,omitempty"`
404462

405463
httpHeader
406464
}

chat_test.go

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,126 @@ func TestO3ModelsChatCompletionsBetaLimitations(t *testing.T) {
331331
}
332332
}
333333

334+
func TestGPT5ModelsChatCompletionsBetaLimitations(t *testing.T) {
335+
tests := []struct {
336+
name string
337+
in openai.ChatCompletionRequest
338+
expectedError error
339+
}{
340+
{
341+
name: "log_probs_unsupported",
342+
in: openai.ChatCompletionRequest{
343+
MaxCompletionTokens: 1000,
344+
LogProbs: true,
345+
Model: openai.GPT5,
346+
},
347+
expectedError: openai.ErrReasoningModelLimitationsLogprobs,
348+
},
349+
{
350+
name: "set_temperature_unsupported",
351+
in: openai.ChatCompletionRequest{
352+
MaxCompletionTokens: 1000,
353+
Model: openai.GPT5Mini,
354+
Messages: []openai.ChatCompletionMessage{
355+
{
356+
Role: openai.ChatMessageRoleUser,
357+
},
358+
{
359+
Role: openai.ChatMessageRoleAssistant,
360+
},
361+
},
362+
Temperature: float32(2),
363+
},
364+
expectedError: openai.ErrReasoningModelLimitationsOther,
365+
},
366+
{
367+
name: "set_top_unsupported",
368+
in: openai.ChatCompletionRequest{
369+
MaxCompletionTokens: 1000,
370+
Model: openai.GPT5Nano,
371+
Messages: []openai.ChatCompletionMessage{
372+
{
373+
Role: openai.ChatMessageRoleUser,
374+
},
375+
{
376+
Role: openai.ChatMessageRoleAssistant,
377+
},
378+
},
379+
Temperature: float32(1),
380+
TopP: float32(0.1),
381+
},
382+
expectedError: openai.ErrReasoningModelLimitationsOther,
383+
},
384+
{
385+
name: "set_n_unsupported",
386+
in: openai.ChatCompletionRequest{
387+
MaxCompletionTokens: 1000,
388+
Model: openai.GPT5ChatLatest,
389+
Messages: []openai.ChatCompletionMessage{
390+
{
391+
Role: openai.ChatMessageRoleUser,
392+
},
393+
{
394+
Role: openai.ChatMessageRoleAssistant,
395+
},
396+
},
397+
Temperature: float32(1),
398+
TopP: float32(1),
399+
N: 2,
400+
},
401+
expectedError: openai.ErrReasoningModelLimitationsOther,
402+
},
403+
{
404+
name: "set_presence_penalty_unsupported",
405+
in: openai.ChatCompletionRequest{
406+
MaxCompletionTokens: 1000,
407+
Model: openai.GPT5,
408+
Messages: []openai.ChatCompletionMessage{
409+
{
410+
Role: openai.ChatMessageRoleUser,
411+
},
412+
{
413+
Role: openai.ChatMessageRoleAssistant,
414+
},
415+
},
416+
PresencePenalty: float32(0.1),
417+
},
418+
expectedError: openai.ErrReasoningModelLimitationsOther,
419+
},
420+
{
421+
name: "set_frequency_penalty_unsupported",
422+
in: openai.ChatCompletionRequest{
423+
MaxCompletionTokens: 1000,
424+
Model: openai.GPT5Mini,
425+
Messages: []openai.ChatCompletionMessage{
426+
{
427+
Role: openai.ChatMessageRoleUser,
428+
},
429+
{
430+
Role: openai.ChatMessageRoleAssistant,
431+
},
432+
},
433+
FrequencyPenalty: float32(0.1),
434+
},
435+
expectedError: openai.ErrReasoningModelLimitationsOther,
436+
},
437+
}
438+
439+
for _, tt := range tests {
440+
t.Run(tt.name, func(t *testing.T) {
441+
config := openai.DefaultConfig("whatever")
442+
config.BaseURL = "http://localhost/v1"
443+
client := openai.NewClientWithConfig(config)
444+
ctx := context.Background()
445+
446+
_, err := client.CreateChatCompletion(ctx, tt.in)
447+
checks.HasError(t, err)
448+
msg := fmt.Sprintf("CreateChatCompletion should return wrong model error, returned: %s", err)
449+
checks.ErrorIs(t, err, tt.expectedError, msg)
450+
})
451+
}
452+
}
453+
334454
func TestChatRequestOmitEmpty(t *testing.T) {
335455
data, err := json.Marshal(openai.ChatCompletionRequest{
336456
// We set model b/c it's required, so omitempty doesn't make sense
@@ -946,3 +1066,142 @@ func TestFinishReason(t *testing.T) {
9461066
}
9471067
}
9481068
}
1069+
1070+
func TestChatCompletionResponseFormatJSONSchema_UnmarshalJSON(t *testing.T) {
1071+
type args struct {
1072+
data []byte
1073+
}
1074+
tests := []struct {
1075+
name string
1076+
args args
1077+
wantErr bool
1078+
}{
1079+
{
1080+
"",
1081+
args{
1082+
data: []byte(`{
1083+
"name": "math_response",
1084+
"strict": true,
1085+
"schema": {
1086+
"type": "object",
1087+
"properties": {
1088+
"steps": {
1089+
"type": "array",
1090+
"items": {
1091+
"type": "object",
1092+
"properties": {
1093+
"explanation": { "type": "string" },
1094+
"output": { "type": "string" }
1095+
},
1096+
"required": ["explanation","output"],
1097+
"additionalProperties": false
1098+
}
1099+
},
1100+
"final_answer": { "type": "string" }
1101+
},
1102+
"required": ["steps","final_answer"],
1103+
"additionalProperties": false
1104+
}
1105+
}`),
1106+
},
1107+
false,
1108+
},
1109+
{
1110+
"",
1111+
args{
1112+
data: []byte(`{
1113+
"name": "math_response",
1114+
"strict": true,
1115+
"schema": null
1116+
}`),
1117+
},
1118+
false,
1119+
},
1120+
{
1121+
"",
1122+
args{
1123+
data: []byte(`[123,456]`),
1124+
},
1125+
true,
1126+
},
1127+
{
1128+
"",
1129+
args{
1130+
data: []byte(`{
1131+
"name": "math_response",
1132+
"strict": true,
1133+
"schema": 123456
1134+
}`),
1135+
},
1136+
true,
1137+
},
1138+
}
1139+
for _, tt := range tests {
1140+
t.Run(tt.name, func(t *testing.T) {
1141+
var r openai.ChatCompletionResponseFormatJSONSchema
1142+
err := r.UnmarshalJSON(tt.args.data)
1143+
if (err != nil) != tt.wantErr {
1144+
t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
1145+
}
1146+
})
1147+
}
1148+
}
1149+
1150+
func TestChatCompletionRequest_UnmarshalJSON(t *testing.T) {
1151+
type args struct {
1152+
bs []byte
1153+
}
1154+
tests := []struct {
1155+
name string
1156+
args args
1157+
wantErr bool
1158+
}{
1159+
{
1160+
"",
1161+
args{bs: []byte(`{
1162+
"model": "llama3-1b",
1163+
"messages": [
1164+
{ "role": "system", "content": "You are a helpful math tutor." },
1165+
{ "role": "user", "content": "solve 8x + 31 = 2" }
1166+
],
1167+
"response_format": {
1168+
"type": "json_schema",
1169+
"json_schema": {
1170+
"name": "math_response",
1171+
"strict": true,
1172+
"schema": {
1173+
"type": "object",
1174+
"properties": {
1175+
"steps": {
1176+
"type": "array",
1177+
"items": {
1178+
"type": "object",
1179+
"properties": {
1180+
"explanation": { "type": "string" },
1181+
"output": { "type": "string" }
1182+
},
1183+
"required": ["explanation","output"],
1184+
"additionalProperties": false
1185+
}
1186+
},
1187+
"final_answer": { "type": "string" }
1188+
},
1189+
"required": ["steps","final_answer"],
1190+
"additionalProperties": false
1191+
}
1192+
}
1193+
}
1194+
}`)},
1195+
false,
1196+
},
1197+
}
1198+
for _, tt := range tests {
1199+
t.Run(tt.name, func(t *testing.T) {
1200+
var m openai.ChatCompletionRequest
1201+
err := json.Unmarshal(tt.args.bs, &m)
1202+
if err != nil {
1203+
t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
1204+
}
1205+
})
1206+
}
1207+
}

0 commit comments

Comments
 (0)