mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-16 21:53:07 +00:00
feat: Add GPT-5 model variants and improve Codex execution logic. Addressed code review comments
This commit is contained in:
@@ -7,11 +7,16 @@
|
||||
*/
|
||||
export type CodexModelId =
|
||||
| 'codex-gpt-5.3-codex'
|
||||
| 'codex-gpt-5.3-codex-spark'
|
||||
| 'codex-gpt-5.2-codex'
|
||||
| 'codex-gpt-5.1-codex-max'
|
||||
| 'codex-gpt-5.1-codex-mini'
|
||||
| 'codex-gpt-5.1-codex'
|
||||
| 'codex-gpt-5-codex'
|
||||
| 'codex-gpt-5-codex-mini'
|
||||
| 'codex-gpt-5.2'
|
||||
| 'codex-gpt-5.1';
|
||||
| 'codex-gpt-5.1'
|
||||
| 'codex-gpt-5';
|
||||
|
||||
/**
|
||||
* Codex model metadata
|
||||
@@ -37,6 +42,13 @@ export const CODEX_MODEL_CONFIG_MAP: Record<CodexModelId, CodexModelConfig> = {
|
||||
hasThinking: true,
|
||||
supportsVision: true,
|
||||
},
|
||||
'codex-gpt-5.3-codex-spark': {
|
||||
id: 'codex-gpt-5.3-codex-spark',
|
||||
label: 'GPT-5.3-Codex-Spark',
|
||||
description: 'Near-instant real-time coding model, 1000+ tokens/sec',
|
||||
hasThinking: true,
|
||||
supportsVision: true,
|
||||
},
|
||||
'codex-gpt-5.2-codex': {
|
||||
id: 'codex-gpt-5.2-codex',
|
||||
label: 'GPT-5.2-Codex',
|
||||
@@ -58,6 +70,27 @@ export const CODEX_MODEL_CONFIG_MAP: Record<CodexModelId, CodexModelConfig> = {
|
||||
hasThinking: false,
|
||||
supportsVision: true,
|
||||
},
|
||||
'codex-gpt-5.1-codex': {
|
||||
id: 'codex-gpt-5.1-codex',
|
||||
label: 'GPT-5.1-Codex',
|
||||
description: 'Original GPT-5.1 Codex agentic coding model',
|
||||
hasThinking: true,
|
||||
supportsVision: true,
|
||||
},
|
||||
'codex-gpt-5-codex': {
|
||||
id: 'codex-gpt-5-codex',
|
||||
label: 'GPT-5-Codex',
|
||||
description: 'Original GPT-5 Codex model',
|
||||
hasThinking: true,
|
||||
supportsVision: true,
|
||||
},
|
||||
'codex-gpt-5-codex-mini': {
|
||||
id: 'codex-gpt-5-codex-mini',
|
||||
label: 'GPT-5-Codex-Mini',
|
||||
description: 'Smaller, cheaper GPT-5 Codex variant',
|
||||
hasThinking: false,
|
||||
supportsVision: true,
|
||||
},
|
||||
'codex-gpt-5.2': {
|
||||
id: 'codex-gpt-5.2',
|
||||
label: 'GPT-5.2 (Codex)',
|
||||
@@ -72,6 +105,13 @@ export const CODEX_MODEL_CONFIG_MAP: Record<CodexModelId, CodexModelConfig> = {
|
||||
hasThinking: true,
|
||||
supportsVision: true,
|
||||
},
|
||||
'codex-gpt-5': {
|
||||
id: 'codex-gpt-5',
|
||||
label: 'GPT-5 (Codex)',
|
||||
description: 'Base GPT-5 model via Codex',
|
||||
hasThinking: true,
|
||||
supportsVision: true,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,6 +8,8 @@ export type ErrorType =
|
||||
| 'execution'
|
||||
| 'rate_limit'
|
||||
| 'quota_exhausted'
|
||||
| 'model_not_found'
|
||||
| 'stream_disconnected'
|
||||
| 'unknown';
|
||||
|
||||
/**
|
||||
@@ -21,6 +23,8 @@ export interface ErrorInfo {
|
||||
isCancellation: boolean;
|
||||
isRateLimit: boolean;
|
||||
isQuotaExhausted: boolean; // Session/weekly usage limit reached
|
||||
isModelNotFound: boolean; // Model does not exist or user lacks access
|
||||
isStreamDisconnected: boolean; // Stream disconnected before completion
|
||||
retryAfter?: number; // Seconds to wait before retrying (for rate limit errors)
|
||||
originalError: unknown;
|
||||
}
|
||||
|
||||
@@ -60,14 +60,35 @@ export type EventType =
|
||||
| 'cherry-pick:success'
|
||||
| 'cherry-pick:conflict'
|
||||
| 'cherry-pick:failure'
|
||||
| 'cherry-pick:verify-failed'
|
||||
| 'cherry-pick:abort'
|
||||
| 'rebase:started'
|
||||
| 'rebase:success'
|
||||
| 'rebase:conflict'
|
||||
| 'rebase:failure'
|
||||
| 'stash:start'
|
||||
| 'stash:progress'
|
||||
| 'stash:conflicts'
|
||||
| 'stash:success'
|
||||
| 'stash:failure'
|
||||
| 'merge:start'
|
||||
| 'merge:success'
|
||||
| 'merge:conflict'
|
||||
| 'merge:error'
|
||||
| 'branchCommitLog:start'
|
||||
| 'branchCommitLog:progress'
|
||||
| 'branchCommitLog:done'
|
||||
| 'branchCommitLog:error'
|
||||
| 'commitLog:start'
|
||||
| 'commitLog:progress'
|
||||
| 'commitLog:complete'
|
||||
| 'commitLog:error'
|
||||
| 'switch:start'
|
||||
| 'switch:stash'
|
||||
| 'switch:checkout'
|
||||
| 'switch:pop'
|
||||
| 'switch:done'
|
||||
| 'switch:error'
|
||||
| 'notification:created';
|
||||
|
||||
export type EventCallback = (type: EventType, payload: unknown) => void;
|
||||
|
||||
@@ -80,6 +80,14 @@ export const CODEX_MODELS: (ModelOption & { hasReasoning?: boolean })[] = [
|
||||
provider: 'codex',
|
||||
hasReasoning: true,
|
||||
},
|
||||
{
|
||||
id: CODEX_MODEL_MAP.gpt53CodexSpark,
|
||||
label: 'GPT-5.3-Codex-Spark',
|
||||
description: 'Near-instant real-time coding model, 1000+ tokens/sec.',
|
||||
badge: 'Speed',
|
||||
provider: 'codex',
|
||||
hasReasoning: true,
|
||||
},
|
||||
{
|
||||
id: CODEX_MODEL_MAP.gpt52Codex,
|
||||
label: 'GPT-5.2-Codex',
|
||||
@@ -104,6 +112,30 @@ export const CODEX_MODELS: (ModelOption & { hasReasoning?: boolean })[] = [
|
||||
provider: 'codex',
|
||||
hasReasoning: false,
|
||||
},
|
||||
{
|
||||
id: CODEX_MODEL_MAP.gpt51Codex,
|
||||
label: 'GPT-5.1-Codex',
|
||||
description: 'Original GPT-5.1 Codex agentic coding model.',
|
||||
badge: 'Balanced',
|
||||
provider: 'codex',
|
||||
hasReasoning: true,
|
||||
},
|
||||
{
|
||||
id: CODEX_MODEL_MAP.gpt5Codex,
|
||||
label: 'GPT-5-Codex',
|
||||
description: 'Original GPT-5 Codex model.',
|
||||
badge: 'Balanced',
|
||||
provider: 'codex',
|
||||
hasReasoning: true,
|
||||
},
|
||||
{
|
||||
id: CODEX_MODEL_MAP.gpt5CodexMini,
|
||||
label: 'GPT-5-Codex-Mini',
|
||||
description: 'Smaller, cheaper GPT-5 Codex variant.',
|
||||
badge: 'Speed',
|
||||
provider: 'codex',
|
||||
hasReasoning: false,
|
||||
},
|
||||
{
|
||||
id: CODEX_MODEL_MAP.gpt52,
|
||||
label: 'GPT-5.2',
|
||||
@@ -120,6 +152,14 @@ export const CODEX_MODELS: (ModelOption & { hasReasoning?: boolean })[] = [
|
||||
provider: 'codex',
|
||||
hasReasoning: true,
|
||||
},
|
||||
{
|
||||
id: CODEX_MODEL_MAP.gpt5,
|
||||
label: 'GPT-5',
|
||||
description: 'Base GPT-5 model.',
|
||||
badge: 'Balanced',
|
||||
provider: 'codex',
|
||||
hasReasoning: true,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -222,11 +262,16 @@ export function getModelDisplayName(model: ModelAlias | string): string {
|
||||
sonnet: 'Claude Sonnet',
|
||||
opus: 'Claude Opus',
|
||||
[CODEX_MODEL_MAP.gpt53Codex]: 'GPT-5.3-Codex',
|
||||
[CODEX_MODEL_MAP.gpt53CodexSpark]: 'GPT-5.3-Codex-Spark',
|
||||
[CODEX_MODEL_MAP.gpt52Codex]: 'GPT-5.2-Codex',
|
||||
[CODEX_MODEL_MAP.gpt51CodexMax]: 'GPT-5.1-Codex-Max',
|
||||
[CODEX_MODEL_MAP.gpt51CodexMini]: 'GPT-5.1-Codex-Mini',
|
||||
[CODEX_MODEL_MAP.gpt51Codex]: 'GPT-5.1-Codex',
|
||||
[CODEX_MODEL_MAP.gpt5Codex]: 'GPT-5-Codex',
|
||||
[CODEX_MODEL_MAP.gpt5CodexMini]: 'GPT-5-Codex-Mini',
|
||||
[CODEX_MODEL_MAP.gpt52]: 'GPT-5.2',
|
||||
[CODEX_MODEL_MAP.gpt51]: 'GPT-5.1',
|
||||
[CODEX_MODEL_MAP.gpt5]: 'GPT-5',
|
||||
};
|
||||
|
||||
// Check direct match first
|
||||
|
||||
@@ -52,18 +52,28 @@ export const CODEX_MODEL_MAP = {
|
||||
// Recommended Codex-specific models
|
||||
/** Latest frontier agentic coding model */
|
||||
gpt53Codex: 'codex-gpt-5.3-codex',
|
||||
/** Smaller, near-instant version of GPT-5.3-Codex for real-time coding */
|
||||
gpt53CodexSpark: 'codex-gpt-5.3-codex-spark',
|
||||
/** Frontier agentic coding model */
|
||||
gpt52Codex: 'codex-gpt-5.2-codex',
|
||||
/** Codex-optimized flagship for deep and fast reasoning */
|
||||
gpt51CodexMax: 'codex-gpt-5.1-codex-max',
|
||||
/** Optimized for codex. Cheaper, faster, but less capable */
|
||||
gpt51CodexMini: 'codex-gpt-5.1-codex-mini',
|
||||
/** Original GPT-5.1 Codex model */
|
||||
gpt51Codex: 'codex-gpt-5.1-codex',
|
||||
/** Original GPT-5 Codex model */
|
||||
gpt5Codex: 'codex-gpt-5-codex',
|
||||
/** Smaller, cheaper GPT-5 Codex variant */
|
||||
gpt5CodexMini: 'codex-gpt-5-codex-mini',
|
||||
|
||||
// General-purpose GPT models (also available in Codex)
|
||||
/** Latest frontier model with improvements across knowledge, reasoning and coding */
|
||||
gpt52: 'codex-gpt-5.2',
|
||||
/** Great for coding and agentic tasks across domains */
|
||||
gpt51: 'codex-gpt-5.1',
|
||||
/** Base GPT-5 model */
|
||||
gpt5: 'codex-gpt-5',
|
||||
} as const;
|
||||
|
||||
export const CODEX_MODEL_IDS = Object.values(CODEX_MODEL_MAP);
|
||||
@@ -74,10 +84,14 @@ export const CODEX_MODEL_IDS = Object.values(CODEX_MODEL_MAP);
|
||||
*/
|
||||
export const REASONING_CAPABLE_MODELS = new Set([
|
||||
CODEX_MODEL_MAP.gpt53Codex,
|
||||
CODEX_MODEL_MAP.gpt53CodexSpark,
|
||||
CODEX_MODEL_MAP.gpt52Codex,
|
||||
CODEX_MODEL_MAP.gpt51CodexMax,
|
||||
CODEX_MODEL_MAP.gpt51Codex,
|
||||
CODEX_MODEL_MAP.gpt5Codex,
|
||||
CODEX_MODEL_MAP.gpt52,
|
||||
CODEX_MODEL_MAP.gpt51,
|
||||
CODEX_MODEL_MAP.gpt5,
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -117,6 +117,44 @@ export function isQuotaExhaustedError(error: unknown): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an error indicates a model-not-found or model access issue
|
||||
*
|
||||
* @param error - The error to check
|
||||
* @returns True if the error indicates the model doesn't exist or user lacks access
|
||||
*/
|
||||
export function isModelNotFoundError(error: unknown): boolean {
|
||||
const message = error instanceof Error ? error.message : String(error || '');
|
||||
const lowerMessage = message.toLowerCase();
|
||||
|
||||
return (
|
||||
lowerMessage.includes('does not exist or you do not have access') ||
|
||||
lowerMessage.includes('model_not_found') ||
|
||||
lowerMessage.includes('invalid_model') ||
|
||||
(lowerMessage.includes('model') &&
|
||||
(lowerMessage.includes('does not exist') || lowerMessage.includes('not found')))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an error indicates a stream disconnection
|
||||
*
|
||||
* @param error - The error to check
|
||||
* @returns True if the error indicates the stream was disconnected
|
||||
*/
|
||||
export function isStreamDisconnectedError(error: unknown): boolean {
|
||||
const message = error instanceof Error ? error.message : String(error || '');
|
||||
const lowerMessage = message.toLowerCase();
|
||||
|
||||
return (
|
||||
lowerMessage.includes('stream disconnected') ||
|
||||
lowerMessage.includes('stream ended') ||
|
||||
lowerMessage.includes('connection reset') ||
|
||||
lowerMessage.includes('socket hang up') ||
|
||||
lowerMessage.includes('econnreset')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract retry-after duration from rate limit error
|
||||
*
|
||||
@@ -154,11 +192,17 @@ export function classifyError(error: unknown): ErrorInfo {
|
||||
const isCancellation = isCancellationError(message);
|
||||
const isRateLimit = isRateLimitError(error);
|
||||
const isQuotaExhausted = isQuotaExhaustedError(error);
|
||||
const isModelNotFound = isModelNotFoundError(error);
|
||||
const isStreamDisconnected = isStreamDisconnectedError(error);
|
||||
const retryAfter = isRateLimit ? (extractRetryAfter(error) ?? 60) : undefined;
|
||||
|
||||
let type: ErrorType;
|
||||
if (isAuth) {
|
||||
type = 'authentication';
|
||||
} else if (isModelNotFound) {
|
||||
type = 'model_not_found';
|
||||
} else if (isStreamDisconnected) {
|
||||
type = 'stream_disconnected';
|
||||
} else if (isQuotaExhausted) {
|
||||
// Quota exhaustion takes priority over rate limit since it's more specific
|
||||
type = 'quota_exhausted';
|
||||
@@ -182,6 +226,8 @@ export function classifyError(error: unknown): ErrorInfo {
|
||||
isCancellation,
|
||||
isRateLimit,
|
||||
isQuotaExhausted,
|
||||
isModelNotFound,
|
||||
isStreamDisconnected,
|
||||
retryAfter,
|
||||
originalError: error,
|
||||
};
|
||||
@@ -204,6 +250,14 @@ export function getUserFriendlyErrorMessage(error: unknown): string {
|
||||
return 'Authentication failed. Please check your API key.';
|
||||
}
|
||||
|
||||
if (info.isModelNotFound) {
|
||||
return `Model not available: ${info.message}\n\nSome models require specific subscription plans or authentication methods. Try authenticating with 'codex login' or switch to a different model.`;
|
||||
}
|
||||
|
||||
if (info.isStreamDisconnected) {
|
||||
return `Connection interrupted: ${info.message}\n\nThe stream was disconnected before the response could complete. This may be caused by network issues, model access restrictions, or server timeouts. Try again or switch to a different model.`;
|
||||
}
|
||||
|
||||
if (info.isQuotaExhausted) {
|
||||
return 'Usage limit reached. Auto Mode has been paused. Please wait for your quota to reset or upgrade your plan.';
|
||||
}
|
||||
@@ -241,3 +295,25 @@ export function getUserFriendlyErrorMessage(error: unknown): string {
|
||||
export function getErrorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : 'Unknown error';
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an error with a context message to stderr.
|
||||
*
|
||||
* Convenience utility for consistent error logging throughout the codebase.
|
||||
* Outputs a formatted error line to stderr with an ❌ prefix and the context.
|
||||
*
|
||||
* @param error - The error value to log
|
||||
* @param context - Descriptive context message indicating where/why the error occurred
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* try {
|
||||
* await someOperation();
|
||||
* } catch (error) {
|
||||
* logError(error, 'Failed to perform some operation');
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function logError(error: unknown, context: string): void {
|
||||
console.error(`❌ ${context}:`, error);
|
||||
}
|
||||
|
||||
@@ -10,10 +10,13 @@ export {
|
||||
isAuthenticationError,
|
||||
isRateLimitError,
|
||||
isQuotaExhaustedError,
|
||||
isModelNotFoundError,
|
||||
isStreamDisconnectedError,
|
||||
extractRetryAfter,
|
||||
classifyError,
|
||||
getUserFriendlyErrorMessage,
|
||||
getErrorMessage,
|
||||
logError,
|
||||
} from './error-handler.js';
|
||||
|
||||
// Conversation utilities
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
isAuthenticationError,
|
||||
isRateLimitError,
|
||||
isQuotaExhaustedError,
|
||||
isModelNotFoundError,
|
||||
isStreamDisconnectedError,
|
||||
extractRetryAfter,
|
||||
classifyError,
|
||||
getUserFriendlyErrorMessage,
|
||||
@@ -179,6 +181,76 @@ describe('error-handler.ts', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('isModelNotFoundError', () => {
|
||||
it('should return true for "does not exist or you do not have access" errors', () => {
|
||||
expect(
|
||||
isModelNotFoundError(
|
||||
new Error('The model `gpt-5.3-codex` does not exist or you do not have access to it.')
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for model_not_found errors', () => {
|
||||
expect(isModelNotFoundError(new Error('model_not_found: gpt-5.3-codex'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for invalid_model errors', () => {
|
||||
expect(isModelNotFoundError(new Error('invalid_model: unknown model'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for "model does not exist" errors', () => {
|
||||
expect(isModelNotFoundError(new Error('The model does not exist'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for "model not found" errors', () => {
|
||||
expect(isModelNotFoundError(new Error('model not found'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for regular errors', () => {
|
||||
expect(isModelNotFoundError(new Error('Something went wrong'))).toBe(false);
|
||||
expect(isModelNotFoundError(new Error('Network error'))).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for null/undefined', () => {
|
||||
expect(isModelNotFoundError(null)).toBe(false);
|
||||
expect(isModelNotFoundError(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isStreamDisconnectedError', () => {
|
||||
it('should return true for "stream disconnected" errors', () => {
|
||||
expect(isStreamDisconnectedError(new Error('stream disconnected before completion'))).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('should return true for "stream ended" errors', () => {
|
||||
expect(isStreamDisconnectedError(new Error('stream ended unexpectedly'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for "connection reset" errors', () => {
|
||||
expect(isStreamDisconnectedError(new Error('connection reset by peer'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for "socket hang up" errors', () => {
|
||||
expect(isStreamDisconnectedError(new Error('socket hang up'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for ECONNRESET errors', () => {
|
||||
expect(isStreamDisconnectedError(new Error('ECONNRESET'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for regular errors', () => {
|
||||
expect(isStreamDisconnectedError(new Error('Something went wrong'))).toBe(false);
|
||||
expect(isStreamDisconnectedError(new Error('Network error'))).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for null/undefined', () => {
|
||||
expect(isStreamDisconnectedError(null)).toBe(false);
|
||||
expect(isStreamDisconnectedError(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractRetryAfter', () => {
|
||||
it('should extract retry-after from error message', () => {
|
||||
const error = new Error('Rate limit exceeded. retry-after: 60');
|
||||
@@ -298,6 +370,28 @@ describe('error-handler.ts', () => {
|
||||
expect(result.isAbort).toBe(false);
|
||||
});
|
||||
|
||||
it('should classify model not found errors', () => {
|
||||
const error = new Error(
|
||||
'The model `gpt-5.3-codex` does not exist or you do not have access to it.'
|
||||
);
|
||||
const result = classifyError(error);
|
||||
|
||||
expect(result.type).toBe('model_not_found');
|
||||
expect(result.isModelNotFound).toBe(true);
|
||||
expect(result.isStreamDisconnected).toBe(false);
|
||||
expect(result.isAuth).toBe(false);
|
||||
});
|
||||
|
||||
it('should classify stream disconnected errors', () => {
|
||||
const error = new Error('stream disconnected before completion');
|
||||
const result = classifyError(error);
|
||||
|
||||
expect(result.type).toBe('stream_disconnected');
|
||||
expect(result.isStreamDisconnected).toBe(true);
|
||||
expect(result.isModelNotFound).toBe(false);
|
||||
expect(result.isAuth).toBe(false);
|
||||
});
|
||||
|
||||
it('should classify execution errors (regular Error)', () => {
|
||||
const error = new Error('Something went wrong');
|
||||
const result = classifyError(error);
|
||||
@@ -397,6 +491,24 @@ describe('error-handler.ts', () => {
|
||||
expect(message).toBe('Authentication failed. Please check your API key.');
|
||||
});
|
||||
|
||||
it('should return friendly message for model not found errors', () => {
|
||||
const error = new Error(
|
||||
'The model `gpt-5.3-codex` does not exist or you do not have access to it.'
|
||||
);
|
||||
const message = getUserFriendlyErrorMessage(error);
|
||||
|
||||
expect(message).toContain('Model not available');
|
||||
expect(message).toContain('codex login');
|
||||
});
|
||||
|
||||
it('should return friendly message for stream disconnected errors', () => {
|
||||
const error = new Error('stream disconnected before completion');
|
||||
const message = getUserFriendlyErrorMessage(error);
|
||||
|
||||
expect(message).toContain('Connection interrupted');
|
||||
expect(message).toContain('stream was disconnected');
|
||||
});
|
||||
|
||||
it('should return friendly message for quota exhausted errors', () => {
|
||||
const error = new Error('overloaded_error');
|
||||
const message = getUserFriendlyErrorMessage(error);
|
||||
|
||||
Reference in New Issue
Block a user