koneck 2.25.65 → 2.25.66

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
@@ -7,8 +7,8 @@ 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 } 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';
@@ -1250,6 +1250,23 @@ function App({ config: initialConfig, clearFrame }) {
1250
1250
  }, [cfg.cwd]);
1251
1251
  /** Nearest first, which is the order resolveEndpoint reads them in. */
1252
1252
  const endpointLayers = (store = stored) => [outerLayers.project, store, outerLayers.global];
1253
+ /**
1254
+ * The model in force for a provider, with where the choice came from.
1255
+ *
1256
+ * A model belongs to a provider: qwen3-coder:30b means nothing to Anthropic. Reported as
1257
+ * "koneck keeps reverting the model to codellama" — which is the Ollama definition's built-in
1258
+ * default, reached because nothing else was ever saved against ollama.
1259
+ */
1260
+ function modelFor(provider, store) {
1261
+ const def = PROVIDERS[provider.trim().toLowerCase()];
1262
+ return resolveModelChoice(provider, endpointLayers(store), def?.defaultModel ?? 'auto');
1263
+ }
1264
+ /** Remembers a model against the provider it belongs to. */
1265
+ async function rememberModel(provider, model) {
1266
+ const saved = withModel(await loadKoneckConfig(), provider, model);
1267
+ await saveKoneckConfig(saved);
1268
+ setStored(saved);
1269
+ }
1253
1270
  /** The endpoint in force for a provider, with where it came from. */
1254
1271
  function endpointFor(provider, store) {
1255
1272
  const def = PROVIDERS[provider.trim().toLowerCase()];
@@ -1654,10 +1671,25 @@ function App({ config: initialConfig, clearFrame }) {
1654
1671
  /** The text an edit box opens on: what the row is showing. */
1655
1672
  function settingText(field) {
1656
1673
  const key = String(field.key);
1657
- const value = key === 'baseURL' || stored[key] === undefined ? liveSetting(key) : stored[key];
1674
+ const value = key === 'baseURL' || key === 'model' || stored[key] === undefined
1675
+ ? liveSetting(key) : stored[key];
1658
1676
  return value === undefined || value === null ? '' : String(value);
1659
1677
  }
1660
1678
  async function applySetting(field, value) {
1679
+ // The model row is one provider's model, so it is saved against the provider in force. In the
1680
+ // single shared slot, choosing a model for Ollama also chose it for Anthropic.
1681
+ if (field.key === 'model') {
1682
+ const name = typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
1683
+ const saved = withModel(await loadKoneckConfig(), cfg.provider, name);
1684
+ await saveKoneckConfig(saved);
1685
+ setStored(saved);
1686
+ const now = modelFor(cfg.provider, saved);
1687
+ const merged = { ...cfg, model: now.model };
1688
+ setCfg(merged);
1689
+ await resetSession(merged);
1690
+ addSystem(`**${cfg.provider}** → ${now.model} (${now.source})`);
1691
+ return;
1692
+ }
1661
1693
  // The endpoint row is not a plain setting: it is one provider's address, so it is saved
1662
1694
  // against the provider in force rather than into a single slot every provider shares.
1663
1695
  if (field.key === 'baseURL') {
@@ -1695,20 +1727,19 @@ function App({ config: initialConfig, clearFrame }) {
1695
1727
  setConfigMaxWidth(typeof value === 'number' && value >= 40 ? value : undefined);
1696
1728
  break;
1697
1729
  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.
1730
+ // Changing the provider here has to move the endpoint and the model with it, or the new
1731
+ // provider is addressed at the old one's URL and asked for the old one's model — the same
1732
+ // fault /provider had, in a second place.
1700
1733
  const name = typeof value === 'string' && value.trim() !== '' ? value.trim() : cfg.provider;
1701
- const def = PROVIDERS[name.toLowerCase()];
1702
1734
  const merged = {
1703
1735
  ...cfg, provider: name,
1704
- model: def?.defaultModel ?? cfg.model,
1736
+ model: resolveModelChoice(name, endpointLayers(next), PROVIDERS[name.toLowerCase()]?.defaultModel ?? 'auto').model,
1705
1737
  baseURL: endpointFor(name, next).url,
1706
1738
  };
1707
1739
  setCfg(merged);
1708
1740
  await resetSession(merged);
1709
1741
  break;
1710
1742
  }
1711
- case 'model':
1712
1743
  case 'maxTurns':
1713
1744
  case 'requireApproval':
1714
1745
  case 'agentConcurrency':
@@ -1799,12 +1830,16 @@ function App({ config: initialConfig, clearFrame }) {
1799
1830
  async function fetchModels() {
1800
1831
  const infos = await loadModelCatalog();
1801
1832
  const statuses = readModelStatus();
1833
+ const selfHosted = needsNoCredential(resolveProvider(cfg.provider, endpointFor(cfg.provider).url));
1802
1834
  return [...infos]
1803
1835
  .sort((a, b) => a.id.localeCompare(b.id))
1804
1836
  .map(m => {
1805
1837
  const st = statuses[`${cfg.provider}:${m.id}`];
1806
1838
  const parts = [
1807
- m.free ? 'free' : 'paid',
1839
+ // "paid" is a claim about somebody's billing, and a model on your own hardware has no
1840
+ // billing. The catalogue's free flag describes a gateway's tiers; it means nothing for
1841
+ // a runtime you are running, and calling qwen3-coder on a LAN box "paid" is just wrong.
1842
+ selfHosted ? 'local' : m.free ? 'free' : 'paid',
1808
1843
  ...(m.contextLength ? [`${humanTokens(m.contextLength)} ctx`] : []),
1809
1844
  ...(m.toolCalling === false ? ['no tools'] : []),
1810
1845
  ...(st && st.state !== 'ok' ? [stateLabel(st.state)] : st ? ['ready'] : []),
@@ -1904,7 +1939,10 @@ function App({ config: initialConfig, clearFrame }) {
1904
1939
  if (kind === 'model') {
1905
1940
  const next = { ...cfg, model: item.value };
1906
1941
  await resetSession(next);
1907
- addSystem(`Model → ${item.value} (session reset)`);
1942
+ // Saved against the provider. Choosing a model used to change the running session and
1943
+ // nothing else, so it lasted until the next provider switch or the next start.
1944
+ await rememberModel(cfg.provider, item.value);
1945
+ addSystem(`Model → ${item.value} · remembered for ${cfg.provider} (session reset)`);
1908
1946
  return;
1909
1947
  }
1910
1948
  if (kind === 'provider') {
@@ -1914,9 +1952,12 @@ function App({ config: initialConfig, clearFrame }) {
1914
1952
  // not loopback and does need a key, whatever the provider is called.
1915
1953
  const endpoint = endpointFor(item.value);
1916
1954
  const resolved = resolveProvider(item.value, endpoint.url);
1955
+ // Whichever model was last used with this provider, not its built-in default. The old line
1956
+ // reached for PROVIDERS[name].defaultModel — `codellama` for Ollama — and wrote it to disk.
1957
+ const chosen = modelFor(item.value);
1917
1958
  const next = {
1918
1959
  ...cfg, provider: item.value,
1919
- model: def?.defaultModel ?? cfg.model,
1960
+ model: chosen.model,
1920
1961
  baseURL: resolved.baseURL,
1921
1962
  apiKey: undefined, // a key for the previous provider must not carry over
1922
1963
  };
@@ -1931,10 +1972,13 @@ function App({ config: initialConfig, clearFrame }) {
1931
1972
  }
1932
1973
  if (usable) {
1933
1974
  await resetSession(next);
1934
- const savedCfg = { ...(await loadKoneckConfig()), provider: item.value, model: next.model };
1975
+ // The provider is persisted; the model is not, because it is already stored against the
1976
+ // provider it belongs to. Writing it into the shared `model` key is what made a switch to
1977
+ // ollama leave `codellama` on disk for everything else to inherit.
1978
+ const savedCfg = { ...(await loadKoneckConfig()), provider: item.value };
1935
1979
  await saveKoneckConfig(savedCfg);
1936
1980
  setStored(savedCfg);
1937
- addSystem(`Provider → ${item.value} · model → ${next.model}\n` +
1981
+ addSystem(`Provider → ${item.value} · model → ${next.model} (${chosen.source})\n` +
1938
1982
  `Endpoint ${resolved.baseURL} (${endpoint.source}) · session reset`);
1939
1983
  return;
1940
1984
  }
@@ -2012,12 +2056,17 @@ function App({ config: initialConfig, clearFrame }) {
2012
2056
  }
2013
2057
  case '/model': {
2014
2058
  if (!arg) {
2015
- addSystem(`Current model: ${cfg.model}\nUsage: /model <name>`);
2059
+ const now = modelFor(cfg.provider);
2060
+ addSystem(`Current model: ${cfg.model} (${now.source})\n` +
2061
+ `Usage: /model <name> — remembered for ${cfg.provider}`);
2016
2062
  return;
2017
2063
  }
2018
2064
  const newCfg = { ...cfg, model: arg };
2019
2065
  await resetSession(newCfg);
2020
- addSystem(`Model → ${arg} (session reset)`);
2066
+ // Persisted against the provider. It used to change the running session only, so the
2067
+ // next start or the next provider switch put the old model back.
2068
+ await rememberModel(cfg.provider, arg);
2069
+ addSystem(`Model → ${arg} · remembered for ${cfg.provider} (session reset)`);
2021
2070
  return;
2022
2071
  }
2023
2072
  case '/provider': {
@@ -2039,15 +2088,19 @@ function App({ config: initialConfig, clearFrame }) {
2039
2088
  // address with PROVIDERS[name].baseURL, which is how a configured Ollama kept coming back
2040
2089
  // as localhost.
2041
2090
  const endpoint = endpointFor(name);
2042
- const newCfg = { ...cfg, provider: name, model: def.defaultModel ?? cfg.model, baseURL: endpoint.url };
2091
+ // Whichever model was last used with this provider. Forcing def.defaultModel here — and
2092
+ // then writing it to disk — is what "keeps reverting the model to codellama" was.
2093
+ const chosen = modelFor(name);
2094
+ const newCfg = { ...cfg, provider: name, model: chosen.model, baseURL: endpoint.url };
2043
2095
  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 };
2096
+ // The provider is persisted, or the switch is undone by the next start. The model is not:
2097
+ // it already lives against the provider it belongs to.
2098
+ const savedCfg = { ...(await loadKoneckConfig()), provider: name };
2046
2099
  await saveKoneckConfig(savedCfg);
2047
2100
  setStored(savedCfg);
2048
- addSystem(`Provider → ${name} model → ${newCfg.model}\n` +
2101
+ addSystem(`Provider → ${name} model → ${chosen.model} (${chosen.source})\n` +
2049
2102
  `Endpoint ${endpoint.url} (${endpoint.source})\n` +
2050
- `Session reset. /endpoint <url> changes the address.`);
2103
+ `Session reset. /endpoint <url> changes the address, /model <name> the model.`);
2051
2104
  return;
2052
2105
  }
2053
2106
  case '/redraw': {
@@ -3550,7 +3603,8 @@ function App({ config: initialConfig, clearFrame }) {
3550
3603
  // An endpoint always has a source worth naming, so it never reads as unset or as
3551
3604
  // merely inherited: the question it answers is which file won.
3552
3605
  const isEndpoint = f.key === 'baseURL';
3553
- const here = !isEndpoint && stored[f.key] !== undefined;
3606
+ const isModel = f.key === 'model';
3607
+ const here = !isEndpoint && !isModel && stored[f.key] !== undefined;
3554
3608
  const value = here ? stored[f.key] : liveSetting(String(f.key));
3555
3609
  const raw = f.key === 'maxTurns' && (value === 0 || value === undefined)
3556
3610
  ? 'no limit'
@@ -3558,7 +3612,8 @@ function App({ config: initialConfig, clearFrame }) {
3558
3612
  const shown = beingTyped
3559
3613
  ? `${editing.value}_`
3560
3614
  : isEndpoint ? `${raw} (${shortSource(endpoint.source)})`
3561
- : raw + (here || value === undefined ? '' : ' (inherited)');
3615
+ : isModel ? `${raw} (${shortSource(modelFor(cfg.provider).source)})`
3616
+ : raw + (here || value === undefined ? '' : ' (inherited)');
3562
3617
  // The marker is what makes the selection legible without colour, which a panel line
3563
3618
  // drawn as one string cannot carry per-segment.
3564
3619
  const mark = selected ? '>' : ' ';
@@ -3643,7 +3698,8 @@ function App({ config: initialConfig, clearFrame }) {
3643
3698
  const st = statuses[`${cfg.provider}:${cfg.model}`];
3644
3699
  lines.push(`**In use** ${cfg.model}`);
3645
3700
  if (current) {
3646
- lines.push(` ${current.free ? 'free' : 'paid'}` +
3701
+ const hosted = needsNoCredential(resolveProvider(cfg.provider, endpointFor(cfg.provider).url));
3702
+ lines.push(` ${hosted ? 'local' : current.free ? 'free' : 'paid'}` +
3647
3703
  (current.contextLength ? ` · ${humanTokens(current.contextLength)} context` : '') +
3648
3704
  (current.maxOutput ? ` · ${humanTokens(current.maxOutput)} max out` : '') +
3649
3705
  (current.toolCalling === false ? ' · no tool calling' : ''));