@@ -66,16 +66,23 @@ type ModelSession struct {
6666 ResponseID string `json:"responseId"`
6767}
6868
69+ // InputMessage represents a single message in the Responses API input array.
70+ // Role can be "developer" (instructions/reminders) or "user" (player actions).
71+ type InputMessage struct {
72+ Role string `json:"role"`
73+ Content string `json:"content"`
74+ }
75+
6976// ResponsesAPIRequest is the request body for the Responses API
7077type ResponsesAPIRequest struct {
71- Model string `json:"model"`
72- Input string `json:"input"`
73- Instructions string `json:"instructions,omitempty"`
74- PreviousResponseID string `json:"previous_response_id,omitempty"`
75- Store bool `json:"store"`
76- Stream bool `json:"stream,omitempty"`
77- MaxOutputTokens int `json:"max_output_tokens,omitempty"`
78- Text * TextConfig `json:"text,omitempty"`
78+ Model string `json:"model"`
79+ Input [] InputMessage `json:"input"`
80+ Instructions string `json:"instructions,omitempty"`
81+ PreviousResponseID string `json:"previous_response_id,omitempty"`
82+ Store bool `json:"store"`
83+ Stream bool `json:"stream,omitempty"`
84+ MaxOutputTokens int `json:"max_output_tokens,omitempty"`
85+ Text * TextConfig `json:"text,omitempty"`
7986}
8087
8188type TextConfig struct {
@@ -193,10 +200,10 @@ func (p *OpenAiPlatform) ExecuteAction(ctx context.Context, session *obj.GameSes
193200 // Serialize the player action as JSON input (minimal AI-facing structure)
194201 actionInput := action .ToAiJSON ()
195202
196- // Build the request
203+ // Build the request (Input is set below based on action type)
197204 req := ResponsesAPIRequest {
198205 Model : model ,
199- Input : actionInput ,
206+ Input : [] InputMessage {{ Role : "user" , Content : actionInput }} ,
200207 Store : true ,
201208 MaxOutputTokens : 5000 ,
202209 Text : & TextConfig {
@@ -212,9 +219,17 @@ func (p *OpenAiPlatform) ExecuteAction(ctx context.Context, session *obj.GameSes
212219 // System messages become instructions, otherwise use previous_response_id for continuity
213220 if action .Type == obj .GameSessionMessageTypeSystem {
214221 req .Instructions = action .Message
215- req .Input = templates .PromptMessageStart
222+ req .Input = [] InputMessage {{ Role : "developer" , Content : templates .PromptMessageStart }}
216223 } else if modelSession .ResponseID != "" {
217224 req .PreviousResponseID = modelSession .ResponseID
225+ // Inject developer reminder with every player action to reinforce brevity
226+ req .Input = []InputMessage {
227+ {Role : "developer" , Content : templates .ReminderExecuteAction },
228+ {Role : "user" , Content : actionInput },
229+ }
230+ // Set debug prompt showing full input sent to the AI
231+ response .PromptStatusUpdate = functional .Ptr (
232+ "[developer] " + templates .ReminderExecuteAction + "\n \n [user] " + actionInput )
218233 }
219234
220235 responseStream := stream .Get ().Lookup (response .ID )
@@ -273,21 +288,13 @@ func (p *OpenAiPlatform) ExecuteAction(ctx context.Context, session *obj.GameSes
273288
274289 response .ResponseRaw = & responseText
275290
276- // Parse the AI response (uses flat status map) and convert to internal format
277- log .Debug ("parsing OpenAI response" , "response_length" , len (responseText ), "response_text" , responseText )
278- var aiResp obj.GameSessionMessageAi
279- if err := json .Unmarshal ([]byte (responseText ), & aiResp ); err != nil {
291+ // Parse the AI response and convert to internal format
292+ log .Debug ("parsing AI response" , "response_length" , len (responseText ), "response_text" , responseText )
293+ if err := status .ParseGameResponse (responseText , session .StatusFields , action .StatusFields , response ); err != nil {
280294 log .Error ("failed to parse game response" , "error" , err , "response_text" , responseText )
281- return usage , fmt . Errorf ( "failed to parse game response: %w" , err )
295+ return usage , err
282296 }
283297
284- // Convert flat status map back to ordered []StatusField using session's field definitions.
285- // Pass action's current status as fallback in case the AI omits a field.
286- fieldNames := status .FieldNames (session .StatusFields )
287- response .Message = aiResp .Message
288- response .StatusFields = status .MapToFields (aiResp .Status , fieldNames , status .FieldsToMap (action .StatusFields ))
289- response .ImagePrompt = aiResp .ImagePrompt
290-
291298 // Update model session with new response ID
292299 modelSession .ResponseID = apiResponse .ID
293300 response .URLAnalytics = functional .Ptr ("https://platform.openai.com/logs/" + apiResponse .ID )
@@ -348,10 +355,13 @@ func (p *OpenAiPlatform) ExpandStory(ctx context.Context, session *obj.GameSessi
348355 }
349356
350357 // Build streaming request - plain text, no JSON schema
358+ // Use developer role for the narration instruction (it's a directive, not player input)
351359 model := p .ResolveModel (session .AiModel )
352360 req := ResponsesAPIRequest {
353- Model : model ,
354- Input : templates .PromptNarratePlotOutline ,
361+ Model : model ,
362+ Input : []InputMessage {
363+ {Role : "developer" , Content : templates .PromptNarratePlotOutline },
364+ },
355365 Store : true ,
356366 Stream : true ,
357367 MaxOutputTokens : 5000 ,
@@ -509,9 +519,9 @@ func callImageGenerationAPI(ctx context.Context, apiKey string, prompt string, s
509519
510520 // Note: style parameter is only supported for dall-e-3, not gpt-image-1
511521 // For gpt-image-1, we include the style in the prompt instead
512- fullPrompt := prompt
522+ fullPrompt := prompt + templates . ImagePromptSuffix
513523 if style != "" {
514- fullPrompt = fmt .Sprintf ("%s. Style: %s" , prompt , style )
524+ fullPrompt = fmt .Sprintf ("%s Style: %s" , fullPrompt , style )
515525 }
516526
517527 reqBody := map [string ]interface {}{
@@ -608,7 +618,7 @@ func (p *OpenAiPlatform) Translate(ctx context.Context, apiKey string, input []s
608618 req := ResponsesAPIRequest {
609619 Model : translateModel ,
610620 Instructions : lang .TranslateInstruction ,
611- Input : fmt .Sprintf ("Translate this JSON to %s:\n \n %s" , lang .GetLanguageName (targetLang ), originals ),
621+ Input : [] InputMessage {{ Role : "user" , Content : fmt .Sprintf ("Translate this JSON to %s:\n \n %s" , lang .GetLanguageName (targetLang ), originals )}} ,
612622 Store : false ,
613623 Text : & TextConfig {
614624 Format : FormatConfig {
@@ -729,45 +739,7 @@ func isIrrelevantModel(modelID string) bool {
729739
730740// isDatedModel checks if a model ID ends with a date or version pattern
731741func isDatedModel (modelID string ) bool {
732- parts := strings .Split (modelID , "-" )
733- if len (parts ) < 2 {
734- return false
735- }
736-
737- lastPart := parts [len (parts )- 1 ]
738-
739- // Check if ends with 4 digits (e.g., -1106, -0914)
740- if len (lastPart ) == 4 {
741- for _ , ch := range lastPart {
742- if ch < '0' || ch > '9' {
743- return false
744- }
745- }
746- return true
747- }
748-
749- // Check if model ends with -YYYY-MM-DD pattern
750- if len (parts ) < 3 {
751- return false
752- }
753-
754- // Get last 3 parts
755- lastThree := parts [len (parts )- 3 :]
756-
757- // Check if they match YYYY-MM-DD pattern
758- if len (lastThree [0 ]) == 4 && len (lastThree [1 ]) == 2 && len (lastThree [2 ]) == 2 {
759- // Verify they're all numeric
760- for _ , part := range lastThree {
761- for _ , ch := range part {
762- if ch < '0' || ch > '9' {
763- return false
764- }
765- }
766- }
767- return true
768- }
769-
770- return false
742+ return functional .EndsWithDigits (modelID , 4 ) || functional .EndsWithDatePattern (modelID )
771743}
772744
773745// GenerateTheme generates a visual theme JSON for the game player UI
@@ -785,7 +757,7 @@ func (p *OpenAiPlatform) GenerateTheme(ctx context.Context, session *obj.GameSes
785757 reqBody := ResponsesAPIRequest {
786758 Model : model ,
787759 Instructions : systemPrompt ,
788- Input : userPrompt ,
760+ Input : [] InputMessage {{ Role : "user" , Content : userPrompt }} ,
789761 Store : false , // Don't store theme generation in conversation history
790762 }
791763
0 commit comments