koneck 2.25.65 → 2.25.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ink-chat.js CHANGED
@@ -2,13 +2,13 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
2
2
  import React, { useState, useEffect, useRef } from 'react';
3
3
  import { Box, Static, Text, render, useApp, useInput, useStdin } from 'ink';
4
4
  import { execa } from 'execa';
5
- import { createAgentSession, buildClient, resolveAgentConcurrency, toolsUnsupportedNotice, isLocalRuntime } from './engine.js';
5
+ import { createAgentSession, buildClient, resolveAgentConcurrency, toolsUnsupportedNotice } from './engine.js';
6
6
  import { estimateCost, formatCost } from './pricing.js';
7
7
  import { generateSessionId, saveSession, listSessions, loadSession, relativeAge, renameSession, forkSession, archiveSession, findSession, } from './session.js';
8
8
  import { loadMemory } from './memory.js';
9
9
  import { loadKoneckConfig, saveKoneckConfig, setConfigKey, configKeyDescriptions, CONFIG_FILE, CONFIG_SCHEMA, stepValue, displayValue } from './config-store.js';
10
- import { PROVIDERS, resolveProvider, getApiKey, addUserProvider, userProvidersPath } from './providers.js';
11
- import { resolveEndpoint, normaliseEndpoint, withEndpoint, savedEndpoints, shortSource } from './endpoints.js';
10
+ import { PROVIDERS, resolveProvider, getApiKey, addUserProvider, userProvidersPath, needsNoCredential, servesLocally } from './providers.js';
11
+ import { resolveEndpoint, normaliseEndpoint, withEndpoint, savedEndpoints, shortSource, resolveModelChoice, withModel } from './endpoints.js';
12
12
  import { loadProjectConfig, loadGlobalConfig } from './config.js';
13
13
  import { listCheckpoints, revertCheckpoint, snapshotTree, filesTouchedBy } from './checkpoint.js';
14
14
  import { fileDiff, diffSummary } from './diff-view.js';
@@ -810,6 +810,8 @@ function App({ config: initialConfig, clearFrame }) {
810
810
  const incompleteRef = useRef(null);
811
811
  /** The model already reported as unable to call tools, so the notice is not repeated. */
812
812
  const toolsWarnedRef = useRef(null);
813
+ /** The crowded-window notice is about the setup, so it is said once per session. */
814
+ const crowdedWarnedRef = useRef(false);
813
815
  /**
814
816
  * The id to save under, mirrored in a ref.
815
817
  *
@@ -1031,16 +1033,25 @@ function App({ config: initialConfig, clearFrame }) {
1031
1033
  toolsWarnedRef.current = model;
1032
1034
  addSystem(toolsUnsupportedNotice(model));
1033
1035
  },
1036
+ onCrowdedWindow: (_fixed, _limit, advice) => {
1037
+ // Once per session: it is a property of the setup, not of the turn.
1038
+ if (crowdedWarnedRef.current)
1039
+ return;
1040
+ crowdedWarnedRef.current = true;
1041
+ addSystem(advice);
1042
+ },
1034
1043
  onLooping: (copies) => {
1035
1044
  // The loop itself is the symptom. On a local model the cause is almost always the window:
1036
1045
  // Ollama serves 4096 tokens unless told otherwise, whatever the model was trained for, and
1037
1046
  // a conversation that no longer fits is exactly what makes a model start repeating.
1038
- const local = isLocalRuntime(cfg.provider);
1047
+ // Judged by the endpoint, not the provider's name: a LAN Ollama reached through a slot
1048
+ // called "omni" is still Ollama, and asking about the name missed exactly that setup.
1049
+ const local = servesLocally(cfg.provider, endpointFor(cfg.provider).url);
1039
1050
  addSystem(`${cfg.model} repeated the same text ${copies} times, so the reply was cut short.` +
1040
1051
  (local
1041
- ? `\n\nThis is usually the context window rather than the model. Ollama serves 4096 ` +
1042
- `tokens by default however large the model is — set ` +
1043
- `\`OLLAMA_CONTEXT_LENGTH=32768\` and restart it, then \`/context\` to confirm.`
1052
+ ? `\n\nThis is usually the context window rather than the model. Ollama serves 4,096 ` +
1053
+ `tokens by default however large the model is — set \`OLLAMA_CONTEXT_LENGTH=32768\` ` +
1054
+ `where Ollama runs, restart it, then \`/context\` to confirm.`
1044
1055
  : ''));
1045
1056
  },
1046
1057
  onThinking: (text, totalChars) => {
@@ -1250,6 +1261,23 @@ function App({ config: initialConfig, clearFrame }) {
1250
1261
  }, [cfg.cwd]);
1251
1262
  /** Nearest first, which is the order resolveEndpoint reads them in. */
1252
1263
  const endpointLayers = (store = stored) => [outerLayers.project, store, outerLayers.global];
1264
+ /**
1265
+ * The model in force for a provider, with where the choice came from.
1266
+ *
1267
+ * A model belongs to a provider: qwen3-coder:30b means nothing to Anthropic. Reported as
1268
+ * "koneck keeps reverting the model to codellama" — which is the Ollama definition's built-in
1269
+ * default, reached because nothing else was ever saved against ollama.
1270
+ */
1271
+ function modelFor(provider, store) {
1272
+ const def = PROVIDERS[provider.trim().toLowerCase()];
1273
+ return resolveModelChoice(provider, endpointLayers(store), def?.defaultModel ?? 'auto');
1274
+ }
1275
+ /** Remembers a model against the provider it belongs to. */
1276
+ async function rememberModel(provider, model) {
1277
+ const saved = withModel(await loadKoneckConfig(), provider, model);
1278
+ await saveKoneckConfig(saved);
1279
+ setStored(saved);
1280
+ }
1253
1281
  /** The endpoint in force for a provider, with where it came from. */
1254
1282
  function endpointFor(provider, store) {
1255
1283
  const def = PROVIDERS[provider.trim().toLowerCase()];
@@ -1654,10 +1682,25 @@ function App({ config: initialConfig, clearFrame }) {
1654
1682
  /** The text an edit box opens on: what the row is showing. */
1655
1683
  function settingText(field) {
1656
1684
  const key = String(field.key);
1657
- const value = key === 'baseURL' || stored[key] === undefined ? liveSetting(key) : stored[key];
1685
+ const value = key === 'baseURL' || key === 'model' || stored[key] === undefined
1686
+ ? liveSetting(key) : stored[key];
1658
1687
  return value === undefined || value === null ? '' : String(value);
1659
1688
  }
1660
1689
  async function applySetting(field, value) {
1690
+ // The model row is one provider's model, so it is saved against the provider in force. In the
1691
+ // single shared slot, choosing a model for Ollama also chose it for Anthropic.
1692
+ if (field.key === 'model') {
1693
+ const name = typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
1694
+ const saved = withModel(await loadKoneckConfig(), cfg.provider, name);
1695
+ await saveKoneckConfig(saved);
1696
+ setStored(saved);
1697
+ const now = modelFor(cfg.provider, saved);
1698
+ const merged = { ...cfg, model: now.model };
1699
+ setCfg(merged);
1700
+ await resetSession(merged);
1701
+ addSystem(`**${cfg.provider}** → ${now.model} (${now.source})`);
1702
+ return;
1703
+ }
1661
1704
  // The endpoint row is not a plain setting: it is one provider's address, so it is saved
1662
1705
  // against the provider in force rather than into a single slot every provider shares.
1663
1706
  if (field.key === 'baseURL') {
@@ -1695,20 +1738,19 @@ function App({ config: initialConfig, clearFrame }) {
1695
1738
  setConfigMaxWidth(typeof value === 'number' && value >= 40 ? value : undefined);
1696
1739
  break;
1697
1740
  case 'provider': {
1698
- // Changing the provider here has to move the endpoint with it, or the new provider is
1699
- // addressed at the old one's URL the same fault /provider had.
1741
+ // Changing the provider here has to move the endpoint and the model with it, or the new
1742
+ // provider is addressed at the old one's URL and asked for the old one's model — the same
1743
+ // fault /provider had, in a second place.
1700
1744
  const name = typeof value === 'string' && value.trim() !== '' ? value.trim() : cfg.provider;
1701
- const def = PROVIDERS[name.toLowerCase()];
1702
1745
  const merged = {
1703
1746
  ...cfg, provider: name,
1704
- model: def?.defaultModel ?? cfg.model,
1747
+ model: resolveModelChoice(name, endpointLayers(next), PROVIDERS[name.toLowerCase()]?.defaultModel ?? 'auto').model,
1705
1748
  baseURL: endpointFor(name, next).url,
1706
1749
  };
1707
1750
  setCfg(merged);
1708
1751
  await resetSession(merged);
1709
1752
  break;
1710
1753
  }
1711
- case 'model':
1712
1754
  case 'maxTurns':
1713
1755
  case 'requireApproval':
1714
1756
  case 'agentConcurrency':
@@ -1799,12 +1841,16 @@ function App({ config: initialConfig, clearFrame }) {
1799
1841
  async function fetchModels() {
1800
1842
  const infos = await loadModelCatalog();
1801
1843
  const statuses = readModelStatus();
1844
+ const selfHosted = needsNoCredential(resolveProvider(cfg.provider, endpointFor(cfg.provider).url));
1802
1845
  return [...infos]
1803
1846
  .sort((a, b) => a.id.localeCompare(b.id))
1804
1847
  .map(m => {
1805
1848
  const st = statuses[`${cfg.provider}:${m.id}`];
1806
1849
  const parts = [
1807
- m.free ? 'free' : 'paid',
1850
+ // "paid" is a claim about somebody's billing, and a model on your own hardware has no
1851
+ // billing. The catalogue's free flag describes a gateway's tiers; it means nothing for
1852
+ // a runtime you are running, and calling qwen3-coder on a LAN box "paid" is just wrong.
1853
+ selfHosted ? 'local' : m.free ? 'free' : 'paid',
1808
1854
  ...(m.contextLength ? [`${humanTokens(m.contextLength)} ctx`] : []),
1809
1855
  ...(m.toolCalling === false ? ['no tools'] : []),
1810
1856
  ...(st && st.state !== 'ok' ? [stateLabel(st.state)] : st ? ['ready'] : []),
@@ -1904,7 +1950,10 @@ function App({ config: initialConfig, clearFrame }) {
1904
1950
  if (kind === 'model') {
1905
1951
  const next = { ...cfg, model: item.value };
1906
1952
  await resetSession(next);
1907
- addSystem(`Model ${item.value} (session reset)`);
1953
+ // Saved against the provider. Choosing a model used to change the running session and
1954
+ // nothing else, so it lasted until the next provider switch or the next start.
1955
+ await rememberModel(cfg.provider, item.value);
1956
+ addSystem(`Model → ${item.value} · remembered for ${cfg.provider} (session reset)`);
1908
1957
  return;
1909
1958
  }
1910
1959
  if (kind === 'provider') {
@@ -1914,9 +1963,12 @@ function App({ config: initialConfig, clearFrame }) {
1914
1963
  // not loopback and does need a key, whatever the provider is called.
1915
1964
  const endpoint = endpointFor(item.value);
1916
1965
  const resolved = resolveProvider(item.value, endpoint.url);
1966
+ // Whichever model was last used with this provider, not its built-in default. The old line
1967
+ // reached for PROVIDERS[name].defaultModel — `codellama` for Ollama — and wrote it to disk.
1968
+ const chosen = modelFor(item.value);
1917
1969
  const next = {
1918
1970
  ...cfg, provider: item.value,
1919
- model: def?.defaultModel ?? cfg.model,
1971
+ model: chosen.model,
1920
1972
  baseURL: resolved.baseURL,
1921
1973
  apiKey: undefined, // a key for the previous provider must not carry over
1922
1974
  };
@@ -1931,10 +1983,13 @@ function App({ config: initialConfig, clearFrame }) {
1931
1983
  }
1932
1984
  if (usable) {
1933
1985
  await resetSession(next);
1934
- const savedCfg = { ...(await loadKoneckConfig()), provider: item.value, model: next.model };
1986
+ // The provider is persisted; the model is not, because it is already stored against the
1987
+ // provider it belongs to. Writing it into the shared `model` key is what made a switch to
1988
+ // ollama leave `codellama` on disk for everything else to inherit.
1989
+ const savedCfg = { ...(await loadKoneckConfig()), provider: item.value };
1935
1990
  await saveKoneckConfig(savedCfg);
1936
1991
  setStored(savedCfg);
1937
- addSystem(`Provider → ${item.value} · model → ${next.model}\n` +
1992
+ addSystem(`Provider → ${item.value} · model → ${next.model} (${chosen.source})\n` +
1938
1993
  `Endpoint ${resolved.baseURL} (${endpoint.source}) · session reset`);
1939
1994
  return;
1940
1995
  }
@@ -2012,12 +2067,17 @@ function App({ config: initialConfig, clearFrame }) {
2012
2067
  }
2013
2068
  case '/model': {
2014
2069
  if (!arg) {
2015
- addSystem(`Current model: ${cfg.model}\nUsage: /model <name>`);
2070
+ const now = modelFor(cfg.provider);
2071
+ addSystem(`Current model: ${cfg.model} (${now.source})\n` +
2072
+ `Usage: /model <name> — remembered for ${cfg.provider}`);
2016
2073
  return;
2017
2074
  }
2018
2075
  const newCfg = { ...cfg, model: arg };
2019
2076
  await resetSession(newCfg);
2020
- addSystem(`Model ${arg} (session reset)`);
2077
+ // Persisted against the provider. It used to change the running session only, so the
2078
+ // next start or the next provider switch put the old model back.
2079
+ await rememberModel(cfg.provider, arg);
2080
+ addSystem(`Model → ${arg} · remembered for ${cfg.provider} (session reset)`);
2021
2081
  return;
2022
2082
  }
2023
2083
  case '/provider': {
@@ -2039,15 +2099,19 @@ function App({ config: initialConfig, clearFrame }) {
2039
2099
  // address with PROVIDERS[name].baseURL, which is how a configured Ollama kept coming back
2040
2100
  // as localhost.
2041
2101
  const endpoint = endpointFor(name);
2042
- const newCfg = { ...cfg, provider: name, model: def.defaultModel ?? cfg.model, baseURL: endpoint.url };
2102
+ // Whichever model was last used with this provider. Forcing def.defaultModel here and
2103
+ // then writing it to disk — is what "keeps reverting the model to codellama" was.
2104
+ const chosen = modelFor(name);
2105
+ const newCfg = { ...cfg, provider: name, model: chosen.model, baseURL: endpoint.url };
2043
2106
  await resetSession(newCfg);
2044
- // Persisted, or the switch is undone by the next start and the old provider wins again.
2045
- const savedCfg = { ...(await loadKoneckConfig()), provider: name, model: newCfg.model };
2107
+ // The provider is persisted, or the switch is undone by the next start. The model is not:
2108
+ // it already lives against the provider it belongs to.
2109
+ const savedCfg = { ...(await loadKoneckConfig()), provider: name };
2046
2110
  await saveKoneckConfig(savedCfg);
2047
2111
  setStored(savedCfg);
2048
- addSystem(`Provider → ${name} model → ${newCfg.model}\n` +
2112
+ addSystem(`Provider → ${name} model → ${chosen.model} (${chosen.source})\n` +
2049
2113
  `Endpoint ${endpoint.url} (${endpoint.source})\n` +
2050
- `Session reset. /endpoint <url> changes the address.`);
2114
+ `Session reset. /endpoint <url> changes the address, /model <name> the model.`);
2051
2115
  return;
2052
2116
  }
2053
2117
  case '/redraw': {
@@ -3550,7 +3614,8 @@ function App({ config: initialConfig, clearFrame }) {
3550
3614
  // An endpoint always has a source worth naming, so it never reads as unset or as
3551
3615
  // merely inherited: the question it answers is which file won.
3552
3616
  const isEndpoint = f.key === 'baseURL';
3553
- const here = !isEndpoint && stored[f.key] !== undefined;
3617
+ const isModel = f.key === 'model';
3618
+ const here = !isEndpoint && !isModel && stored[f.key] !== undefined;
3554
3619
  const value = here ? stored[f.key] : liveSetting(String(f.key));
3555
3620
  const raw = f.key === 'maxTurns' && (value === 0 || value === undefined)
3556
3621
  ? 'no limit'
@@ -3558,7 +3623,8 @@ function App({ config: initialConfig, clearFrame }) {
3558
3623
  const shown = beingTyped
3559
3624
  ? `${editing.value}_`
3560
3625
  : isEndpoint ? `${raw} (${shortSource(endpoint.source)})`
3561
- : raw + (here || value === undefined ? '' : ' (inherited)');
3626
+ : isModel ? `${raw} (${shortSource(modelFor(cfg.provider).source)})`
3627
+ : raw + (here || value === undefined ? '' : ' (inherited)');
3562
3628
  // The marker is what makes the selection legible without colour, which a panel line
3563
3629
  // drawn as one string cannot carry per-segment.
3564
3630
  const mark = selected ? '>' : ' ';
@@ -3643,7 +3709,8 @@ function App({ config: initialConfig, clearFrame }) {
3643
3709
  const st = statuses[`${cfg.provider}:${cfg.model}`];
3644
3710
  lines.push(`**In use** ${cfg.model}`);
3645
3711
  if (current) {
3646
- lines.push(` ${current.free ? 'free' : 'paid'}` +
3712
+ const hosted = needsNoCredential(resolveProvider(cfg.provider, endpointFor(cfg.provider).url));
3713
+ lines.push(` ${hosted ? 'local' : current.free ? 'free' : 'paid'}` +
3647
3714
  (current.contextLength ? ` · ${humanTokens(current.contextLength)} context` : '') +
3648
3715
  (current.maxOutput ? ` · ${humanTokens(current.maxOutput)} max out` : '') +
3649
3716
  (current.toolCalling === false ? ' · no tool calling' : ''));