diff --git a/src/gateway/server-v2.ts b/src/gateway/server-v2.ts index 3bbf154..f385d9f 100644 --- a/src/gateway/server-v2.ts +++ b/src/gateway/server-v2.ts @@ -12937,6 +12937,44 @@ app.get('/api/ollama/models', async (_req, res) => { } }); +// GET /api/model-context?model= — return context window size for a model +app.get('/api/model-context', requireAuth, async (req, res) => { + const model = String(req.query.model || '').trim(); + // Claude models always have 200K context + if (!model || model.toLowerCase().includes('claude')) { + res.json({ contextWindow: 200000 }); + return; + } + // For Ollama models, query /api/show + try { + const ollamaEndpoint = (getConfig().getConfig() as any).ollama?.endpoint || 'http://localhost:11434'; + const response = await fetch(`${ollamaEndpoint}/api/show`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: model }), + }); + if (!response.ok) { res.json({ contextWindow: null }); return; } + const data = await response.json() as any; + // Prefer model_info context length (most accurate) + let ctxWindow: number | null = null; + const modelInfo = data.model_info || {}; + for (const key of Object.keys(modelInfo)) { + if (key.endsWith('.context_length') && typeof modelInfo[key] === 'number') { + ctxWindow = modelInfo[key]; + break; + } + } + // Fallback: parse num_ctx from parameters string + if (!ctxWindow && typeof data.parameters === 'string') { + const m = data.parameters.match(/\bnum_ctx\s+(\d+)/); + if (m) ctxWindow = parseInt(m[1], 10); + } + res.json({ contextWindow: ctxWindow }); + } catch (err: any) { + res.json({ contextWindow: null, error: err.message }); + } +}); + import * as osModule from 'os'; diff --git a/web-ui/code.js b/web-ui/code.js index 2134470..69d42f7 100644 --- a/web-ui/code.js +++ b/web-ui/code.js @@ -3893,14 +3893,25 @@ function setCodeSbTab(tab) { } -function _codeAiCtxMax() { - const m = (codeAiModel || '').toLowerCase(); - if (!m || m.includes('claude')) return 200000; - // Common Ollama context windows - if (m.includes('llama3') || m.includes('llama-3') || m.includes('qwen')) return 128000; - if (m.includes('mistral') || m.includes('mixtral')) return 32000; - if (m.includes('gemma')) return 8192; - return 32000; // safe default for unknown local models +let _codeAiCtxMaxCached = 200000; // default until server responds +let _codeAiCtxMaxFetchedFor = ''; // model name the cache is valid for + +function _codeAiCtxMax() { return _codeAiCtxMaxCached; } + +async function _fetchModelCtxMax(model) { + const key = model || ''; + if (key === _codeAiCtxMaxFetchedFor) return; // already fetched for this model + _codeAiCtxMaxFetchedFor = key; + try { + const url = '/api/model-context' + (key ? `?model=${encodeURIComponent(key)}` : ''); + const r = await fetch(url, { headers: authHeaders() }); + if (!r.ok) return; + const d = await r.json(); + if (d.contextWindow) { + _codeAiCtxMaxCached = d.contextWindow; + updateSessionPanel(); + } + } catch (_) {} } function updateSessionPanel() { @@ -4143,6 +4154,8 @@ function csbModelChanged() { codeAiModel = sel ? sel.value : ''; localStorage.setItem('codeAiModel', codeAiModel); updateCodeAiLabel(); + _codeAiCtxMaxFetchedFor = ''; // invalidate cache so next fetch runs + _fetchModelCtxMax(codeAiModel); } function csbFontSize(val) { @@ -4460,6 +4473,7 @@ function setupCodeVResize() { try { const inp = document.getElementById('csb-termpath'); if (inp && localTermPath) inp.value = localTermPath; } catch (e) {} try { await restoreLocalDirHandle(); } catch (e) {} try { updateSessionPanel(); } catch (e) {} + try { _fetchModelCtxMax(codeAiModel); } catch (e) {} try { codeSbRefresh(); } catch (e) {} try { companionConnect(); } catch (e) {} // 히스토리 없으면 세션 목록 표시