diff --git a/src/__tests__/costTracker.test.ts b/src/__tests__/costTracker.test.ts index fffda47..3ad031b 100644 --- a/src/__tests__/costTracker.test.ts +++ b/src/__tests__/costTracker.test.ts @@ -30,15 +30,15 @@ const localStorageMock = (() => { vi.stubGlobal('localStorage', localStorageMock); describe('calculateCostJpy', () => { - it('gpt-5-nano のコストを正しく計算する', () => { + it('gpt-5.4-nano のコストを正しく計算する', () => { // input: 1000 tokens × 10/1M = 0.01, output: 500 tokens × 40/1M = 0.02 - const cost = calculateCostJpy('gpt-5-nano', 1000, 500); + const cost = calculateCostJpy('gpt-5.4-nano', 1000, 500); expect(cost).toBeCloseTo(0.03, 4); }); - it('gpt-5-mini のコストを正しく計算する', () => { + it('gpt-5.4-mini のコストを正しく計算する', () => { // input: 1000 × 50/1M = 0.05, output: 500 × 200/1M = 0.10 - const cost = calculateCostJpy('gpt-5-mini', 1000, 500); + const cost = calculateCostJpy('gpt-5.4-mini', 1000, 500); expect(cost).toBeCloseTo(0.15, 4); }); @@ -54,14 +54,14 @@ describe('addUsage / getSessionTotal / getCycleTotal', () => { it('addUsage でセッション・サイクル累計が増加する', () => { const before = getSessionTotal(); - const cost = addUsage({ modelId: 'gpt-5-nano', promptTokens: 1000, completionTokens: 500 }); + const cost = addUsage({ modelId: 'gpt-5.4-nano', promptTokens: 1000, completionTokens: 500 }); expect(cost).toBeGreaterThan(0); expect(getSessionTotal()).toBeGreaterThan(before); expect(getCycleTotal()).toBeGreaterThan(0); }); it('resetCycle でサイクル累計のみリセットされる', () => { - addUsage({ modelId: 'gpt-5-nano', promptTokens: 1000, completionTokens: 500 }); + addUsage({ modelId: 'gpt-5.4-nano', promptTokens: 1000, completionTokens: 500 }); const sessionBefore = getSessionTotal(); resetCycle(); expect(getCycleTotal()).toBe(0); @@ -83,9 +83,9 @@ describe('checkBudget', () => { }); it('サイクル累計が ¥5 超で warn を返す', () => { - // サイクル累計を ¥5 超にする: gpt-5-mini × 大量トークン + // サイクル累計を ¥5 超にする: gpt-5.4-mini × 大量トークン for (let i = 0; i < 50; i++) { - addUsage({ modelId: 'gpt-5-mini', promptTokens: 5000, completionTokens: 2000 }); + addUsage({ modelId: 'gpt-5.4-mini', promptTokens: 5000, completionTokens: 2000 }); } const warning = checkBudget(0.1, true); expect(warning).not.toBeNull(); diff --git a/src/__tests__/modelRouter.test.ts b/src/__tests__/modelRouter.test.ts index 3553848..e830270 100644 --- a/src/__tests__/modelRouter.test.ts +++ b/src/__tests__/modelRouter.test.ts @@ -19,43 +19,43 @@ const baseForm: BrainstormForm = { describe('selectModel', () => { it('手動選択(auto以外)はそのまま返す', () => { const input: ModelRouterInput = { taskType: 'generate', depth: 1 }; - const result = selectModel('gpt-4.1-mini', input, true); - expect(result.modelId).toBe('gpt-4.1-mini'); + const result = selectModel('gpt-5.4-mini', input, true); + expect(result.modelId).toBe('gpt-5.4-mini'); expect(result.reason).toBe('手動選択'); }); - it('Free mode + auto → 常に gpt-5-nano', () => { + it('Free mode + auto → 常に gpt-5.4-nano', () => { const input: ModelRouterInput = { taskType: 'generate', depth: 4, form: baseForm }; const result = selectModel(AUTO_MODEL_ID, input, false); - expect(result.modelId).toBe('gpt-5-nano'); + expect(result.modelId).toBe('gpt-5.4-nano'); }); - it('Pro + depth 1 + シンプル入力 → gpt-5-nano', () => { + it('Pro + depth 1 + シンプル入力 → gpt-5.4-nano', () => { const input: ModelRouterInput = { taskType: 'generate', depth: 1, form: baseForm }; const result = selectModel(AUTO_MODEL_ID, input, true); - expect(result.modelId).toBe('gpt-5-nano'); + expect(result.modelId).toBe('gpt-5.4-nano'); }); - it('Pro + depth 2 + シンプル入力 → gpt-5-nano', () => { + it('Pro + depth 2 + シンプル入力 → gpt-5.4-nano', () => { const input: ModelRouterInput = { taskType: 'generate', depth: 2, form: baseForm }; const result = selectModel(AUTO_MODEL_ID, input, true); - expect(result.modelId).toBe('gpt-5-nano'); + expect(result.modelId).toBe('gpt-5.4-nano'); }); - it('Pro + depth 3 → gpt-5-mini', () => { + it('Pro + depth 3 → gpt-5.4-mini', () => { const input: ModelRouterInput = { taskType: 'generate', depth: 3, form: baseForm }; const result = selectModel(AUTO_MODEL_ID, input, true); - expect(result.modelId).toBe('gpt-5-mini'); + expect(result.modelId).toBe('gpt-5.4-mini'); expect(result.reason).toContain('深度3'); }); - it('Pro + depth 4 → gpt-5-mini', () => { + it('Pro + depth 4 → gpt-5.4-mini', () => { const input: ModelRouterInput = { taskType: 'generate', depth: 4, form: baseForm }; const result = selectModel(AUTO_MODEL_ID, input, true); - expect(result.modelId).toBe('gpt-5-mini'); + expect(result.modelId).toBe('gpt-5.4-mini'); }); - it('Pro + 課題3件 + 長い目標 → gpt-5-mini', () => { + it('Pro + 課題3件 + 長い目標 → gpt-5.4-mini', () => { const form: BrainstormForm = { ...baseForm, teamGoals: @@ -68,32 +68,32 @@ describe('selectModel', () => { }; const input: ModelRouterInput = { taskType: 'generate', depth: 2, form }; const result = selectModel(AUTO_MODEL_ID, input, true); - expect(result.modelId).toBe('gpt-5-mini'); + expect(result.modelId).toBe('gpt-5.4-mini'); expect(result.reason).toContain('課題'); }); - it('Pro + 競合データあり → gpt-5-mini', () => { + it('Pro + 競合データあり → gpt-5.4-mini', () => { const form: BrainstormForm = { ...baseForm, competitors: [{ name: '競合A', url: 'https://example.com', note: '' }], }; const input: ModelRouterInput = { taskType: 'generate', depth: 1, form }; const result = selectModel(AUTO_MODEL_ID, input, true); - expect(result.modelId).toBe('gpt-5-mini'); + expect(result.modelId).toBe('gpt-5.4-mini'); expect(result.reason).toContain('競合'); }); - it('Pro + KPIデータあり → gpt-5-mini', () => { + it('Pro + KPIデータあり → gpt-5.4-mini', () => { const form: BrainstormForm = { ...baseForm, kpis: [{ label: '成約率', value: '15%' }], }; const input: ModelRouterInput = { taskType: 'generate', depth: 1, form }; const result = selectModel(AUTO_MODEL_ID, input, true); - expect(result.modelId).toBe('gpt-5-mini'); + expect(result.modelId).toBe('gpt-5.4-mini'); }); - it('Pro + トークン2000超 → gpt-5-mini', () => { + it('Pro + トークン2000超 → gpt-5.4-mini', () => { const longText = 'あ'.repeat(4100); // 4100文字 / 2 = 2050トークン const input: ModelRouterInput = { taskType: 'deepDive', @@ -101,7 +101,7 @@ describe('selectModel', () => { messages: [{ role: 'user', content: longText }], }; const result = selectModel(AUTO_MODEL_ID, input, true); - expect(result.modelId).toBe('gpt-5-mini'); + expect(result.modelId).toBe('gpt-5.4-mini'); expect(result.reason).toContain('トークン'); }); }); diff --git a/src/components/layout/HeaderBar.stories.tsx b/src/components/layout/HeaderBar.stories.tsx index 9bb83d0..834666c 100644 --- a/src/components/layout/HeaderBar.stories.tsx +++ b/src/components/layout/HeaderBar.stories.tsx @@ -34,7 +34,7 @@ type Story = StoryObj; export const FreeMode: Story = { args: { proMode: false, - modelLabel: 'Auto', + modelLabel: '5.4 Nano', connStatus: { status: 'idle', msg: '' }, isDark: false, lastUsedModel: null, @@ -46,22 +46,22 @@ export const FreeMode: Story = { export const ProMode: Story = { args: { proMode: true, - modelLabel: 'Auto', + modelLabel: '5.4 Nano', connStatus: { status: 'ok', msg: '接続成功' }, isDark: false, - lastUsedModel: 'gpt-5-mini', + lastUsedModel: 'gpt-5.4-mini', freeRemaining: null, }, }; -/** Auto → 5-nano 表示 */ -export const WithAutoResolved: Story = { +/** 5.4 Nano 表示 */ +export const WithNanoModel: Story = { args: { proMode: true, - modelLabel: 'Auto', + modelLabel: '5.4 Nano', connStatus: { status: 'ok', msg: '接続成功' }, isDark: true, - lastUsedModel: 'gpt-5-nano', + lastUsedModel: 'gpt-5.4-nano', freeRemaining: null, }, }; diff --git a/src/components/layout/HeaderBar.tsx b/src/components/layout/HeaderBar.tsx index 5156929..5b33f95 100644 --- a/src/components/layout/HeaderBar.tsx +++ b/src/components/layout/HeaderBar.tsx @@ -121,12 +121,9 @@ export const HeaderBar: React.FC = ({ )}
{modelLabel} - {modelLabel === 'Auto' && lastUsedModel && ( - → {lastUsedModel.replace('gpt-', '')} - )} {!proMode && freeRemaining && (() => { diff --git a/src/components/modals/HelpModal.tsx b/src/components/modals/HelpModal.tsx index 0151c26..f9a41a0 100644 --- a/src/components/modals/HelpModal.tsx +++ b/src/components/modals/HelpModal.tsx @@ -136,9 +136,8 @@ export const HelpModal: React.FC = ({ onClose }) => { {/* Cost note */}

- 💡 コストの目安:{' '} - gpt-4.1-nanoなら1回あたり約$0.001〜0.005(〜0.5〜0.8円)。 - gpt-5-nanoでも1回あたり数円程度です。 + 💡 コストの目安: gpt-5.4-nanoなら1回あたり約0.5〜1円程度です。 + gpt-5.4-miniでも1回あたり数円程度です。

diff --git a/src/components/modals/SettingsModal.stories.tsx b/src/components/modals/SettingsModal.stories.tsx index b1619ea..5e9a3ae 100644 --- a/src/components/modals/SettingsModal.stories.tsx +++ b/src/components/modals/SettingsModal.stories.tsx @@ -25,10 +25,10 @@ const meta = { export default meta; type Story = StoryObj; -/** APIキーなし、Autoモデル、残り回数表示 */ +/** APIキーなし、デフォルトモデル、残り回数表示 */ export const FreeMode: Story = { args: { - modelId: 'auto', + modelId: 'gpt-5.4-nano', connStatus: { status: 'idle', msg: '' }, apiKey: '', sessionCost: 0, @@ -40,11 +40,11 @@ export const FreeMode: Story = { /** APIキーあり、コスト表示 */ export const ProMode: Story = { args: { - modelId: 'auto', + modelId: 'gpt-5.4-nano', connStatus: { status: 'ok', msg: '接続成功' }, apiKey: 'sk-test-key-1234', sessionCost: 2.45, - lastUsedModel: 'gpt-5-nano', + lastUsedModel: 'gpt-5.4-nano', freeRemaining: null, }, }; @@ -52,11 +52,11 @@ export const ProMode: Story = { /** Free mode 残り少ない */ export const WithRateLimit: Story = { args: { - modelId: 'auto', + modelId: 'gpt-5.4-nano', connStatus: { status: 'idle', msg: '' }, apiKey: '', sessionCost: 0, - lastUsedModel: 'gpt-5-nano', + lastUsedModel: 'gpt-5.4-nano', freeRemaining: { remaining: 3, limit: 50, resetAt: Date.now() + 3600_000 }, }, }; diff --git a/src/components/modals/SettingsModal.tsx b/src/components/modals/SettingsModal.tsx index 3278170..249d35b 100644 --- a/src/components/modals/SettingsModal.tsx +++ b/src/components/modals/SettingsModal.tsx @@ -15,7 +15,7 @@ import { } from 'lucide-react'; import { MODELS, - AUTO_MODEL_ID, + DEFAULT_MODEL_ID, isProMode, isLocalProvider, PROVIDER_DEFAULTS, @@ -193,12 +193,16 @@ export const SettingsModal: React.FC = ({
- {isProMode(apiKey) && ( + {isProMode(apiKey) ? (

✓ プロモード有効 — フル機能・高深度分析が使えます

- )} - {!isProMode(apiKey) && ( + ) : apiKey.trim().length > 0 ? ( +

+ ⚠ APIキーの形式が正しくありません(sk-... + で始まる20文字以上のキーを入力してください) +

+ ) : (

入力するとプロモードに切り替わり、全機能が使えます

@@ -271,24 +275,21 @@ export const SettingsModal: React.FC = ({

モデル

{MODELS.map((m) => { - const isAuto = m.id === AUTO_MODEL_ID; + const isProOnly = m.id !== DEFAULT_MODEL_ID; + const locked = isProOnly && !proMode; const isSelected = modelId === m.id; - let cls: string; - if (isSelected && isAuto) { - cls = - 'bg-emerald-600 dark:bg-emerald-700 border-emerald-500 dark:border-emerald-500 text-white font-medium'; - } else if (isSelected) { - cls = - 'bg-slate-800 dark:bg-slate-700 border-slate-600 dark:border-slate-500 text-slate-100 font-medium'; - } else if (isAuto) { - cls = `${T.btnGhost} border-emerald-200 dark:border-emerald-700/60`; - } else { - cls = `${T.btnGhost} border-slate-200 dark:border-slate-700/60`; - } + const cls = locked + ? 'opacity-50 cursor-not-allowed border-slate-200 dark:border-slate-700/60 bg-slate-100 dark:bg-slate-800/40 text-slate-400 dark:text-slate-500' + : isSelected + ? 'bg-slate-800 dark:bg-slate-700 border-slate-600 dark:border-slate-500 text-slate-100 font-medium' + : `${T.btnGhost} border-slate-200 dark:border-slate-700/60`; return ( ); })}
-

- {modelId === AUTO_MODEL_ID - ? 'タスクに応じて最適なモデルを自動選択します(おすすめ)' - : '迷ったら「Auto」がおすすめです'} +

+ {proMode + ? '選択したモデルは自動で保存されます' + : 'APIキーを入力すると上位モデルが選択可能になります'}

{(sessionCost > 0 || lastUsedModel) && (
diff --git a/src/components/results/RichText.tsx b/src/components/results/RichText.tsx index c5c1d94..0b3b537 100644 --- a/src/components/results/RichText.tsx +++ b/src/components/results/RichText.tsx @@ -79,7 +79,7 @@ export const RichText: React.FC = React.memo(({ text }) => { const hdr = tableRows[0], body = tableRows.slice(2); elements.push( -
+
@@ -89,7 +89,7 @@ export const RichText: React.FC = React.memo(({ text }) => { .map((c, ci) => ( @@ -105,7 +105,7 @@ export const RichText: React.FC = React.memo(({ text }) => { .map((c, ci) => ( @@ -134,7 +134,7 @@ export const RichText: React.FC = React.memo(({ text }) => { } if (ln.startsWith('### ')) elements.push( -

+

{ri(ln.slice(4))}

, ); @@ -142,24 +142,24 @@ export const RichText: React.FC = React.memo(({ text }) => { elements.push(

{ri(ln.slice(3))}

, ); else if (ln.startsWith('# ')) elements.push( -

+

{ri(ln.slice(2))}

, ); else if (ln.startsWith('---')) - elements.push(
); + elements.push(
); else if (ln.match(/^[-*]\s/)) elements.push(
{ri(ln.replace(/^[-*]\s/, ''))} @@ -169,7 +169,7 @@ export const RichText: React.FC = React.memo(({ text }) => { elements.push(
{ln.match(/^(\d+)\./)?.[1] || ''}. @@ -177,7 +177,7 @@ export const RichText: React.FC = React.memo(({ text }) => { {ri(ln.replace(/^\d+\.\s/, ''))}
, ); - else if (ln.trim() === '') elements.push(
); + else if (ln.trim() === '') elements.push(
); else elements.push(

@@ -187,5 +187,5 @@ export const RichText: React.FC = React.memo(({ text }) => { i++; } if (inTable) flushTable(); - return

{elements}
; + return
{elements}
; }); diff --git a/src/components/support/SupportAIChat.tsx b/src/components/support/SupportAIChat.tsx index 46552e8..9458e66 100644 --- a/src/components/support/SupportAIChat.tsx +++ b/src/components/support/SupportAIChat.tsx @@ -4,7 +4,7 @@ import { T } from '../../constants/theme'; import { callAI, callAIWithKey, isProMode } from '../../constants/models'; import { SUPPORT_SYSTEM_PROMPT } from './supportData'; -const SUPPORT_MODEL = 'gpt-4.1-nano'; +const SUPPORT_MODEL = 'gpt-5.4-nano'; interface Message { role: 'user' | 'assistant'; diff --git a/src/components/support/supportData.ts b/src/components/support/supportData.ts index 755df28..0cf4b8d 100644 --- a/src/components/support/supportData.ts +++ b/src/components/support/supportData.ts @@ -63,7 +63,7 @@ export const FAQ_CATEGORIES: FAQCategory[] = [ }, { q: 'APIキーの利用料金は?', - a: 'gpt-4.1-nanoなら1回あたり約0.5〜0.8円、gpt-5-nanoでも数円程度です。通常の利用であれば月数百円程度に収まります。', + a: 'gpt-5.4-nanoなら1回あたり約0.5〜1円程度です。通常の利用であれば月数百円程度に収まります。', }, ], }, @@ -162,8 +162,8 @@ HR SaaS、DX推進、新規事業、マーケ戦略、オペ改革など多業 【設定】 ・APIキー: ヘッダー右端の歯車アイコンから設定。sk-...形式のOpenAI APIキー -・モデル選択: gpt-5-nano(最速), gpt-5-mini(バランス), gpt-4.1-mini(コスパ), gpt-4.1-nano(最安) -・API料金目安: gpt-4.1-nanoで1回0.5〜0.8円、gpt-5-nanoで数円程度 +・モデル選択: gpt-5.4-nano(最速・低コスト), gpt-5.4-mini(高精度) +・API料金目安: gpt-5.4-nanoで1回約0.5〜1円程度 【UI機能】 ・レイアウト: デスクトップでは左右パネルをドラッグまたはプリセット(30:70/40:60/50:50)で調整 diff --git a/src/constants/models.ts b/src/constants/models.ts index 4f2c59d..841fe4e 100644 --- a/src/constants/models.ts +++ b/src/constants/models.ts @@ -1,7 +1,10 @@ import { ModelInfo, ChatMessage, LLMProvider } from '../types'; -/** APIキーがPro mode(ユーザー所有)か判定 */ -export const isProMode = (apiKey: string): boolean => apiKey.trim().startsWith('sk-'); +/** APIキーがPro mode(ユーザー所有)か判定。OpenAI キーは sk- で始まり 20 文字以上 */ +export const isProMode = (apiKey: string): boolean => { + const k = apiKey.trim(); + return k.startsWith('sk-') && k.length >= 20; +}; const friendlyError = (status: number, body: string): string => { if (status === 429) @@ -30,7 +33,8 @@ const friendlyError = (status: number, body: string): string => { }; export const AUTO_MODEL_ID = 'auto'; -export const DEFAULT_MODEL_ID = AUTO_MODEL_ID; +export const DEFAULT_MODEL_ID = 'gpt-5.4-nano'; +export const MODEL_STORAGE_KEY = 'ai-brainstorm-model'; const API_ENDPOINT = '/api/openai'; @@ -52,19 +56,14 @@ export const PROVIDER_DEFAULTS: Record< export const isLocalProvider = (p: LLMProvider): boolean => p !== 'openai'; export const MODELS: ModelInfo[] = [ - { id: AUTO_MODEL_ID, label: 'Auto', t: '自動選択', cost: '$~$$' }, - { id: 'gpt-5-nano', label: '5 Nano', t: '最速', cost: '$' }, - { id: 'gpt-5-mini', label: '5 Mini', t: 'バランス', cost: '$$' }, - { id: 'gpt-4.1-mini', label: '4.1 Mini', t: 'コスパ', cost: '$' }, - { id: 'gpt-4.1-nano', label: '4.1 Nano', t: '最安', cost: '$' }, + { id: 'gpt-5.4-nano', label: '5.4 Nano', t: '最速・低コスト', cost: '$' }, + { id: 'gpt-5.4-mini', label: '5.4 Mini', t: '高精度', cost: '$$' }, ]; /** モデル別コスト単価 (JPY / 1M tokens) */ export const MODEL_COSTS: Record = { - 'gpt-5-nano': { inputPerM: 10, outputPerM: 40 }, - 'gpt-5-mini': { inputPerM: 50, outputPerM: 200 }, - 'gpt-4.1-mini': { inputPerM: 40, outputPerM: 160 }, - 'gpt-4.1-nano': { inputPerM: 10, outputPerM: 45 }, + 'gpt-5.4-nano': { inputPerM: 10, outputPerM: 40 }, + 'gpt-5.4-mini': { inputPerM: 50, outputPerM: 200 }, }; /** API呼び出し結果(usage トークン数 + rate limit 情報を含む) */ @@ -75,7 +74,7 @@ export interface APICallResult { } export const testConn = async (modelId: string, apiKey = ''): Promise => { - const resolvedId = modelId === AUTO_MODEL_ID ? 'gpt-5-nano' : modelId; + const resolvedId = modelId === AUTO_MODEL_ID ? DEFAULT_MODEL_ID : modelId; const usesCompletionTokens = resolvedId.startsWith('gpt-5') || resolvedId.startsWith('o'); const tokenParam = usesCompletionTokens ? { max_completion_tokens: 100 } : { max_tokens: 100 }; const headers: Record = { 'Content-Type': 'application/json' }; diff --git a/src/hooks/useAI.ts b/src/hooks/useAI.ts index 62b3fa6..554e50c 100644 --- a/src/hooks/useAI.ts +++ b/src/hooks/useAI.ts @@ -9,6 +9,8 @@ import { testConn, testConnLocal, DEFAULT_MODEL_ID, + MODEL_STORAGE_KEY, + MODELS, isProMode, isLocalProvider, } from '../constants/models'; @@ -52,7 +54,23 @@ function buildCompetitiveIntelContext(form: BrainstormForm): string { } export const useAI = () => { - const [modelId, setModelId] = useState(DEFAULT_MODEL_ID); + const [modelId, setModelIdState] = useState(() => { + try { + const saved = localStorage.getItem(MODEL_STORAGE_KEY); + if (saved && MODELS.some((m) => m.id === saved)) return saved; + } catch { + /* ignore */ + } + return DEFAULT_MODEL_ID; + }); + const setModelId = useCallback((id: string) => { + setModelIdState(id); + try { + localStorage.setItem(MODEL_STORAGE_KEY, id); + } catch { + /* ignore */ + } + }, []); const [connStatus, setConnStatus] = useState({ status: 'idle', msg: '' }); const [loading, setLoading] = useState(false); @@ -130,7 +148,9 @@ export const useAI = () => { ): Promise<{ content: string; resolvedModel: string }> => { const isLocal = isLocalProvider(provider); const pro = isLocal || isProMode(apiKey); - const currentModel = isLocal ? localModel || modelId : modelId; + // Free モードでは nano に強制(mini は Pro 専用) + const rawModel = isLocal ? localModel || modelId : modelId; + const currentModel = !pro && rawModel !== DEFAULT_MODEL_ID ? DEFAULT_MODEL_ID : rawModel; const decision = selectModel(currentModel, routerInput, pro, isLocal); const resolvedId = decision.modelId; diff --git a/src/utils/modelRouter.ts b/src/utils/modelRouter.ts index 2b94bc4..4a2461b 100644 --- a/src/utils/modelRouter.ts +++ b/src/utils/modelRouter.ts @@ -20,8 +20,8 @@ export function estimateTokens(messages: ChatMessage[]): number { return messages.reduce((sum, m) => sum + Math.ceil(m.content.length / 2), 0); } -const NANO = 'gpt-5-nano'; -const MINI = 'gpt-5-mini'; +const NANO = 'gpt-5.4-nano'; +const MINI = 'gpt-5.4-mini'; /** * Auto モード時のモデル自動選択 diff --git a/vite.config.ts b/vite.config.ts index bbc3e95..1d77510 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -19,8 +19,14 @@ export default defineConfig(({ mode }) => { rewrite: (path) => path.replace(/^\/api\/openai/, '/v1/chat/completions'), configure: (proxy) => { proxy.on('proxyReq', (proxyReq) => { - if (env.OPENAI_API_KEY) + // Pro モード: ユーザーの API キーを優先 + const userKey = proxyReq.getHeader('x-api-key') as string | undefined; + if (userKey) { + proxyReq.setHeader('Authorization', `Bearer ${userKey}`); + proxyReq.removeHeader('x-api-key'); + } else if (env.OPENAI_API_KEY) { proxyReq.setHeader('Authorization', `Bearer ${env.OPENAI_API_KEY}`); + } }); }, },
{c.trim()} {ri(c.trim())}