diff --git a/src/config/index.mjs b/src/config/index.mjs index cb1e0c3ea..b242380e0 100644 --- a/src/config/index.mjs +++ b/src/config/index.mjs @@ -50,6 +50,7 @@ export const chatgptApiModelKeys = [ 'chatgptApi35_1106', 'chatgptApi35_0125', 'chatgptApi4o_128k', + 'chatgptApiChatLatest', 'chatgptApi5Latest', 'chatgptApi5', 'chatgptApi5_1Latest', @@ -254,6 +255,7 @@ export const Models = { value: 'gpt-4-0125-preview', desc: 'OpenAI (GPT-4-Turbo 128k 0125 Preview)', }, + chatgptApiChatLatest: { value: 'chat-latest', desc: 'OpenAI (Chat latest)' }, chatgptApi5Latest: { value: 'gpt-5-chat-latest', desc: 'OpenAI (GPT-5 latest)' }, chatgptApi5: { value: 'gpt-5', desc: 'OpenAI (GPT-5)' }, chatgptApi5_1Latest: { value: 'gpt-5.1-chat-latest', desc: 'OpenAI (GPT-5.1 latest)' }, diff --git a/src/services/apis/openai-token-params.mjs b/src/services/apis/openai-token-params.mjs index c9cf9bfca..6293192c4 100644 --- a/src/services/apis/openai-token-params.mjs +++ b/src/services/apis/openai-token-params.mjs @@ -1,4 +1,4 @@ -const GPT5_CHAT_COMPLETIONS_MODEL_PATTERN = /^gpt-5([.-]|$)/ +const OPENAI_MAX_COMPLETION_TOKENS_MODEL_PATTERN = /^(?:gpt-5(?:[.-]|$)|chat-latest$)/ function shouldUseMaxCompletionTokens(provider, model) { const normalizedProvider = String(provider || '').toLowerCase() @@ -6,7 +6,7 @@ function shouldUseMaxCompletionTokens(provider, model) { switch (true) { case normalizedProvider === 'openai' && - GPT5_CHAT_COMPLETIONS_MODEL_PATTERN.test(normalizedModel): + OPENAI_MAX_COMPLETION_TOKENS_MODEL_PATTERN.test(normalizedModel): return true default: return false diff --git a/tests/unit/services/apis/openai-api-compat.test.mjs b/tests/unit/services/apis/openai-api-compat.test.mjs index 39b890598..e9e7d4288 100644 --- a/tests/unit/services/apis/openai-api-compat.test.mjs +++ b/tests/unit/services/apis/openai-api-compat.test.mjs @@ -8,13 +8,15 @@ import { import { createFakePort } from '../../helpers/port.mjs' import { createMockSseResponse } from '../../helpers/sse-response.mjs' -const gpt5LatestCompatModelNames = [ +const latestCompatModelNames = [ + 'chatgptApi-chat-latest', 'chatgptApi-gpt-5-chat-latest', 'chatgptApi-gpt-5.1-chat-latest', 'chatgptApi-gpt-5.2-chat-latest', 'chatgptApi-gpt-5.3-chat-latest', ] -const gpt5LatestMappedModels = [ +const latestMappedModels = [ + ['chatgptApiChatLatest', 'chat-latest'], ['chatgptApi5Latest', 'gpt-5-chat-latest'], ['chatgptApi5_1Latest', 'gpt-5.1-chat-latest'], ['chatgptApi5_2Latest', 'gpt-5.2-chat-latest'], @@ -93,7 +95,7 @@ test('generateAnswersWithOpenAiApiCompat sends expected request and aggregates S assert.deepEqual(session.conversationRecords.at(-1), { question: 'CurrentQ', answer: 'Hello' }) }) -test('generateAnswersWithOpenAiApiCompat uses max_completion_tokens for OpenAI latest gpt-5 compat models', async (t) => { +test('generateAnswersWithOpenAiApiCompat uses max_completion_tokens for OpenAI latest compat models', async (t) => { t.mock.method(console, 'debug', () => {}) setStorage({ maxConversationContextLength: 3, @@ -108,7 +110,7 @@ test('generateAnswersWithOpenAiApiCompat uses max_completion_tokens for OpenAI l ]) }) - for (const modelName of gpt5LatestCompatModelNames) { + for (const modelName of latestCompatModelNames) { capturedInit = undefined const session = { modelName, @@ -133,7 +135,7 @@ test('generateAnswersWithOpenAiApiCompat uses max_completion_tokens for OpenAI l } }) -test('generateAnswersWithOpenAiApiCompat uses latest mapped gpt-5 API model values', async (t) => { +test('generateAnswersWithOpenAiApiCompat uses latest mapped API model values', async (t) => { t.mock.method(console, 'debug', () => {}) setStorage({ maxConversationContextLength: 3, @@ -148,7 +150,7 @@ test('generateAnswersWithOpenAiApiCompat uses latest mapped gpt-5 API model valu ]) }) - for (const [modelName, expectedModel] of gpt5LatestMappedModels) { + for (const [modelName, expectedModel] of latestMappedModels) { capturedInit = undefined const session = { modelName, @@ -279,7 +281,7 @@ test('generateAnswersWithOpenAiApi uses max_completion_tokens for GPT-5.4 nano', assert.equal(Object.hasOwn(body, 'max_tokens'), false) }) -test('generateAnswersWithOpenAiApiCompat keeps max_tokens for latest mapped gpt-5 models in compat provider', async (t) => { +test('generateAnswersWithOpenAiApiCompat keeps max_tokens for latest mapped models in compat provider', async (t) => { t.mock.method(console, 'debug', () => {}) setStorage({ maxConversationContextLength: 3, @@ -294,7 +296,7 @@ test('generateAnswersWithOpenAiApiCompat keeps max_tokens for latest mapped gpt- ]) }) - for (const [modelName, expectedModel] of gpt5LatestMappedModels) { + for (const [modelName, expectedModel] of latestMappedModels) { capturedInit = undefined const session = { modelName, @@ -361,7 +363,7 @@ test('generateAnswersWithOpenAiApiCompat removes conflicting token key from extr assert.equal(body.top_p, 0.9) }) -test('generateAnswersWithOpenAiApiCompat removes max_tokens from extraBody for OpenAI gpt-5 models', async (t) => { +test('generateAnswersWithOpenAiApiCompat removes max_tokens from extraBody for OpenAI latest models', async (t) => { t.mock.method(console, 'debug', () => {}) setStorage({ maxConversationContextLength: 3, @@ -376,7 +378,7 @@ test('generateAnswersWithOpenAiApiCompat removes max_tokens from extraBody for O ]) }) - for (const modelName of gpt5LatestCompatModelNames) { + for (const modelName of latestCompatModelNames) { capturedInit = undefined const session = { modelName, @@ -446,7 +448,7 @@ test('generateAnswersWithOpenAiApiCompat allows max_tokens override for compat p assert.equal(body.top_p, 0.75) }) -test('generateAnswersWithOpenAiApiCompat allows max_completion_tokens override for OpenAI gpt-5 models', async (t) => { +test('generateAnswersWithOpenAiApiCompat allows max_completion_tokens override for OpenAI latest models', async (t) => { t.mock.method(console, 'debug', () => {}) setStorage({ maxConversationContextLength: 3, @@ -461,7 +463,7 @@ test('generateAnswersWithOpenAiApiCompat allows max_completion_tokens override f ]) }) - for (const modelName of gpt5LatestCompatModelNames) { + for (const modelName of latestCompatModelNames) { capturedInit = undefined const session = { modelName, diff --git a/tests/unit/services/apis/openai-token-params.test.mjs b/tests/unit/services/apis/openai-token-params.test.mjs index 4c50a250f..5cd8215bb 100644 --- a/tests/unit/services/apis/openai-token-params.test.mjs +++ b/tests/unit/services/apis/openai-token-params.test.mjs @@ -8,6 +8,12 @@ test('uses max_completion_tokens for gpt-5.x chat models', () => { }) }) +test('uses max_completion_tokens for chat-latest model', () => { + assert.deepEqual(getChatCompletionsTokenParams('openai', 'chat-latest', 1025), { + max_completion_tokens: 1025, + }) +}) + test('uses max_tokens for provider-prefixed gpt-5.x model names', () => { assert.deepEqual(getChatCompletionsTokenParams('openai', 'openai/gpt-5.2', 2048), { max_tokens: 2048, @@ -49,6 +55,9 @@ test('uses max_tokens for lookalike model names', () => { assert.deepEqual(getChatCompletionsTokenParams('openai', 'my-gpt-5-clone', 640), { max_tokens: 640, }) + assert.deepEqual(getChatCompletionsTokenParams('openai', 'my-chat-latest', 641), { + max_tokens: 641, + }) }) test('uses max_tokens for empty model values', () => { diff --git a/tests/unit/utils/model-name-convert.test.mjs b/tests/unit/utils/model-name-convert.test.mjs index fb1fe4880..29e726366 100644 --- a/tests/unit/utils/model-name-convert.test.mjs +++ b/tests/unit/utils/model-name-convert.test.mjs @@ -257,6 +257,7 @@ test('modelNameToValue returns value for known model', () => { }) test('modelNameToValue returns endpoint for latest chatgptApi models', () => { + assert.equal(modelNameToValue('chatgptApiChatLatest'), 'chat-latest') assert.equal(modelNameToValue('chatgptApi5Latest'), 'gpt-5-chat-latest') assert.equal(modelNameToValue('chatgptApi5_1Latest'), 'gpt-5.1-chat-latest') assert.equal(modelNameToValue('chatgptApi5_2Latest'), 'gpt-5.2-chat-latest')