Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 38 additions & 11 deletions __tests__/src/api/openai.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { jest, describe, it, expect, beforeEach } from '@jest/globals';

// Mock OpenAI client to capture options and simulate chat completions
const createMock = jest.fn();
const chatCreateMock = jest.fn();
const responsesCreateMock = jest.fn();
class MockOpenAI {
options: any;
chat: any;
responses: any;
constructor(options: any) {
this.options = options;
this.chat = { completions: { create: createMock } };
this.chat = { completions: { create: chatCreateMock } };
this.responses = { create: responsesCreateMock };
}
}
jest.unstable_mockModule('openai', () => ({
Expand All @@ -21,7 +24,8 @@ const { getOpenAIClient, generateCommitMessage } = await import(
);

beforeEach(() => {
createMock.mockReset();
chatCreateMock.mockReset();
responsesCreateMock.mockReset();
});

describe('getOpenAIClient', () => {
Expand Down Expand Up @@ -54,8 +58,8 @@ describe('generateCommitMessage', () => {
const userPrompt = 'Test prompt';

it('returns a valid one-line commit message', async () => {
createMock.mockResolvedValueOnce({
choices: [{ message: { content: 'feat: add feature\nDetails' } }],
responsesCreateMock.mockResolvedValueOnce({
output_text: 'feat: add feature\nDetails',
});
const result = await generateCommitMessage(
changedFiles,
Expand All @@ -64,13 +68,30 @@ describe('generateCommitMessage', () => {
config,
);
expect(result).toBe('feat: add feature');
expect(createMock).toHaveBeenCalledWith(
expect(responsesCreateMock).toHaveBeenCalledWith(
expect.objectContaining({ model: 'commit-model' }),
);
});

it('falls back to chat completions when responses API fails', async () => {
responsesCreateMock.mockRejectedValueOnce(new Error('responses error'));
chatCreateMock.mockResolvedValueOnce({
choices: [{ message: { content: 'fix: use chat fallback' } }],
});
const result = await generateCommitMessage(
changedFiles,
userPrompt,
{},
config,
);
expect(result).toBe('fix: use chat fallback');
});

it('falls back to PR message on empty commit', async () => {
createMock.mockResolvedValueOnce({
responsesCreateMock.mockResolvedValueOnce({
output_text: '',
});
chatCreateMock.mockResolvedValueOnce({
choices: [{ message: { content: '' } }],
});
const result = await generateCommitMessage(
Expand All @@ -84,8 +105,8 @@ describe('generateCommitMessage', () => {

it('falls back to Issue message on too long commit', async () => {
const longContent = 'a'.repeat(101);
createMock.mockResolvedValueOnce({
choices: [{ message: { content: longContent } }],
responsesCreateMock.mockResolvedValueOnce({
output_text: longContent,
});
const result = await generateCommitMessage(
changedFiles,
Expand All @@ -97,7 +118,10 @@ describe('generateCommitMessage', () => {
});

it('falls back to file count message when no context', async () => {
createMock.mockResolvedValueOnce({
responsesCreateMock.mockResolvedValueOnce({
output_text: '',
});
chatCreateMock.mockResolvedValueOnce({
choices: [{ message: { content: '' } }],
});
const result = await generateCommitMessage(
Expand All @@ -107,7 +131,10 @@ describe('generateCommitMessage', () => {
config,
);
expect(result).toBe('chore: apply changes to 2 files');
createMock.mockResolvedValueOnce({
responsesCreateMock.mockResolvedValueOnce({
output_text: '',
});
chatCreateMock.mockResolvedValueOnce({
choices: [{ message: { content: '' } }],
});
const single = await generateCommitMessage(
Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ inputs:
description: 'OpenAI model identifier to use (default: gpt-5.3-codex)'
required: false
openai-commit-message-model:
description: 'OpenAI model identifier used to generate commit messages (default: gpt-5.2-codex)'
description: 'OpenAI model identifier used to generate commit messages (default: gpt-4.1-mini)'
required: false
direct-prompt:
description: 'One-shot prompt for automated workflows. Bypasses comment triggers.'
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `openai-base-url` | OpenAI API base URL |
| `openai-model` | OpenAI model identifier to use (default: `gpt-5.3-codex`) |
| `openai-commit-message-model` | OpenAI model identifier used to generate commit messages (default: `gpt-5.2-codex`) |
| `openai-commit-message-model` | OpenAI model identifier used to generate commit messages (default: `gpt-4.1-mini`) |
| `trigger-phrase` | Custom trigger phrase to invoke Codez (default: `/codex`) |
| `assignee-trigger` | Comma-separated list of GitHub usernames to trigger Codez on issue assignment |
| `codex-env` | Custom environment variables to inject into the Codex CLI execution context. Accepts either a YAML mapping (multiline) or comma-separated key=value pairs. |
Expand Down
64 changes: 53 additions & 11 deletions src/api/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { ParseError } from '../utils/errors';
*/

export const defaultModel = 'gpt-5.3-codex';
export const defaultCommitMessageModel = 'gpt-5.2-codex';

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/codex For commit messages, please continue using a different model, as GPT-5.3-Codex is not available via the API.

@github-actions github-actions Bot Mar 1, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated as requested: commit-message generation now keeps a separate default model from the main Codex model.

Changed:

Validation note:

  • Could not run build/tests in this environment because dependencies are not installed (esbuild and jest not found).

export const defaultCommitMessageModel = 'gpt-4.1-mini';

/**
* Create and configure an OpenAI API client instance.
Expand Down Expand Up @@ -58,16 +58,12 @@ export async function generateCommitMessage(

const openai = getOpenAIClient(config);
try {
const response = await openai.chat.completions.create({
model: config.openaiCommitMessageModel,
max_completion_tokens: MAX_COMPLETION_TOKENS,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userMessage },
],
});

const content = response.choices?.[0]?.message?.content?.trim() ?? '';
const content = await generateCommitMessageCompletion(
openai,
config.openaiCommitMessageModel,
systemPrompt,
userMessage,
);
const subject = content.split(/\r?\n/)[0] || '';
if (!subject || subject.length > MAX_SUBJECT_LENGTH) {
throw new ParseError(`Invalid commit message: "${subject}"`);
Expand All @@ -85,6 +81,52 @@ export async function generateCommitMessage(
}
}

async function generateCommitMessageCompletion(
openai: OpenAI,
model: string,
systemPrompt: string,
userMessage: string,
): Promise<string> {
let responsesError: unknown | undefined;
try {
const response = await openai.responses.create({
model,
instructions: systemPrompt,
input: userMessage,
max_output_tokens: MAX_COMPLETION_TOKENS,
});
const content = response.output_text?.trim() ?? '';
if (content) {
return content;
}
throw new ParseError('OpenAI responses API returned empty commit message');
} catch (error) {
responsesError = error;
}

try {
const response = await openai.chat.completions.create({
model,
max_completion_tokens: MAX_COMPLETION_TOKENS,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userMessage },
],
});
return response.choices?.[0]?.message?.content?.trim() ?? '';
} catch (chatError) {
const responseErrorMessage =
responsesError instanceof Error
? responsesError.message
: String(responsesError);
const chatErrorMessage =
chatError instanceof Error ? chatError.message : String(chatError);
throw new Error(
`OpenAI responses API failed (${responseErrorMessage}) and chat completions failed (${chatErrorMessage})`,
);
}
}

/** Assemble the user prompt for OpenAI including file changes and context. */
function buildUserMessage(
changedFiles: string[],
Expand Down