neko-harness 0.4.5 → 0.4.6

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/gateway.js CHANGED
@@ -16497,14 +16497,16 @@ var GATEWAY_USAGE = `\u7528\u6CD5: neko serve [\u9009\u9879]
16497
16497
  --no-socket \u7981\u7528 Unix socket \u4F20\u8F93\uFF08\u4EC5 WebSocket\uFF09
16498
16498
  --server-id <id> \u903B\u8F91\u670D\u52A1\u5668\u8EAB\u4EFD\uFF08\u9ED8\u8BA4\u8BFB\u53D6/\u751F\u6210 <\u6570\u636E\u76EE\u5F55>/gateway-server-id\uFF09
16499
16499
  --cwd <dir> \u4F1A\u8BDD\u5DE5\u5177\u7684\u5DE5\u4F5C\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09
16500
- --model <ref> \u6A21\u578B\uFF0C\u683C\u5F0F provider/modelId\uFF08\u9ED8\u8BA4 deepseek/deepseek-flash\uFF09
16500
+ --model <ref> \u6A21\u578B\uFF0C\u683C\u5F0F provider/modelId\uFF08modelId \u53EF\u542B /\uFF0C\u9ED8\u8BA4 deepseek/deepseek-flash\uFF09
16501
16501
  --storage <path> SQLite \u5B58\u50A8\u6587\u4EF6\uFF08\u9ED8\u8BA4 ~/.neko/neko.sqlite\uFF09
16502
16502
  --credentials <path> \u7528\u6237\u51ED\u636E\u6587\u4EF6\uFF08\u9ED8\u8BA4 ~/.neko/credentials.json\uFF09
16503
16503
  --config <dir> neko.config.json \u67E5\u627E\u8D77\u59CB\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09
16504
16504
  -h, --help \u663E\u793A\u6B64\u5E2E\u52A9
16505
16505
 
16506
- \u53EF\u901A\u8FC7\u524D\u7AEF\u8BBE\u7F6E\u6216 DEEPSEEK_API_KEY / TAVILY_API_KEY \u914D\u7F6E\u51ED\u636E\uFF1B
16507
- \u8054\u7F51\u641C\u7D22\u670D\u52A1\u53EF\u5728\u524D\u7AEF\u8BBE\u7F6E\uFF0C\u6216\u7528\u914D\u7F6E\u6587\u4EF6 webSearch.provider\uFF08tavily / deepseek\uFF0C\u9ED8\u8BA4 tavily\uFF09\u3002`;
16506
+ \u53EF\u901A\u8FC7\u524D\u7AEF\u8BBE\u7F6E\u6216\u73AF\u5883\u53D8\u91CF\uFF08DEEPSEEK_API_KEY / OPENROUTER_API_KEY / OPENCODE_API_KEY / TAVILY_API_KEY \u7B49\uFF09\u914D\u7F6E\u51ED\u636E\uFF1B
16507
+ \u9664 DeepSeek \u5916\u7684\u6A21\u578B provider \u9700\u5148\u5728\u524D\u7AEF\u542F\u7528\uFF0C--model \u6307\u5411\u672A\u542F\u7528\u7684 provider \u65F6\u63D0\u4EA4\u4F1A\u88AB\u62D2\u7EDD\uFF1B
16508
+ \u8054\u7F51\u641C\u7D22\u670D\u52A1\u53EF\u5728\u524D\u7AEF\u8BBE\u7F6E\uFF0C\u6216\u7528\u914D\u7F6E\u6587\u4EF6 webSearch.provider\uFF08tavily / deepseek\uFF0C\u9ED8\u8BA4 tavily\uFF09\uFF1B
16509
+ \u9ED8\u8BA4\u6A21\u578B\u8FD8\u53EF\u5728 TUI /model \u6216 Web \u6A21\u578B\u9009\u62E9\u5668\u4E2D\u300C\u8BBE\u4E3A\u9ED8\u8BA4\u300D\uFF0C\u4F18\u5148\u7EA7\u4F4E\u4E8E --model\u3001\u9AD8\u4E8E\u914D\u7F6E\u6587\u4EF6 defaultModel\u3002`;
16508
16510
  function parseGatewayArgs(argv) {
16509
16511
  let port;
16510
16512
  const options = {};
@@ -16912,16 +16914,70 @@ import { homedir as homedir2 } from "os";
16912
16914
  import { dirname as dirname2, join as join2 } from "path";
16913
16915
 
16914
16916
  // packages/core/src/credential-providers.ts
16917
+ var model = (id, name, envVar, signupUrl) => ({ id, kind: "model", name, envVar, signupUrl, keyless: false });
16918
+ var MODEL_PROVIDERS = [
16919
+ model("deepseek", "DeepSeek", "DEEPSEEK_API_KEY", "https://platform.deepseek.com/api_keys"),
16920
+ model("openrouter", "OpenRouter", "OPENROUTER_API_KEY", "https://openrouter.ai/keys"),
16921
+ model("opencode-go", "OpenCode Go", "OPENCODE_API_KEY", "https://opencode.ai/auth"),
16922
+ model("opencode", "OpenCode Zen", "OPENCODE_API_KEY", "https://opencode.ai/auth"),
16923
+ model("openai", "OpenAI", "OPENAI_API_KEY", "https://platform.openai.com/api-keys"),
16924
+ model("google", "Google", "GEMINI_API_KEY", "https://aistudio.google.com/apikey"),
16925
+ model("groq", "Groq", "GROQ_API_KEY", "https://console.groq.com/keys"),
16926
+ model("mistral", "Mistral", "MISTRAL_API_KEY", "https://console.mistral.ai/api-keys"),
16927
+ model("xai", "xAI", "XAI_API_KEY", "https://console.x.ai"),
16928
+ model("together", "Together", "TOGETHER_API_KEY", "https://api.together.ai/settings/api-keys"),
16929
+ model("fireworks", "Fireworks", "FIREWORKS_API_KEY", "https://fireworks.ai/account/api-keys"),
16930
+ model("cerebras", "Cerebras", "CEREBRAS_API_KEY", "https://cloud.cerebras.ai"),
16931
+ model("nvidia", "NVIDIA", "NVIDIA_API_KEY", "https://build.nvidia.com/settings/api-keys"),
16932
+ model("huggingface", "Hugging Face", "HF_TOKEN", "https://huggingface.co/settings/tokens"),
16933
+ model("vercel-ai-gateway", "Vercel AI Gateway", "AI_GATEWAY_API_KEY", "https://vercel.com/ai-gateway"),
16934
+ model("moonshotai", "Moonshot AI", "MOONSHOT_API_KEY", "https://platform.moonshot.ai/console/api-keys"),
16935
+ model("moonshotai-cn", "Moonshot AI CN", "MOONSHOT_API_KEY", "https://platform.moonshot.cn/console/api-keys"),
16936
+ model("minimax", "MiniMax", "MINIMAX_API_KEY", "https://platform.minimax.io"),
16937
+ model("minimax-cn", "MiniMax CN", "MINIMAX_CN_API_KEY", "https://platform.minimaxi.com"),
16938
+ model("zai", "Z.AI", "ZAI_API_KEY", "https://z.ai/manage-apikey/apikey-list"),
16939
+ model("zai-coding-cn", "Z.AI Coding CN", "ZAI_CODING_CN_API_KEY", "https://bigmodel.cn/usercenter/proj-mgmt/apikeys"),
16940
+ model("qwen-token-plan", "Qwen Token Plan", "QWEN_TOKEN_PLAN_API_KEY", "https://modelstudio.console.alibabacloud.com"),
16941
+ model("qwen-token-plan-cn", "Qwen Token Plan CN", "QWEN_TOKEN_PLAN_CN_API_KEY", "https://bailian.console.aliyun.com"),
16942
+ model("qwen-token-plan-individual", "Qwen Token Plan Individual", "QWEN_TOKEN_PLAN_API_KEY", "https://modelstudio.console.alibabacloud.com"),
16943
+ model("xiaomi", "Xiaomi MiMo", "XIAOMI_API_KEY", "https://platform.xiaomimimo.com"),
16944
+ model("xiaomi-token-plan-cn", "Xiaomi Token Plan CN", "XIAOMI_TOKEN_PLAN_CN_API_KEY", "https://platform.xiaomimimo.com"),
16945
+ model("xiaomi-token-plan-ams", "Xiaomi Token Plan AMS", "XIAOMI_TOKEN_PLAN_AMS_API_KEY", "https://platform.xiaomimimo.com"),
16946
+ model("xiaomi-token-plan-sgp", "Xiaomi Token Plan SGP", "XIAOMI_TOKEN_PLAN_SGP_API_KEY", "https://platform.xiaomimimo.com"),
16947
+ model("baseten", "Baseten", "BASETEN_API_KEY", "https://app.baseten.co/settings/api_keys"),
16948
+ model("ant-ling", "Ant Ling", "ANT_LING_API_KEY", "https://ling.tbox.cn"),
16949
+ model("kimi-coding", "Kimi For Coding", "KIMI_API_KEY", "https://www.kimi.com/code/console"),
16950
+ model("meta", "Meta", "META_API_KEY", "https://llama.developer.meta.com")
16951
+ ];
16952
+ var SEARCH_CREDENTIAL_PROVIDERS = [
16953
+ {
16954
+ id: "tavily",
16955
+ kind: "search",
16956
+ name: "Tavily",
16957
+ envVar: "TAVILY_API_KEY",
16958
+ signupUrl: "https://app.tavily.com",
16959
+ keyless: true
16960
+ }
16961
+ ];
16915
16962
  var CREDENTIAL_PROVIDERS = [
16916
- { id: "deepseek", envVar: "DEEPSEEK_API_KEY", signupUrl: "https://platform.deepseek.com/api_keys", keyless: false },
16917
- { id: "tavily", envVar: "TAVILY_API_KEY", signupUrl: "https://app.tavily.com", keyless: true }
16963
+ ...MODEL_PROVIDERS,
16964
+ ...SEARCH_CREDENTIAL_PROVIDERS
16918
16965
  ];
16966
+ var DEFAULT_ENABLED_PROVIDERS = ["deepseek"];
16967
+ var LOCKED_PROVIDERS = ["deepseek"];
16968
+ function isModelProvider(id) {
16969
+ return MODEL_PROVIDERS.some((item) => item.id === id);
16970
+ }
16919
16971
  function credentialProvider(id) {
16920
16972
  const spec = CREDENTIAL_PROVIDERS.find((item) => item.id === id);
16921
16973
  if (!spec)
16922
16974
  throw new Error(`Unknown credential provider: ${id}`);
16923
16975
  return spec;
16924
16976
  }
16977
+ function normalizeEnabledProviders(saved) {
16978
+ const wanted = new Set([...LOCKED_PROVIDERS, ...saved ?? DEFAULT_ENABLED_PROVIDERS]);
16979
+ return MODEL_PROVIDERS.map((item) => item.id).filter((id) => wanted.has(id));
16980
+ }
16925
16981
 
16926
16982
  // packages/core/src/credentials.ts
16927
16983
  import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
@@ -17815,7 +17871,7 @@ var structuredOutputSchema = Type.Union([
17815
17871
  next: Type.Union([Type.Integer(), Type.Null()])
17816
17872
  }, { additionalProperties: false })
17817
17873
  ]);
17818
- function createSubagentExtension(prepare, discard) {
17874
+ function createSubagentExtension(prepare, discard, guard) {
17819
17875
  const finish = async (tx, input, status, report) => {
17820
17876
  const round = (await tx.doc(DelegationsDoc, input.parentId)).items.find((r) => r.taskId === input.taskId);
17821
17877
  round.status = status;
@@ -17959,6 +18015,8 @@ function createSubagentExtension(prepare, discard) {
17959
18015
  if (!args.task.trim() || !args.title.trim() || Buffer.byteLength(args.task) > 32768)
17960
18016
  throw new Error("Task/title must be nonempty; task limit is 32 KiB.");
17961
18017
  const agent = await api.agent(context);
18018
+ if (agent.model)
18019
+ await guard?.(agent.model);
17962
18020
  const round = await api.commit(async (tx) => {
17963
18021
  const rounds = await refreshDelegations(tx, api.conversationId);
17964
18022
  const receipt = rounds.find((r) => r.taskId === api.taskId);
@@ -18868,7 +18926,7 @@ import { homedir as homedir4 } from "os";
18868
18926
  import { join as join5 } from "path";
18869
18927
  import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT3 } from "@earendil-works/chord/context";
18870
18928
  import { clampThinkingLevel, createModels, getSupportedThinkingLevels as getSupportedThinkingLevels2 } from "@earendil-works/pi-ai/models";
18871
- import { deepseekProvider } from "@earendil-works/pi-ai/providers/deepseek";
18929
+ import { builtinProviders } from "@earendil-works/pi-ai/providers/all";
18872
18930
  import {
18873
18931
  AssistantEntry as AssistantEntry2,
18874
18932
  createRegistry,
@@ -20443,7 +20501,7 @@ function mergeUsage(base, extra) {
20443
20501
  }
20444
20502
  return result;
20445
20503
  }
20446
- function namingTask() {
20504
+ function namingTask(canUse) {
20447
20505
  let active = 0;
20448
20506
  const waits = new Set;
20449
20507
  return defineTask3({
@@ -20480,7 +20538,7 @@ function namingTask() {
20480
20538
  try {
20481
20539
  const saved = await runtime.snapshot(SessionMetadataDoc, runtime.conversationId, context);
20482
20540
  const model = saved?.model && runtime.models.getModel(saved.model.provider, saved.model.modelId);
20483
- if (!saved || !model || saved.attempts >= 2 || !getSupportedThinkingLevels(model).includes("off")) {
20541
+ if (!saved || !model || saved.attempts >= 2 || !getSupportedThinkingLevels(model).includes("off") || !await canUse(model.provider, model.id)) {
20484
20542
  await runtime.commit(async (tx) => {
20485
20543
  (await tx.doc(SessionMetadataDoc, runtime.conversationId)).naming = "fallback";
20486
20544
  return { status: "terminal", outcome: { status: "completed", result: null } };
@@ -20560,12 +20618,13 @@ class SessionMetadataManager {
20560
20618
  #known = new Set;
20561
20619
  #unsubscribe;
20562
20620
  #live = new Map;
20563
- task = namingTask();
20621
+ task;
20564
20622
  harness;
20565
20623
  storage;
20566
20624
  context;
20567
20625
  report;
20568
- constructor(harness, storage, context, registry, report = console.error) {
20626
+ constructor(harness, storage, context, registry, report = console.error, canUseModel = async () => true) {
20627
+ this.task = namingTask(canUseModel);
20569
20628
  this.harness = harness;
20570
20629
  this.storage = storage;
20571
20630
  this.context = context;
@@ -21852,6 +21911,22 @@ var SearchSettingsDoc = defineDoc7({
21852
21911
  fork: "initial",
21853
21912
  initial: () => ({ provider: null })
21854
21913
  });
21914
+ var ProviderSettingsDoc = defineDoc7({
21915
+ kind: "app.neko.provider-settings",
21916
+ version: 1,
21917
+ scope: "conversation",
21918
+ history: "latest",
21919
+ fork: "initial",
21920
+ initial: () => ({ enabled: null })
21921
+ });
21922
+ var DefaultModelDoc = defineDoc7({
21923
+ kind: "app.neko.default-model",
21924
+ version: 1,
21925
+ scope: "conversation",
21926
+ history: "latest",
21927
+ fork: "initial",
21928
+ initial: () => ({ model: null })
21929
+ });
21855
21930
  var ContextSettingsDoc = defineDoc7({
21856
21931
  kind: "app.neko.context-settings",
21857
21932
  version: 1,
@@ -21872,6 +21947,7 @@ async function createNekoRuntime(options = {}) {
21872
21947
  const context = options.context ?? BACKGROUND_CONTEXT3;
21873
21948
  const defaultCwd = normalizeWorkspace(options.cwd);
21874
21949
  const model = options.model ?? DEFAULT_MODEL;
21950
+ const modelOverride = options.modelOverride;
21875
21951
  const providers = options.providers ?? [];
21876
21952
  const credentials = options.credentials ?? (options.storage === "memory" && !options.credentialsPath ? new InMemoryCredentialStore : new FileCredentialStore(options.credentialsPath));
21877
21953
  const runtimeKeys = new Map;
@@ -21891,15 +21967,45 @@ async function createNekoRuntime(options = {}) {
21891
21967
  delete: (id) => credentials.delete(id)
21892
21968
  } : credentials;
21893
21969
  const models = createModels({ credentials: effectiveCredentials });
21894
- models.setProvider(deepseekProvider());
21970
+ for (const provider of builtinProviders())
21971
+ if (isModelProvider(provider.id))
21972
+ models.setProvider(provider);
21895
21973
  for (const provider of providers)
21896
21974
  models.setProvider(provider);
21975
+ let enabled = new Set(normalizeEnabledProviders(null));
21976
+ const guardProvider = async (ref) => {
21977
+ if (!ref || !isModelProvider(ref.provider))
21978
+ return;
21979
+ const spec = credentialProvider(ref.provider);
21980
+ if (!enabled.has(ref.provider))
21981
+ throw new Error(`PROVIDER_DISABLED: ${spec.name} is not enabled. Enable it in /login or Web settings, or switch model.`);
21982
+ if (!await models.checkAuth(ref.provider))
21983
+ throw new Error(`PROVIDER_KEY_REQUIRED: Configure a ${spec.name} API key in /login or Web settings.`);
21984
+ };
21897
21985
  const resolveLevel = (ref, level) => {
21898
21986
  const info = models.getModel(ref.provider, ref.modelId);
21899
21987
  if (!info)
21900
21988
  throw new Error("Unknown model");
21901
21989
  return clampThinkingLevel(info, level);
21902
21990
  };
21991
+ let modelInfoCache;
21992
+ const allModels = () => {
21993
+ modelInfoCache ??= models.getModels().map((model) => ({
21994
+ provider: model.provider,
21995
+ modelId: model.id,
21996
+ name: model.name,
21997
+ contextWindow: model.contextWindow,
21998
+ input: model.input,
21999
+ ...model.inputLimits ? { inputLimits: model.inputLimits } : {},
22000
+ reasoning: model.reasoning,
22001
+ supportedThinkingLevels: getSupportedThinkingLevels2(model),
22002
+ thinkingLevelAdjustments: Object.fromEntries(["off", "minimal", "low", "medium", "high", "xhigh", "max"].map((level) => [
22003
+ level,
22004
+ clampThinkingLevel(model, level)
22005
+ ]))
22006
+ }));
22007
+ return modelInfoCache;
22008
+ };
21903
22009
  const workspaceFiles = new WorkspaceFiles;
21904
22010
  const storage = options.storage === "memory" ? new MemoryStorage : await openNodeSqliteStorage(options.storagePath ?? DEFAULT_STORAGE_PATH);
21905
22011
  const registry = createRegistry();
@@ -21926,7 +22032,7 @@ async function createNekoRuntime(options = {}) {
21926
22032
  registry.install(createNekoPrompt(resourceSnapshots.current));
21927
22033
  registry.install(createSubagentExtension(async (id, requestId, cwd) => {
21928
22034
  resourceSnapshots.stage(id, requestId, cwd, await loadResources(cwd));
21929
- }, resourceSnapshots.discard));
22035
+ }, resourceSnapshots.discard, (ref) => guardProvider(ref)));
21930
22036
  const jobs = new JobManager;
21931
22037
  registry.install(jobs.extension);
21932
22038
  registry.install(createTodoExtension());
@@ -21944,7 +22050,7 @@ async function createNekoRuntime(options = {}) {
21944
22050
  registry,
21945
22051
  settings: {
21946
22052
  get compaction() {
21947
- return compactionPolicy(contextState, models.getModels().map((model) => model.contextWindow));
22053
+ return compactionPolicy(contextState, allModels().filter((model) => !isModelProvider(model.provider) || enabled.has(model.provider)).map((model) => model.contextWindow));
21948
22054
  }
21949
22055
  },
21950
22056
  onReport: options.onReport,
@@ -21986,13 +22092,15 @@ async function createNekoRuntime(options = {}) {
21986
22092
  throw error;
21987
22093
  }
21988
22094
  await questions.reconcileAll();
22095
+ const rootModel = modelOverride ?? model;
21989
22096
  const conversation = await harness.root(context, {
21990
22097
  agent: {
21991
- model,
21992
- thinkingLevel: resolveLevel(model, options.thinkingLevel ?? "high"),
22098
+ model: rootModel,
22099
+ thinkingLevel: resolveLevel(rootModel, options.thinkingLevel ?? "high"),
21993
22100
  cwd: defaultCwd
21994
22101
  }
21995
22102
  });
22103
+ enabled = new Set(normalizeEnabledProviders((await harness.snapshot(ProviderSettingsDoc, conversation.id, context))?.enabled));
21996
22104
  const savedSearch = (await harness.snapshot(SearchSettingsDoc, conversation.id, context))?.provider;
21997
22105
  if (isSearchProvider(savedSearch))
21998
22106
  searchProvider = savedSearch;
@@ -22038,7 +22146,7 @@ async function createNekoRuntime(options = {}) {
22038
22146
  };
22039
22147
  if (existing.length === 0)
22040
22148
  await touchConversation(conversation.id);
22041
- const metadata = new SessionMetadataManager(harness, storage, context, registry, options.onReport);
22149
+ const metadata = new SessionMetadataManager(harness, storage, context, registry, options.onReport, (provider, modelId) => guardProvider({ provider, modelId }).then(() => true, () => false));
22042
22150
  if (existing.length === 0)
22043
22151
  await metadata.initialize(conversation.id);
22044
22152
  for (const record of existing)
@@ -22099,9 +22207,8 @@ async function createNekoRuntime(options = {}) {
22099
22207
  data.skill = skill;
22100
22208
  });
22101
22209
  }
22102
- if (!duplicate && agent.model?.provider === "deepseek" && !await models.checkAuth("deepseek")) {
22103
- throw new Error("DEEPSEEK_KEY_REQUIRED: Configure a DeepSeek API key in /login or Web settings.");
22104
- }
22210
+ if (!duplicate)
22211
+ await guardProvider(agent.model);
22105
22212
  if (!duplicate && agent.model) {
22106
22213
  const resolved = models.getModel(agent.model.provider, agent.model.modelId);
22107
22214
  if (resolved)
@@ -22233,8 +22340,7 @@ async function createNekoRuntime(options = {}) {
22233
22340
  },
22234
22341
  async compact(instructions) {
22235
22342
  const agent = await target.agent(context);
22236
- if (agent.model?.provider === "deepseek" && !await models.checkAuth("deepseek"))
22237
- throw new Error("DEEPSEEK_KEY_REQUIRED: Configure DeepSeek in /login or Web settings.");
22343
+ await guardProvider(agent.model);
22238
22344
  await admit(target.id, async () => {
22239
22345
  const live = await harness.snapshot(importLiveDoc, target.id, context);
22240
22346
  if (live?.compactions?.length || (await readCompaction(harness, storage, target.id, context))?.phase === "waiting-to-apply")
@@ -22460,11 +22566,13 @@ async function createNekoRuntime(options = {}) {
22460
22566
  },
22461
22567
  async createConversation(createOptions) {
22462
22568
  const workspace = await addWorkspace(createOptions?.cwd ?? defaultCwd);
22569
+ const savedDefault = (await harness.snapshot(DefaultModelDoc, conversation.id, context))?.model ?? null;
22570
+ const effective = createOptions?.model ?? modelOverride ?? savedDefault ?? model;
22463
22571
  const created = await harness.createConversation({
22464
22572
  ownership: { kind: "ownerless" },
22465
22573
  agent: {
22466
- model: createOptions?.model ?? model,
22467
- thinkingLevel: resolveLevel(createOptions?.model ?? model, createOptions?.thinkingLevel ?? options.thinkingLevel ?? "high"),
22574
+ model: effective,
22575
+ thinkingLevel: resolveLevel(effective, createOptions?.thinkingLevel ?? options.thinkingLevel ?? "high"),
22468
22576
  cwd: workspace.path
22469
22577
  }
22470
22578
  }, context);
@@ -22494,6 +22602,8 @@ async function createNekoRuntime(options = {}) {
22494
22602
  const targetModel = ref && models.getModel(ref.provider, ref.modelId);
22495
22603
  if (!targetModel)
22496
22604
  throw new Error("Unknown model");
22605
+ if (change.model && isModelProvider(ref.provider) && !enabled.has(ref.provider))
22606
+ throw new Error(`PROVIDER_DISABLED: ${credentialProvider(ref.provider).name} is not enabled.`);
22497
22607
  await session.conversation.configure({ model: ref, thinkingLevel: clampThinkingLevel(targetModel, change.thinkingLevel ?? current.thinkingLevel) }, context);
22498
22608
  return true;
22499
22609
  });
@@ -22548,20 +22658,62 @@ async function createNekoRuntime(options = {}) {
22548
22658
  }
22549
22659
  },
22550
22660
  listModels() {
22551
- return models.getModels().map((model) => ({
22552
- provider: model.provider,
22553
- modelId: model.id,
22554
- name: model.name,
22555
- contextWindow: model.contextWindow,
22556
- input: model.input,
22557
- ...model.inputLimits ? { inputLimits: model.inputLimits } : {},
22558
- reasoning: model.reasoning,
22559
- supportedThinkingLevels: getSupportedThinkingLevels2(model),
22560
- thinkingLevelAdjustments: Object.fromEntries(["off", "minimal", "low", "medium", "high", "xhigh", "max"].map((level) => [
22561
- level,
22562
- clampThinkingLevel(model, level)
22563
- ]))
22564
- }));
22661
+ return allModels();
22662
+ },
22663
+ async listSelectableModels() {
22664
+ const availability = new Map;
22665
+ const result = [];
22666
+ for (const info of allModels()) {
22667
+ const managed = isModelProvider(info.provider);
22668
+ if (managed && !enabled.has(info.provider))
22669
+ continue;
22670
+ if (!availability.has(info.provider))
22671
+ availability.set(info.provider, !managed || Boolean(await models.checkAuth(info.provider)));
22672
+ result.push({ ...info, available: availability.get(info.provider) ?? false });
22673
+ }
22674
+ return result;
22675
+ },
22676
+ enabledProviders() {
22677
+ return MODEL_PROVIDERS.map((item) => item.id).filter((id) => enabled.has(id));
22678
+ },
22679
+ async setProviderEnabled(provider, value) {
22680
+ if (!isModelProvider(provider))
22681
+ throw new Error("Unknown model provider.");
22682
+ if (typeof value !== "boolean")
22683
+ throw new Error("Enabled must be a boolean.");
22684
+ if (!value && LOCKED_PROVIDERS.includes(provider))
22685
+ throw new Error("DeepSeek is always enabled.");
22686
+ const next = new Set(enabled);
22687
+ if (value)
22688
+ next.add(provider);
22689
+ else
22690
+ next.delete(provider);
22691
+ const list = normalizeEnabledProviders([...next]);
22692
+ await harness.commit(async (tx) => {
22693
+ (await tx.doc(ProviderSettingsDoc, conversation.id)).enabled = list;
22694
+ }, context);
22695
+ enabled = new Set(list);
22696
+ },
22697
+ async defaultModel() {
22698
+ const saved = (await harness.snapshot(DefaultModelDoc, conversation.id, context))?.model ?? null;
22699
+ if (modelOverride)
22700
+ return { effective: modelOverride, source: "cli" };
22701
+ if (saved)
22702
+ return { effective: saved, source: "saved" };
22703
+ if (options.model)
22704
+ return { effective: model, source: "config" };
22705
+ return { effective: model, source: "default" };
22706
+ },
22707
+ async setDefaultModel(ref) {
22708
+ if (ref !== null) {
22709
+ if (!models.getModel(ref.provider, ref.modelId))
22710
+ throw new Error("Unknown model");
22711
+ if (isModelProvider(ref.provider) && !enabled.has(ref.provider))
22712
+ throw new Error(`PROVIDER_DISABLED: ${credentialProvider(ref.provider).name} is not enabled.`);
22713
+ }
22714
+ await harness.commit(async (tx) => {
22715
+ (await tx.doc(DefaultModelDoc, conversation.id)).model = ref;
22716
+ }, context);
22565
22717
  },
22566
22718
  async getAgent(id) {
22567
22719
  const session = await sessionFor(id);
@@ -22626,8 +22778,8 @@ var nullableString = Type7.Union([str, nil]);
22626
22778
  var phase = Type7.Enum(["pending", "running", "done"]);
22627
22779
  var level = Type7.Enum(["project", "user", "builtin"]);
22628
22780
  var thinking = Type7.Enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
22629
- var model = Type7.Object({ provider: str, modelId: str });
22630
- var change = Type7.Object({ model: Type7.Optional(model), thinkingLevel: Type7.Optional(thinking) }, { additionalProperties: false });
22781
+ var model2 = Type7.Object({ provider: str, modelId: str });
22782
+ var change = Type7.Object({ model: Type7.Optional(model2), thinkingLevel: Type7.Optional(thinking) }, { additionalProperties: false });
22631
22783
  var usage = Type7.Object({ input: num, output: num, cacheRead: num, cacheWrite: num, cost: num });
22632
22784
  var image = Type7.Object({
22633
22785
  name: str,
@@ -22865,7 +23017,7 @@ var ChatViewSchema = Type7.Object({
22865
23017
  subagents: Type7.Optional(Type7.Array(DelegationSchema)),
22866
23018
  taskUsage: Type7.Optional(usage),
22867
23019
  model: str,
22868
- agent: Type7.Object({ model: Type7.Union([model, nil]), thinkingLevel: Type7.Union([thinking, nil]) }),
23020
+ agent: Type7.Object({ model: Type7.Union([model2, nil]), thinkingLevel: Type7.Union([thinking, nil]) }),
22869
23021
  cwd: str,
22870
23022
  busy: bool,
22871
23023
  status: nullableString,
@@ -22950,7 +23102,7 @@ var SessionSummarySchema = Type7.Object({
22950
23102
  firstPromptPreview: str,
22951
23103
  createdAt: num,
22952
23104
  lastMessageAt: Type7.Union([num, nil]),
22953
- model: Type7.Union([model, nil]),
23105
+ model: Type7.Union([model2, nil]),
22954
23106
  status: Type7.Enum(["idle", "running", "error"]),
22955
23107
  awaitingAnswer: Type7.Optional(bool),
22956
23108
  id: str
@@ -22961,11 +23113,12 @@ var WorkspaceStateSchema = Type7.Object({ revision: integer, workspaces: Type7.A
22961
23113
  var credentialStatus = Type7.Object({
22962
23114
  savedConfigured: bool,
22963
23115
  environmentConfigured: bool,
22964
- source: Type7.Enum(["saved", "environment", "runtime", "keyless", "none"])
23116
+ source: Type7.Enum(["saved", "environment", "runtime", "keyless", "none"]),
23117
+ enabled: Type7.Optional(bool)
22965
23118
  });
22966
23119
  var CredentialStateSchema = Type7.Object({
22967
23120
  revision: integer,
22968
- statuses: Type7.Object({ deepseek: credentialStatus, tavily: credentialStatus })
23121
+ statuses: Type7.Record(str, credentialStatus)
22969
23122
  });
22970
23123
  var AttachmentSchema = Type7.Object({ sessionId: str, attachmentId: str });
22971
23124
  var chatInput = Type7.Object({ sessionId: str, attachmentId: str }, { additionalProperties: false });
@@ -23007,6 +23160,7 @@ var modelInfo = Type7.Object({
23007
23160
  provider: str,
23008
23161
  modelId: str,
23009
23162
  name: str,
23163
+ available: bool,
23010
23164
  contextWindow: num,
23011
23165
  input: Type7.Array(str),
23012
23166
  inputLimits: Type7.Optional(inputLimits),
@@ -23112,16 +23266,17 @@ var NekoWorkspaces = contract("neko.workspaces", {
23112
23266
  }
23113
23267
  }
23114
23268
  });
23115
- var provider = Type7.Enum(["deepseek", "tavily"]);
23269
+ var provider = Type7.String({ minLength: 1, maxLength: 64 });
23116
23270
  var NekoCredentials = contract("neko.credentials", {
23117
- version: 1,
23271
+ version: 2,
23118
23272
  input: Type7.Object({}, { additionalProperties: false }),
23119
23273
  variants: {
23120
23274
  ready: {
23121
23275
  state: { state: CredentialStateSchema },
23122
23276
  methods: {
23123
23277
  save: rpc(Type7.Object({ provider, key: Type7.String({ minLength: 1, maxLength: 8192 }) }, { additionalProperties: false }), nil),
23124
- remove: rpc(Type7.Object({ provider }, { additionalProperties: false }), nil)
23278
+ remove: rpc(Type7.Object({ provider }, { additionalProperties: false }), nil),
23279
+ setEnabled: rpc(Type7.Object({ provider, enabled: bool }, { additionalProperties: false }), nil)
23125
23280
  }
23126
23281
  }
23127
23282
  }
@@ -23198,6 +23353,24 @@ var NekoContextSettings = contract("neko.context-settings", {
23198
23353
  }
23199
23354
  }
23200
23355
  });
23356
+ var defaultModelSource = Type7.Enum(["cli", "saved", "config", "default"]);
23357
+ var DefaultModelStateSchema = Type7.Object({
23358
+ effective: model2,
23359
+ source: defaultModelSource
23360
+ });
23361
+ var NekoDefaultModel = contract("neko.default-model", {
23362
+ version: 1,
23363
+ input: Type7.Object({}, { additionalProperties: false }),
23364
+ variants: {
23365
+ ready: {
23366
+ state: { state: DefaultModelStateSchema },
23367
+ methods: {
23368
+ set: rpc(model2, nil),
23369
+ clear: rpc(nil, nil)
23370
+ }
23371
+ }
23372
+ }
23373
+ });
23201
23374
  var FileCandidatesSchema = Type7.Object({
23202
23375
  entries: Type7.Array(Type7.Object({ name: str, path: str, kind: Type7.Enum(["file", "directory"]) }), { maxItems: 100 }),
23203
23376
  limited: bool,
@@ -23976,8 +24149,8 @@ function subagentClient(connect) {
23976
24149
  import { createHash as createHash2 } from "crypto";
23977
24150
 
23978
24151
  // packages/core/src/application-version.ts
23979
- var RUNNING_VERSION = "0.4.5";
23980
- var APPLICATION_PROTOCOL = 6;
24152
+ var RUNNING_VERSION = "0.4.6";
24153
+ var APPLICATION_PROTOCOL = 8;
23981
24154
  function stableVersion(value) {
23982
24155
  return typeof value === "string" && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(value) && value.split(".").every((n) => Number.isSafeInteger(Number(n)));
23983
24156
  }
@@ -27995,8 +28168,6 @@ function serveSessions(host, runtime, context) {
27995
28168
  for (const id of input.images ?? [])
27996
28169
  uploads.value.release(id);
27997
28170
  } catch (error) {
27998
- if (error instanceof Error && error.message.startsWith("DEEPSEEK_KEY_REQUIRED:"))
27999
- throw new ChordError4("invalid", "DEEPSEEK_KEY_REQUIRED: Configure DeepSeek in /login or Web settings.");
28000
28171
  throw new ChordError4("invalid", error instanceof Error ? error.message : "Cannot submit message");
28001
28172
  }
28002
28173
  return null;
@@ -28788,9 +28959,11 @@ var ServerState = local3("neko.server-state", {
28788
28959
  credentials: CredentialStateSchema,
28789
28960
  searchSettings: SearchSettingsStateSchema,
28790
28961
  contextSettings: ContextSettingsStateSchema,
28962
+ defaultModel: DefaultModelStateSchema,
28791
28963
  refreshCredentials: method2(Type12.Null(), Type12.Null()),
28792
28964
  refreshSearchSettings: method2(Type12.Null(), Type12.Null()),
28793
- refreshContextSettings: method2(Type12.Null(), Type12.Null())
28965
+ refreshContextSettings: method2(Type12.Null(), Type12.Null()),
28966
+ refreshDefaultModel: method2(Type12.Null(), Type12.Null())
28794
28967
  }
28795
28968
  });
28796
28969
  async function createNekoServerServices(options) {
@@ -28868,13 +29041,20 @@ async function createNekoServerServices(options) {
28868
29041
  status: info.status,
28869
29042
  awaitingAnswer: info.awaitingAnswer
28870
29043
  }));
28871
- const readStatuses = async () => Object.fromEntries(await Promise.all(CREDENTIAL_PROVIDERS.map(async (spec) => [spec.id, await runtime.credentialStatus(spec.id)])));
28872
- const [sessions, workspaces, statuses, search, contextSettings0] = await Promise.all([
29044
+ const readStatuses = async () => {
29045
+ const enabled = new Set(runtime.enabledProviders());
29046
+ return Object.fromEntries(await Promise.all(CREDENTIAL_PROVIDERS.map(async (spec) => {
29047
+ const status = await runtime.credentialStatus(spec.id);
29048
+ return [spec.id, spec.kind === "model" ? { ...status, enabled: enabled.has(spec.id) } : status];
29049
+ })));
29050
+ };
29051
+ const [sessions, workspaces, statuses, search, contextSettings0, defaultModel0] = await Promise.all([
28873
29052
  list(),
28874
29053
  runtime.listWorkspaces(),
28875
29054
  readStatuses(),
28876
29055
  runtime.searchSettings(),
28877
- runtime.contextSettings()
29056
+ runtime.contextSettings(),
29057
+ runtime.defaultModel()
28878
29058
  ]);
28879
29059
  let mutationTail = Promise.resolve();
28880
29060
  const serialize = (fn) => {
@@ -28891,6 +29071,7 @@ async function createNekoServerServices(options) {
28891
29071
  revision: 1,
28892
29072
  ...contextSettings0
28893
29073
  });
29074
+ const defaultModel = self2.state("defaultModel", DefaultModelStateSchema, defaultModel0);
28894
29075
  const refreshContextSettings = async (_input, ctx) => {
28895
29076
  const next = await runtime.contextSettings();
28896
29077
  const { revision, ...current } = contextSettings.value;
@@ -28898,6 +29079,12 @@ async function createNekoServerServices(options) {
28898
29079
  contextSettings.replace(ctx, { revision: revision + 1, ...next });
28899
29080
  return null;
28900
29081
  };
29082
+ const refreshDefaultModel = async (_input, ctx) => {
29083
+ const next = await runtime.defaultModel();
29084
+ if (JSON.stringify(next) !== JSON.stringify(defaultModel.value))
29085
+ defaultModel.replace(ctx, next);
29086
+ return null;
29087
+ };
28901
29088
  const refreshSearchSettings = async (_input, ctx) => {
28902
29089
  const next = await runtime.searchSettings();
28903
29090
  const current = searchSettings.value;
@@ -28926,6 +29113,7 @@ async function createNekoServerServices(options) {
28926
29113
  credentials,
28927
29114
  searchSettings,
28928
29115
  contextSettings,
29116
+ defaultModel,
28929
29117
  async refreshCredentials(_input, ctx) {
28930
29118
  credentials.replace(ctx, {
28931
29119
  revision: credentials.value.revision + 1,
@@ -28934,7 +29122,8 @@ async function createNekoServerServices(options) {
28934
29122
  return null;
28935
29123
  },
28936
29124
  refreshSearchSettings,
28937
- refreshContextSettings
29125
+ refreshContextSettings,
29126
+ refreshDefaultModel
28938
29127
  };
28939
29128
  }, { lifetime: "host" });
28940
29129
  const shared = (self2) => self2.get(ServerState, {});
@@ -28957,7 +29146,17 @@ async function createNekoServerServices(options) {
28957
29146
  remove: async (input, ctx) => {
28958
29147
  await runtime.removeCredential(input.provider);
28959
29148
  return hub.refreshCredentials(null, ctx);
28960
- }
29149
+ },
29150
+ setEnabled: (input, ctx) => serialize(async () => {
29151
+ if (!isModelProvider(input.provider))
29152
+ throw new ChordError5("invalid", "Unknown model provider.");
29153
+ try {
29154
+ await runtime.setProviderEnabled(input.provider, input.enabled);
29155
+ } catch (error) {
29156
+ throw new ChordError5("invalid", error instanceof Error ? error.message : "Cannot change provider.");
29157
+ }
29158
+ return hub.refreshCredentials(null, ctx);
29159
+ })
28961
29160
  };
28962
29161
  });
28963
29162
  host.serve(NekoSearchSettings, (_input, _principal, self2) => {
@@ -28986,9 +29185,24 @@ async function createNekoServerServices(options) {
28986
29185
  })
28987
29186
  };
28988
29187
  });
29188
+ host.serve(NekoDefaultModel, (_input, _principal, self2) => {
29189
+ const hub = shared(self2);
29190
+ return {
29191
+ variant: "ready",
29192
+ state: hub.defaultModel,
29193
+ set: (input, ctx) => serialize(async () => {
29194
+ await runtime.setDefaultModel(input);
29195
+ return hub.refreshDefaultModel(null, ctx);
29196
+ }),
29197
+ clear: (_input, ctx) => serialize(async () => {
29198
+ await runtime.setDefaultModel(null);
29199
+ return hub.refreshDefaultModel(null, ctx);
29200
+ })
29201
+ };
29202
+ });
28989
29203
  host.serve(NekoModels, () => ({
28990
29204
  variant: "ready",
28991
- list: async () => [...runtime.listModels()].map((value) => ({
29205
+ list: async () => (await runtime.listSelectableModels()).map((value) => ({
28992
29206
  ...value,
28993
29207
  input: [...value.input],
28994
29208
  supportedThinkingLevels: [...value.supportedThinkingLevels]
@@ -29282,7 +29496,6 @@ var SERVER_ID_FILENAME = "gateway-server-id";
29282
29496
  async function startGateway(options = {}) {
29283
29497
  const context = options.context ?? BACKGROUND_CONTEXT4;
29284
29498
  const fileConfig = loadNekoConfig(options.configDir);
29285
- const model = options.model ?? fileConfig.defaultModel ?? DEFAULT_MODEL;
29286
29499
  const storagePath = options.storagePath ?? fileConfig.storagePath ?? DEFAULT_STORAGE_PATH;
29287
29500
  const host = options.host ?? fileConfig.gateway?.host ?? DEFAULT_HOST;
29288
29501
  const port = options.port !== undefined ? options.port : fileConfig.gateway?.port ?? (process.platform === "win32" ? 0 : DEFAULT_PORT);
@@ -29300,7 +29513,8 @@ async function startGateway(options = {}) {
29300
29513
  ...options.apiKey !== undefined ? { apiKey: options.apiKey } : {},
29301
29514
  ...searchProvider !== undefined ? { searchProvider } : {},
29302
29515
  ...fileConfig.context !== undefined ? { contextSettings: fileConfig.context } : {},
29303
- model,
29516
+ ...fileConfig.defaultModel !== undefined ? { model: fileConfig.defaultModel } : {},
29517
+ ...options.model !== undefined ? { modelOverride: options.model } : {},
29304
29518
  thinkingLevel: options.thinkingLevel ?? fileConfig.thinkingLevel ?? "high",
29305
29519
  ...options.cwd !== undefined ? { cwd: options.cwd } : {},
29306
29520
  ...options.providers !== undefined ? { providers: options.providers } : {},
@@ -29315,7 +29529,6 @@ async function startGateway(options = {}) {
29315
29529
  socketPath,
29316
29530
  serverId,
29317
29531
  nekoDir,
29318
- model,
29319
29532
  marketUrl,
29320
29533
  promptHome: options.promptHome,
29321
29534
  updates: options.updates,