openai_messages_todo_guard.go 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. package service
  2. import (
  3. "encoding/json"
  4. "strings"
  5. "github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
  6. )
  7. const (
  8. openAICompatClaudeCodeTodoGuardMarker = "<sub2api-claude-code-todo-guard>"
  9. openAICompatClaudeCodeTodoGuardText = openAICompatClaudeCodeTodoGuardMarker + "\nWhen using Claude Code todo or task tracking tools, keep the visible task list consistent. Do not send final or summary text while any item remains in_progress. Before finishing, asking the user to choose, or reporting a blocker, update the todo list so completed work is completed and deferred work is pending/open; leave an item in_progress only when active work will continue in the same turn.\n</sub2api-claude-code-todo-guard>"
  10. )
  11. func appendOpenAICompatClaudeCodeTodoGuard(req *apicompat.ResponsesRequest) bool {
  12. if req == nil || len(req.Input) == 0 {
  13. return false
  14. }
  15. var items []apicompat.ResponsesInputItem
  16. if err := json.Unmarshal(req.Input, &items); err != nil {
  17. return false
  18. }
  19. if len(items) == 0 || responsesInputItemsContainText(items, openAICompatClaudeCodeTodoGuardMarker) {
  20. return false
  21. }
  22. content, err := json.Marshal([]apicompat.ResponsesContentPart{{
  23. Type: "input_text",
  24. Text: openAICompatClaudeCodeTodoGuardText,
  25. }})
  26. if err != nil {
  27. return false
  28. }
  29. guard := apicompat.ResponsesInputItem{
  30. Type: "message",
  31. Role: "developer",
  32. Content: content,
  33. }
  34. insertAt := 0
  35. for insertAt < len(items) && items[insertAt].Type == "message" && items[insertAt].Role == "developer" {
  36. insertAt++
  37. }
  38. items = append(items, apicompat.ResponsesInputItem{})
  39. copy(items[insertAt+1:], items[insertAt:])
  40. items[insertAt] = guard
  41. input, err := json.Marshal(items)
  42. if err != nil {
  43. return false
  44. }
  45. req.Input = input
  46. return true
  47. }
  48. func appendOpenAICompatClaudeCodeTodoGuardToRequestBody(reqBody map[string]any) bool {
  49. if reqBody == nil {
  50. return false
  51. }
  52. input, ok := reqBody["input"].([]any)
  53. if !ok || len(input) == 0 || inputContainsText(input, openAICompatClaudeCodeTodoGuardMarker) {
  54. return false
  55. }
  56. guard := map[string]any{
  57. "type": "message",
  58. "role": "developer",
  59. "content": []any{
  60. map[string]any{
  61. "type": "input_text",
  62. "text": openAICompatClaudeCodeTodoGuardText,
  63. },
  64. },
  65. }
  66. insertAt := 0
  67. for insertAt < len(input) {
  68. item, ok := input[insertAt].(map[string]any)
  69. if !ok || strings.TrimSpace(firstNonEmptyString(item["type"])) != "message" || strings.TrimSpace(firstNonEmptyString(item["role"])) != "developer" {
  70. break
  71. }
  72. insertAt++
  73. }
  74. input = append(input, nil)
  75. copy(input[insertAt+1:], input[insertAt:])
  76. input[insertAt] = guard
  77. reqBody["input"] = input
  78. return true
  79. }
  80. func responsesInputItemsContainText(items []apicompat.ResponsesInputItem, needle string) bool {
  81. needle = strings.TrimSpace(needle)
  82. if needle == "" {
  83. return false
  84. }
  85. for _, item := range items {
  86. if strings.Contains(string(item.Content), needle) {
  87. return true
  88. }
  89. }
  90. return false
  91. }
  92. func inputContainsText(input []any, needle string) bool {
  93. needle = strings.TrimSpace(needle)
  94. if needle == "" {
  95. return false
  96. }
  97. for _, item := range input {
  98. b, err := json.Marshal(item)
  99. if err == nil && strings.Contains(string(b), needle) {
  100. return true
  101. }
  102. }
  103. return false
  104. }