Skip to content

Commit 99bc2b6

Browse files
committed
Merge remote-tracking branch 'upstream/main'
2 parents 5fe6e6c + 140d621 commit 99bc2b6

8 files changed

Lines changed: 212 additions & 84 deletions

File tree

‎internal/api/handlers/management/auth_files.go‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1703,7 +1703,7 @@ func (h *Handler) RequestQwenToken(c *gin.Context) {
17031703
// Create token storage
17041704
tokenStorage := qwenAuth.CreateTokenStorage(tokenData)
17051705

1706-
tokenStorage.Email = fmt.Sprintf("qwen-%d", time.Now().UnixMilli())
1706+
tokenStorage.Email = fmt.Sprintf("%d", time.Now().UnixMilli())
17071707
record := &coreauth.Auth{
17081708
ID: fmt.Sprintf("qwen-%s.json", tokenStorage.Email),
17091709
Provider: "qwen",
@@ -1808,7 +1808,7 @@ func (h *Handler) RequestIFlowToken(c *gin.Context) {
18081808
tokenStorage := authSvc.CreateTokenStorage(tokenData)
18091809
identifier := strings.TrimSpace(tokenStorage.Email)
18101810
if identifier == "" {
1811-
identifier = fmt.Sprintf("iflow-%d", time.Now().UnixMilli())
1811+
identifier = fmt.Sprintf("%d", time.Now().UnixMilli())
18121812
tokenStorage.Email = identifier
18131813
}
18141814
record := &coreauth.Auth{
@@ -1893,15 +1893,17 @@ func (h *Handler) RequestIFlowCookieToken(c *gin.Context) {
18931893
fileName := iflowauth.SanitizeIFlowFileName(email)
18941894
if fileName == "" {
18951895
fileName = fmt.Sprintf("iflow-%d", time.Now().UnixMilli())
1896+
} else {
1897+
fileName = fmt.Sprintf("iflow-%s", fileName)
18961898
}
18971899

18981900
tokenStorage.Email = email
18991901
timestamp := time.Now().Unix()
19001902

19011903
record := &coreauth.Auth{
1902-
ID: fmt.Sprintf("iflow-%s-%d.json", fileName, timestamp),
1904+
ID: fmt.Sprintf("%s-%d.json", fileName, timestamp),
19031905
Provider: "iflow",
1904-
FileName: fmt.Sprintf("iflow-%s-%d.json", fileName, timestamp),
1906+
FileName: fmt.Sprintf("%s-%d.json", fileName, timestamp),
19051907
Storage: tokenStorage,
19061908
Metadata: map[string]any{
19071909
"email": email,

‎internal/logging/global_logger.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ var (
3030
type LogFormatter struct{}
3131

3232
// logFieldOrder defines the display order for common log fields.
33-
var logFieldOrder = []string{"provider", "model", "mode", "budget", "level", "original_value", "original_level", "min", "max", "clamped_to", "error"}
33+
var logFieldOrder = []string{"provider", "model", "mode", "budget", "level", "original_mode", "original_value", "min", "max", "clamped_to", "error"}
3434

3535
// Format renders a single log entry with custom formatting.
3636
func (m *LogFormatter) Format(entry *log.Entry) ([]byte, error) {

‎internal/thinking/apply.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ func ApplyThinking(body []byte, model string, fromFormat string, toFormat string
159159
}
160160

161161
// 5. Validate and normalize configuration
162-
validated, err := ValidateConfig(config, modelInfo, fromFormat, providerFormat)
162+
validated, err := ValidateConfig(config, modelInfo, fromFormat, providerFormat, suffixResult.HasSuffix)
163163
if err != nil {
164164
log.WithFields(log.Fields{
165165
"provider": providerFormat,

‎internal/thinking/validate.go‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ import (
1818
// - Clamps budget values to model's allowed range
1919
// - When converting Budget -> Level for level-only models, clamps the derived standard level to the nearest supported level
2020
// (special values none/auto are preserved)
21+
// - When config comes from a model suffix, strict budget validation is disabled (we clamp instead of error)
2122
//
2223
// Parameters:
2324
// - config: The thinking configuration to validate
2425
// - support: Model's ThinkingSupport properties (nil means no thinking support)
2526
// - fromFormat: Source provider format (used to determine strict validation rules)
2627
// - toFormat: Target provider format
28+
// - fromSuffix: Whether config was sourced from model suffix
2729
//
2830
// Returns:
2931
// - Normalized ThinkingConfig with clamped values
@@ -33,7 +35,7 @@ import (
3335
// - Budget-only model + Level config → Level converted to Budget
3436
// - Level-only model + Budget config → Budget converted to Level
3537
// - Hybrid model → preserve original format
36-
func ValidateConfig(config ThinkingConfig, modelInfo *registry.ModelInfo, fromFormat, toFormat string) (*ThinkingConfig, error) {
38+
func ValidateConfig(config ThinkingConfig, modelInfo *registry.ModelInfo, fromFormat, toFormat string, fromSuffix bool) (*ThinkingConfig, error) {
3739
fromFormat, toFormat = strings.ToLower(strings.TrimSpace(fromFormat)), strings.ToLower(strings.TrimSpace(toFormat))
3840
model := "unknown"
3941
support := (*registry.ThinkingSupport)(nil)
@@ -52,7 +54,7 @@ func ValidateConfig(config ThinkingConfig, modelInfo *registry.ModelInfo, fromFo
5254
}
5355

5456
allowClampUnsupported := isBudgetBasedProvider(fromFormat) && isLevelBasedProvider(toFormat)
55-
strictBudget := fromFormat != "" && isSameProviderFamily(fromFormat, toFormat)
57+
strictBudget := !fromSuffix && fromFormat != "" && isSameProviderFamily(fromFormat, toFormat)
5658
budgetDerivedFromLevel := false
5759

5860
capability := detectModelCapability(modelInfo)
@@ -238,7 +240,7 @@ func clampLevel(level ThinkingLevel, modelInfo *registry.ModelInfo, provider str
238240
log.WithFields(log.Fields{
239241
"provider": provider,
240242
"model": model,
241-
"original_level": string(level),
243+
"original_value": string(level),
242244
"clamped_to": string(clamped),
243245
}).Debug("thinking: level clamped |")
244246
return clamped

‎internal/translator/openai/openai/responses/openai_openai-responses_response.go‎

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import (
1212
"github.com/tidwall/sjson"
1313
)
1414

15+
type oaiToResponsesStateReasoning struct {
16+
ReasoningID string
17+
ReasoningData string
18+
}
1519
type oaiToResponsesState struct {
1620
Seq int
1721
ResponseID string
@@ -23,6 +27,7 @@ type oaiToResponsesState struct {
2327
// Per-output message text buffers by index
2428
MsgTextBuf map[int]*strings.Builder
2529
ReasoningBuf strings.Builder
30+
Reasonings []oaiToResponsesStateReasoning
2631
FuncArgsBuf map[int]*strings.Builder // index -> args
2732
FuncNames map[int]string // index -> name
2833
FuncCallIDs map[int]string // index -> call_id
@@ -63,6 +68,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
6368
MsgItemDone: make(map[int]bool),
6469
FuncArgsDone: make(map[int]bool),
6570
FuncItemDone: make(map[int]bool),
71+
Reasonings: make([]oaiToResponsesStateReasoning, 0),
6672
}
6773
}
6874
st := (*param).(*oaiToResponsesState)
@@ -157,6 +163,31 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
157163
st.Started = true
158164
}
159165

166+
stopReasoning := func(text string) {
167+
// Emit reasoning done events
168+
textDone := `{"type":"response.reasoning_summary_text.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"text":""}`
169+
textDone, _ = sjson.Set(textDone, "sequence_number", nextSeq())
170+
textDone, _ = sjson.Set(textDone, "item_id", st.ReasoningID)
171+
textDone, _ = sjson.Set(textDone, "output_index", st.ReasoningIndex)
172+
textDone, _ = sjson.Set(textDone, "text", text)
173+
out = append(out, emitRespEvent("response.reasoning_summary_text.done", textDone))
174+
partDone := `{"type":"response.reasoning_summary_part.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"part":{"type":"summary_text","text":""}}`
175+
partDone, _ = sjson.Set(partDone, "sequence_number", nextSeq())
176+
partDone, _ = sjson.Set(partDone, "item_id", st.ReasoningID)
177+
partDone, _ = sjson.Set(partDone, "output_index", st.ReasoningIndex)
178+
partDone, _ = sjson.Set(partDone, "part.text", text)
179+
out = append(out, emitRespEvent("response.reasoning_summary_part.done", partDone))
180+
outputItemDone := `{"type":"response.output_item.done","item":{"id":"","type":"reasoning","encrypted_content":"","summary":[{"type":"summary_text","text":""}]},"output_index":0,"sequence_number":0}`
181+
outputItemDone, _ = sjson.Set(outputItemDone, "sequence_number", nextSeq())
182+
outputItemDone, _ = sjson.Set(outputItemDone, "item.id", st.ReasoningID)
183+
outputItemDone, _ = sjson.Set(outputItemDone, "output_index", st.ReasoningIndex)
184+
outputItemDone, _ = sjson.Set(outputItemDone, "item.summary.text", text)
185+
out = append(out, emitRespEvent("response.output_item.done", outputItemDone))
186+
187+
st.Reasonings = append(st.Reasonings, oaiToResponsesStateReasoning{ReasoningID: st.ReasoningID, ReasoningData: text})
188+
st.ReasoningID = ""
189+
}
190+
160191
// choices[].delta content / tool_calls / reasoning_content
161192
if choices := root.Get("choices"); choices.Exists() && choices.IsArray() {
162193
choices.ForEach(func(_, choice gjson.Result) bool {
@@ -165,6 +196,10 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
165196
if delta.Exists() {
166197
if c := delta.Get("content"); c.Exists() && c.String() != "" {
167198
// Ensure the message item and its first content part are announced before any text deltas
199+
if st.ReasoningID != "" {
200+
stopReasoning(st.ReasoningBuf.String())
201+
st.ReasoningBuf.Reset()
202+
}
168203
if !st.MsgItemAdded[idx] {
169204
item := `{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"in_progress","content":[],"role":"assistant"}}`
170205
item, _ = sjson.Set(item, "sequence_number", nextSeq())
@@ -226,6 +261,10 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
226261

227262
// tool calls
228263
if tcs := delta.Get("tool_calls"); tcs.Exists() && tcs.IsArray() {
264+
if st.ReasoningID != "" {
265+
stopReasoning(st.ReasoningBuf.String())
266+
st.ReasoningBuf.Reset()
267+
}
229268
// Before emitting any function events, if a message is open for this index,
230269
// close its text/content to match Codex expected ordering.
231270
if st.MsgItemAdded[idx] && !st.MsgItemDone[idx] {
@@ -361,17 +400,8 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
361400
}
362401

363402
if st.ReasoningID != "" {
364-
// Emit reasoning done events
365-
textDone := `{"type":"response.reasoning_summary_text.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"text":""}`
366-
textDone, _ = sjson.Set(textDone, "sequence_number", nextSeq())
367-
textDone, _ = sjson.Set(textDone, "item_id", st.ReasoningID)
368-
textDone, _ = sjson.Set(textDone, "output_index", st.ReasoningIndex)
369-
out = append(out, emitRespEvent("response.reasoning_summary_text.done", textDone))
370-
partDone := `{"type":"response.reasoning_summary_part.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"part":{"type":"summary_text","text":""}}`
371-
partDone, _ = sjson.Set(partDone, "sequence_number", nextSeq())
372-
partDone, _ = sjson.Set(partDone, "item_id", st.ReasoningID)
373-
partDone, _ = sjson.Set(partDone, "output_index", st.ReasoningIndex)
374-
out = append(out, emitRespEvent("response.reasoning_summary_part.done", partDone))
403+
stopReasoning(st.ReasoningBuf.String())
404+
st.ReasoningBuf.Reset()
375405
}
376406

377407
// Emit function call done events for any active function calls
@@ -485,11 +515,13 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
485515
}
486516
// Build response.output using aggregated buffers
487517
outputsWrapper := `{"arr":[]}`
488-
if st.ReasoningBuf.Len() > 0 {
489-
item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`
490-
item, _ = sjson.Set(item, "id", st.ReasoningID)
491-
item, _ = sjson.Set(item, "summary.0.text", st.ReasoningBuf.String())
492-
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
518+
if len(st.Reasonings) > 0 {
519+
for _, r := range st.Reasonings {
520+
item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`
521+
item, _ = sjson.Set(item, "id", r.ReasoningID)
522+
item, _ = sjson.Set(item, "summary.0.text", r.ReasoningData)
523+
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
524+
}
493525
}
494526
// Append message items in ascending index order
495527
if len(st.MsgItemAdded) > 0 {

‎internal/util/gemini_schema.go‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ func CleanJSONSchemaForAntigravity(jsonStr string) string {
1919
// Phase 1: Convert and add hints
2020
jsonStr = convertRefsToHints(jsonStr)
2121
jsonStr = convertConstToEnum(jsonStr)
22+
jsonStr = convertEnumValuesToStrings(jsonStr)
2223
jsonStr = addEnumHints(jsonStr)
2324
jsonStr = addAdditionalPropertiesHints(jsonStr)
2425
jsonStr = moveConstraintsToDescription(jsonStr)
@@ -77,6 +78,33 @@ func convertConstToEnum(jsonStr string) string {
7778
return jsonStr
7879
}
7980

81+
// convertEnumValuesToStrings ensures all enum values are strings.
82+
// Gemini API requires enum values to be of type string, not numbers or booleans.
83+
func convertEnumValuesToStrings(jsonStr string) string {
84+
for _, p := range findPaths(jsonStr, "enum") {
85+
arr := gjson.Get(jsonStr, p)
86+
if !arr.IsArray() {
87+
continue
88+
}
89+
90+
var stringVals []string
91+
needsConversion := false
92+
for _, item := range arr.Array() {
93+
// Check if any value is not a string
94+
if item.Type != gjson.String {
95+
needsConversion = true
96+
}
97+
stringVals = append(stringVals, item.String())
98+
}
99+
100+
// Only update if we found non-string values
101+
if needsConversion {
102+
jsonStr, _ = sjson.Set(jsonStr, p, stringVals)
103+
}
104+
}
105+
return jsonStr
106+
}
107+
80108
func addEnumHints(jsonStr string) string {
81109
for _, p := range findPaths(jsonStr, "enum") {
82110
arr := gjson.Get(jsonStr, p)

‎internal/util/gemini_schema_test.go‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,3 +818,54 @@ func TestCleanJSONSchemaForAntigravity_MultipleFormats(t *testing.T) {
818818
t.Errorf("date-time format hint should be added, got: %s", result)
819819
}
820820
}
821+
822+
func TestCleanJSONSchemaForAntigravity_NumericEnumToString(t *testing.T) {
823+
// Gemini API requires enum values to be strings, not numbers
824+
input := `{
825+
"type": "object",
826+
"properties": {
827+
"priority": {"type": "integer", "enum": [0, 1, 2]},
828+
"level": {"type": "number", "enum": [1.5, 2.5, 3.5]},
829+
"status": {"type": "string", "enum": ["active", "inactive"]}
830+
}
831+
}`
832+
833+
result := CleanJSONSchemaForAntigravity(input)
834+
835+
// Numeric enum values should be converted to strings
836+
if strings.Contains(result, `"enum":[0,1,2]`) {
837+
t.Errorf("Integer enum values should be converted to strings, got: %s", result)
838+
}
839+
if strings.Contains(result, `"enum":[1.5,2.5,3.5]`) {
840+
t.Errorf("Float enum values should be converted to strings, got: %s", result)
841+
}
842+
// Should contain string versions
843+
if !strings.Contains(result, `"0"`) || !strings.Contains(result, `"1"`) || !strings.Contains(result, `"2"`) {
844+
t.Errorf("Integer enum values should be converted to string format, got: %s", result)
845+
}
846+
// String enum values should remain unchanged
847+
if !strings.Contains(result, `"active"`) || !strings.Contains(result, `"inactive"`) {
848+
t.Errorf("String enum values should remain unchanged, got: %s", result)
849+
}
850+
}
851+
852+
func TestCleanJSONSchemaForAntigravity_BooleanEnumToString(t *testing.T) {
853+
// Boolean enum values should also be converted to strings
854+
input := `{
855+
"type": "object",
856+
"properties": {
857+
"enabled": {"type": "boolean", "enum": [true, false]}
858+
}
859+
}`
860+
861+
result := CleanJSONSchemaForAntigravity(input)
862+
863+
// Boolean enum values should be converted to strings
864+
if strings.Contains(result, `"enum":[true,false]`) {
865+
t.Errorf("Boolean enum values should be converted to strings, got: %s", result)
866+
}
867+
// Should contain string versions "true" and "false"
868+
if !strings.Contains(result, `"true"`) || !strings.Contains(result, `"false"`) {
869+
t.Errorf("Boolean enum values should be converted to string format, got: %s", result)
870+
}
871+
}

0 commit comments

Comments
 (0)