Codex commited on
Commit
8f75a35
·
1 Parent(s): 482b81c

Complete Luna Responses output

Browse files
Files changed (1) hide show
  1. codex-luna-compat.patch +113 -5
codex-luna-compat.patch CHANGED
@@ -139,10 +139,10 @@ index 697cc96..299247f 100644
139
  if streamResponse.Response != nil {
140
  diff --git a/relay/channel/openai/responses_codex_compat.go b/relay/channel/openai/responses_codex_compat.go
141
  new file mode 100644
142
- index 0000000..cb18b81
143
  --- /dev/null
144
  +++ b/relay/channel/openai/responses_codex_compat.go
145
- @@ -0,0 +1,170 @@
146
  +package openai
147
  +
148
  +import (
@@ -155,7 +155,10 @@ index 0000000..cb18b81
155
  + "github.com/tidwall/sjson"
156
  +)
157
  +
158
- +const codexLunaOriginModel = "gpt-5.6-luna"
 
 
 
159
  +
160
  +type codexResponsesStreamState struct {
161
  + nextOutputIndex int
@@ -181,6 +184,13 @@ index 0000000..cb18b81
181
  + if s == nil || event == nil || data == "" {
182
  + return nil, data, nil, nil
183
  + }
 
 
 
 
 
 
 
184
  +
185
  + itemID := event.ItemID
186
  + if itemID == "" && event.Item != nil {
@@ -270,6 +280,27 @@ index 0000000..cb18b81
270
  + before = append(before, codexContentPartEvent("response.content_part.done", itemID, outputIndex, text))
271
  + s.contentDoneBy[itemID] = true
272
  + }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  + }
274
  +
275
  + return before, data, after, nil
@@ -287,6 +318,49 @@ index 0000000..cb18b81
287
  + return updated, nil
288
  +}
289
  +
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  +func codexContentPartEvent(eventType, itemID string, outputIndex *int, text string) dto.ResponsesStreamResponse {
291
  + return dto.ResponsesStreamResponse{
292
  + Type: eventType,
@@ -315,10 +389,10 @@ index 0000000..cb18b81
315
  +}
316
  diff --git a/relay/channel/openai/responses_codex_compat_test.go b/relay/channel/openai/responses_codex_compat_test.go
317
  new file mode 100644
318
- index 0000000..3cb1082
319
  --- /dev/null
320
  +++ b/relay/channel/openai/responses_codex_compat_test.go
321
- @@ -0,0 +1,57 @@
322
  +package openai
323
  +
324
  +import (
@@ -376,6 +450,40 @@ index 0000000..3cb1082
376
  + assert.Equal(t, 0, *event.OutputIndex)
377
  + assert.Equal(t, 0, *event.ContentIndex)
378
  +}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  diff --git a/relay/responses_handler.go b/relay/responses_handler.go
380
  index 5fa23d0..9c74145 100644
381
  --- a/relay/responses_handler.go
 
139
  if streamResponse.Response != nil {
140
  diff --git a/relay/channel/openai/responses_codex_compat.go b/relay/channel/openai/responses_codex_compat.go
141
  new file mode 100644
142
+ index 0000000..00aa2f6
143
  --- /dev/null
144
  +++ b/relay/channel/openai/responses_codex_compat.go
145
+ @@ -0,0 +1,244 @@
146
  +package openai
147
  +
148
  +import (
 
155
  + "github.com/tidwall/sjson"
156
  +)
157
  +
158
+ +const (
159
+ + codexLunaOriginModel = "gpt-5.6-luna"
160
+ + codexLunaUpstreamModel = "gemini-3.7-flash-high"
161
+ +)
162
  +
163
  +type codexResponsesStreamState struct {
164
  + nextOutputIndex int
 
184
  + if s == nil || event == nil || data == "" {
185
  + return nil, data, nil, nil
186
  + }
187
+ + if event.Response != nil {
188
+ + event.Response.Model = codexLunaUpstreamModel
189
+ + data, err = sjson.Set(data, "response.model", codexLunaUpstreamModel)
190
+ + if err != nil {
191
+ + return nil, "", nil, fmt.Errorf("set responses model: %w", err)
192
+ + }
193
+ + }
194
  +
195
  + itemID := event.ItemID
196
  + if itemID == "" && event.Item != nil {
 
280
  + before = append(before, codexContentPartEvent("response.content_part.done", itemID, outputIndex, text))
281
  + s.contentDoneBy[itemID] = true
282
  + }
283
+ + if event.Item != nil && event.Item.Type == "message" {
284
+ + text := ""
285
+ + if builder := s.textByID[itemID]; builder != nil {
286
+ + text = builder.String()
287
+ + }
288
+ + data, err = ensureCodexMessageOutputItem(data, text)
289
+ + if err != nil {
290
+ + return nil, "", nil, err
291
+ + }
292
+ + }
293
+ + case "response.completed", "response.done":
294
+ + if event.Response != nil && len(event.Response.Output) == 0 && itemID != "" {
295
+ + text := ""
296
+ + if builder := s.textByID[itemID]; builder != nil {
297
+ + text = builder.String()
298
+ + }
299
+ + data, err = ensureCodexCompletedOutput(data, itemID, text)
300
+ + if err != nil {
301
+ + return nil, "", nil, err
302
+ + }
303
+ + }
304
  + }
305
  +
306
  + return before, data, after, nil
 
318
  + return updated, nil
319
  +}
320
  +
321
+ +func ensureCodexMessageOutputItem(data, text string) (string, error) {
322
+ + if gjson.Get(data, "item.content").Exists() && gjson.Get(data, "item.content").IsArray() && len(gjson.Get(data, "item.content").Array()) > 0 {
323
+ + return data, nil
324
+ + }
325
+ + data, err := sjson.Set(data, "item.role", "assistant")
326
+ + if err != nil {
327
+ + return "", fmt.Errorf("set responses output item role: %w", err)
328
+ + }
329
+ + content := []dto.ResponsesOutputContent{{Type: "output_text", Text: text, Annotations: []interface{}{}}}
330
+ + contentData, err := common.Marshal(content)
331
+ + if err != nil {
332
+ + return "", fmt.Errorf("marshal responses output item content: %w", err)
333
+ + }
334
+ + data, err = sjson.SetRaw(data, "item.content", string(contentData))
335
+ + if err != nil {
336
+ + return "", fmt.Errorf("set responses output item content: %w", err)
337
+ + }
338
+ + return data, nil
339
+ +}
340
+ +
341
+ +func ensureCodexCompletedOutput(data, itemID, text string) (string, error) {
342
+ + output := []dto.ResponsesOutput{{
343
+ + Type: "message",
344
+ + ID: itemID,
345
+ + Status: "completed",
346
+ + Role: "assistant",
347
+ + Content: []dto.ResponsesOutputContent{{
348
+ + Type: "output_text",
349
+ + Text: text,
350
+ + Annotations: []interface{}{},
351
+ + }},
352
+ + }}
353
+ + outputData, err := common.Marshal(output)
354
+ + if err != nil {
355
+ + return "", fmt.Errorf("marshal responses output: %w", err)
356
+ + }
357
+ + updated, err := sjson.SetRaw(data, "response.output", string(outputData))
358
+ + if err != nil {
359
+ + return "", fmt.Errorf("set responses output: %w", err)
360
+ + }
361
+ + return updated, nil
362
+ +}
363
+ +
364
  +func codexContentPartEvent(eventType, itemID string, outputIndex *int, text string) dto.ResponsesStreamResponse {
365
  + return dto.ResponsesStreamResponse{
366
  + Type: eventType,
 
389
  +}
390
  diff --git a/relay/channel/openai/responses_codex_compat_test.go b/relay/channel/openai/responses_codex_compat_test.go
391
  new file mode 100644
392
+ index 0000000..aa59f50
393
  --- /dev/null
394
  +++ b/relay/channel/openai/responses_codex_compat_test.go
395
+ @@ -0,0 +1,91 @@
396
  +package openai
397
  +
398
  +import (
 
450
  + assert.Equal(t, 0, *event.OutputIndex)
451
  + assert.Equal(t, 0, *event.ContentIndex)
452
  +}
453
+ +
454
+ +func TestCodexResponsesStreamStateCompletesMessageOutput(t *testing.T) {
455
+ + state := newCodexResponsesStreamState()
456
+ +
457
+ + added := dto.ResponsesStreamResponse{
458
+ + Type: "response.output_item.added",
459
+ + Item: &dto.ResponsesOutput{ID: "item_1", Type: "message", Role: "assistant"},
460
+ + }
461
+ + _, _, _, err := state.normalize(&added, `{"type":"response.output_item.added","item":{"id":"item_1","type":"message"}}`)
462
+ + require.NoError(t, err)
463
+ +
464
+ + delta := dto.ResponsesStreamResponse{Type: "response.output_text.delta", ItemID: "item_1", Delta: "OK"}
465
+ + _, _, _, err = state.normalize(&delta, `{"type":"response.output_text.delta","item_id":"item_1","delta":"OK"}`)
466
+ + require.NoError(t, err)
467
+ +
468
+ + done := dto.ResponsesStreamResponse{
469
+ + Type: dto.ResponsesOutputTypeItemDone,
470
+ + ItemID: "item_1",
471
+ + Item: &dto.ResponsesOutput{ID: "item_1", Type: "message", Status: "completed"},
472
+ + }
473
+ + _, doneData, _, err := state.normalize(&done, `{"type":"response.output_item.done","item_id":"item_1","item":{"id":"item_1","type":"message","status":"completed"}}`)
474
+ + require.NoError(t, err)
475
+ + assert.Equal(t, "OK", gjson.Get(doneData, "item.content.0.text").String())
476
+ +
477
+ + completed := dto.ResponsesStreamResponse{
478
+ + Type: "response.completed",
479
+ + ItemID: "item_1",
480
+ + Response: &dto.OpenAIResponsesResponse{Model: "gemini-3.7-flash", Output: []dto.ResponsesOutput{}},
481
+ + }
482
+ + _, completedData, _, err := state.normalize(&completed, `{"type":"response.completed","response":{"model":"gemini-3.7-flash","output":[]}}`)
483
+ + require.NoError(t, err)
484
+ + assert.Equal(t, codexLunaUpstreamModel, gjson.Get(completedData, "response.model").String())
485
+ + assert.Equal(t, "OK", gjson.Get(completedData, "response.output.0.content.0.text").String())
486
+ +}
487
  diff --git a/relay/responses_handler.go b/relay/responses_handler.go
488
  index 5fa23d0..9c74145 100644
489
  --- a/relay/responses_handler.go