@kody-ade/kody-engine 0.4.286 → 0.4.288

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.
Files changed (2) hide show
  1. package/dist/bin/kody.js +217 -31
  2. package/package.json +1 -1
package/dist/bin/kody.js CHANGED
@@ -15,7 +15,7 @@ var init_package = __esm({
15
15
  "package.json"() {
16
16
  package_default = {
17
17
  name: "@kody-ade/kody-engine",
18
- version: "0.4.286",
18
+ version: "0.4.288",
19
19
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
20
20
  license: "MIT",
21
21
  type: "module",
@@ -443,7 +443,7 @@ var STATE_BRANCH;
443
443
  var init_stateBranch = __esm({
444
444
  "src/stateBranch.ts"() {
445
445
  "use strict";
446
- STATE_BRANCH = "kody-state";
446
+ STATE_BRANCH = "main";
447
447
  }
448
448
  });
449
449
 
@@ -497,18 +497,33 @@ function normalizeStatePath(raw, field = "statePath") {
497
497
  }
498
498
  return parts.join("/");
499
499
  }
500
+ function normalizeStateBranch(raw, field = "state.branch") {
501
+ const value = raw?.trim() || STATE_BRANCH;
502
+ if (!value) throw new Error(`kody.config.json: ${field} must not be empty`);
503
+ if (value.startsWith("/") || value.endsWith("/") || value.includes("\\") || value.includes("..") || value.includes("@{") || /[\x00-\x20\x7f~^:?*\[]/.test(value)) {
504
+ throw new Error(`kody.config.json: ${field} contains an invalid branch`);
505
+ }
506
+ for (const part of value.split("/")) {
507
+ if (!part || part === "." || part === ".." || part.startsWith(".") || part.endsWith(".lock")) {
508
+ throw new Error(`kody.config.json: ${field} contains an invalid branch`);
509
+ }
510
+ }
511
+ return value;
512
+ }
500
513
  function resolveStateRepoConfig(config) {
501
514
  if (config.state?.repo && config.state?.path) {
502
515
  parseStateRepoSlug(config.state.repo);
503
516
  return {
504
517
  repo: config.state.repo,
505
- path: normalizeStatePath(config.state.path)
518
+ path: normalizeStatePath(config.state.path),
519
+ branch: normalizeStateBranch(config.state.branch)
506
520
  };
507
521
  }
508
522
  if (config.github?.owner && config.github?.repo) {
509
523
  return {
510
524
  repo: `${config.github.owner}/kody-state`,
511
- path: normalizeStatePath(config.github.repo)
525
+ path: normalizeStatePath(config.github.repo),
526
+ branch: normalizeStateBranch(void 0)
512
527
  };
513
528
  }
514
529
  throw new Error("stateRepo: config.state or config.github owner/repo is required");
@@ -516,7 +531,7 @@ function resolveStateRepoConfig(config) {
516
531
  function parseStateRepo(config) {
517
532
  const state = resolveStateRepoConfig(config);
518
533
  const parsed = parseStateRepoSlug(state.repo);
519
- return { ...parsed, basePath: state.path };
534
+ return { ...parsed, basePath: state.path, branch: state.branch };
520
535
  }
521
536
  function stateRepoPath(config, filePath) {
522
537
  const state = resolveStateRepoConfig(config);
@@ -528,14 +543,15 @@ function apiPath(config, targetPath) {
528
543
  return `/repos/${parsed.owner}/${parsed.repo}/contents/${targetPath}`;
529
544
  }
530
545
  function branchApiPath(config, targetPath) {
531
- return `${apiPath(config, targetPath)}?ref=${encodeURIComponent(STATE_BRANCH)}`;
546
+ const state = resolveStateRepoConfig(config);
547
+ return `${apiPath(config, targetPath)}?ref=${encodeURIComponent(state.branch)}`;
532
548
  }
533
549
  function ensureStateBranch(config, cwd) {
534
550
  const parsed = parseStateRepo(config);
535
- const cacheKey3 = `${parsed.owner}/${parsed.repo}:${STATE_BRANCH}`;
551
+ const cacheKey3 = `${parsed.owner}/${parsed.repo}:${parsed.branch}`;
536
552
  if (ensuredStateBranches.has(cacheKey3)) return;
537
553
  try {
538
- gh(["api", `/repos/${parsed.owner}/${parsed.repo}/git/ref/heads/${STATE_BRANCH}`], { cwd });
554
+ gh(["api", `/repos/${parsed.owner}/${parsed.repo}/git/ref/heads/${parsed.branch}`], { cwd });
539
555
  ensuredStateBranches.add(cacheKey3);
540
556
  return;
541
557
  } catch (err) {
@@ -550,7 +566,7 @@ function ensureStateBranch(config, cwd) {
550
566
  try {
551
567
  gh(["api", "--method", "POST", `/repos/${parsed.owner}/${parsed.repo}/git/refs`, "--input", "-"], {
552
568
  cwd,
553
- input: JSON.stringify({ ref: `refs/heads/${STATE_BRANCH}`, sha })
569
+ input: JSON.stringify({ ref: `refs/heads/${parsed.branch}`, sha })
554
570
  });
555
571
  } catch (err) {
556
572
  if (!isAlreadyExists(err)) throw err;
@@ -590,7 +606,7 @@ function writeStateText(config, cwd, filePath, content, message, sha) {
590
606
  const payload = {
591
607
  message,
592
608
  content: Buffer.from(content, "utf-8").toString("base64"),
593
- branch: STATE_BRANCH
609
+ branch: resolveStateRepoConfig(config).branch
594
610
  };
595
611
  if (sha) payload.sha = sha;
596
612
  gh(["api", "--method", "PUT", apiPath(config, targetPath), "--input", "-"], {
@@ -655,11 +671,51 @@ function parseProviderModel(s) {
655
671
  }
656
672
  return { provider: s.slice(0, slash), model: s.slice(slash + 1) };
657
673
  }
674
+ function optionalRuntimeString(raw, key) {
675
+ const value = raw[key];
676
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
677
+ }
678
+ function parseModelRuntimeConfig(modelSpec, rawConfig) {
679
+ const fallback = parseProviderModel(modelSpec);
680
+ const raw = rawConfig?.trim();
681
+ if (!raw) return fallback;
682
+ let parsed;
683
+ try {
684
+ parsed = JSON.parse(raw);
685
+ } catch (err) {
686
+ const msg = err instanceof Error ? err.message : String(err);
687
+ throw new Error(`KODY_MODEL_CONFIG is invalid JSON: ${msg}`);
688
+ }
689
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
690
+ throw new Error("KODY_MODEL_CONFIG must be a JSON object");
691
+ }
692
+ const record2 = parsed;
693
+ const modelName = optionalRuntimeString(record2, "modelName");
694
+ if (!modelName) {
695
+ throw new Error("KODY_MODEL_CONFIG.modelName is required");
696
+ }
697
+ const protocol = optionalRuntimeString(record2, "protocol");
698
+ const baseURL = optionalRuntimeString(record2, "baseURL");
699
+ const apiKeyEnvVar = optionalRuntimeString(record2, "apiKeyEnvVar");
700
+ const spec = optionalRuntimeString(record2, "spec");
701
+ const provider = optionalRuntimeString(record2, "provider") ?? fallback.provider;
702
+ const out = {
703
+ provider,
704
+ model: modelName
705
+ };
706
+ if (protocol) out.protocol = protocol;
707
+ if (baseURL) out.baseURL = baseURL;
708
+ if (apiKeyEnvVar) out.apiKeyEnvVar = apiKeyEnvVar;
709
+ if (spec) out.spec = spec;
710
+ if (protocol === "openai") out.litellmProvider = "openai";
711
+ return out;
712
+ }
658
713
  function providerApiKeyEnvVar(provider) {
659
714
  if (provider === "anthropic" || provider === "claude") return "ANTHROPIC_API_KEY";
660
715
  return `${provider.toUpperCase()}_API_KEY`;
661
716
  }
662
717
  function needsLitellmProxy(model) {
718
+ if (model.protocol === "anthropic") return false;
663
719
  return model.provider !== "claude" && model.provider !== "anthropic";
664
720
  }
665
721
  function loadConfig(projectDir = process.cwd()) {
@@ -722,12 +778,14 @@ function parseStateConfig(raw, github) {
722
778
  const nested = recordValue(raw.state) ?? {};
723
779
  const repoRaw = typeof raw.stateRepo === "string" ? raw.stateRepo : nested.repo;
724
780
  const pathRaw = typeof raw.statePath === "string" ? raw.statePath : nested.path;
781
+ const branchRaw = typeof raw.stateBranch === "string" ? raw.stateBranch : nested.branch;
725
782
  const stateRepo = typeof repoRaw === "string" && repoRaw.trim().length > 0 ? repoRaw.trim() : `https://github.com/${String(github.owner)}/kody-state`;
726
783
  parseStateRepoSlug(stateRepo);
727
784
  const statePath = typeof pathRaw === "string" && pathRaw.trim().length > 0 ? pathRaw.trim() : String(github.repo);
728
785
  return {
729
786
  repo: stateRepo,
730
- path: normalizeStatePath(statePath)
787
+ path: normalizeStatePath(statePath),
788
+ branch: typeof branchRaw === "string" && branchRaw.trim().length > 0 ? normalizeStateBranch(branchRaw) : normalizeStateBranch(void 0)
731
789
  };
732
790
  }
733
791
  function parseAccessConfig(raw) {
@@ -2423,14 +2481,50 @@ function listRepairCandidates(repoSlug) {
2423
2481
  function dispatchVerb(workflowFile, repoSlug, capability, prNumber) {
2424
2482
  return dispatchWorkflow(workflowFile, capability, prNumber, repoSlug);
2425
2483
  }
2426
- function postRecommendation(prNumber, mention, message, capabilitySlug) {
2484
+ function capabilityMarker(slug2) {
2485
+ return `<!-- kody-capability: ${slug2} -->`;
2486
+ }
2487
+ function normalizeRecommendationIntent(body) {
2488
+ const marker = body.match(/<!--\s*kody-intent:\s*([\s\S]*?)-->/i);
2489
+ if (marker?.[1]) return marker[1].trim().replace(/\s+/g, " ").toLowerCase();
2490
+ const legacy = body.match(/(?:^|\n)\s*(?:kody-cmd:\s*|@kody\s+)([a-z][\w-]*(?:\s+--pr\s+\d+)?)/i);
2491
+ if (legacy?.[1]) return legacy[1].trim().replace(/\s+/g, " ").toLowerCase();
2492
+ return null;
2493
+ }
2494
+ function containsExecutableKodyCommand(body) {
2495
+ return /@kody\b/i.test(body) || /\bkody-cmd\s*:/i.test(body);
2496
+ }
2497
+ function recommendationAlreadyExists(repoSlug, prNumber, body, capabilitySlug) {
2498
+ const requestedIntent = normalizeRecommendationIntent(body);
2499
+ const requestedText = body.trim().replace(/\s+/g, " ");
2500
+ const raw = gh(["issue", "view", String(prNumber), "-R", repoSlug, "--json", "comments"]);
2501
+ const parsed = JSON.parse(raw);
2502
+ return (parsed.comments ?? []).some((comment) => {
2503
+ const existing = comment.body ?? "";
2504
+ if (capabilitySlug && !existing.includes(capabilityMarker(capabilitySlug))) return false;
2505
+ const existingIntent = normalizeRecommendationIntent(existing);
2506
+ if (requestedIntent && existingIntent) return requestedIntent === existingIntent;
2507
+ if (capabilitySlug && requestedIntent) return true;
2508
+ return existing.trim().replace(/\s+/g, " ").includes(requestedText);
2509
+ });
2510
+ }
2511
+ function postRecommendation(repoSlug, prNumber, mention, message, capabilitySlug) {
2512
+ if (containsExecutableKodyCommand(message)) {
2513
+ return {
2514
+ ok: false,
2515
+ error: "recommendation body contains executable Kody command text; use inert kody-intent metadata"
2516
+ };
2517
+ }
2427
2518
  const mentioned = mention ? `${mention} ${message}` : message;
2428
2519
  const body = capabilitySlug ? `${mentioned}
2429
2520
 
2430
- <!-- kody-capability: ${capabilitySlug} -->` : mentioned;
2521
+ ${capabilityMarker(capabilitySlug)}` : mentioned;
2431
2522
  try {
2432
- gh(["pr", "comment", String(prNumber), "--body", body]);
2433
- return { ok: true };
2523
+ if (recommendationAlreadyExists(repoSlug, prNumber, body, capabilitySlug)) {
2524
+ return { ok: true, posted: false };
2525
+ }
2526
+ gh(["issue", "comment", String(prNumber), "-R", repoSlug, "--body-file", "-"], { input: body });
2527
+ return { ok: true, posted: true };
2434
2528
  } catch (err) {
2435
2529
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
2436
2530
  }
@@ -2671,8 +2765,8 @@ function capabilityToolDefinitions(opts) {
2671
2765
  handler: async (args) => {
2672
2766
  const pr = Number(args.pr);
2673
2767
  const body = String(args.body ?? "");
2674
- const result = postRecommendation(pr, opts.operatorMention, body, opts.capabilitySlug);
2675
- const text = result.ok ? `Recommendation posted on PR #${pr}.` : `Recommendation failed on PR #${pr}: ${result.error}`;
2768
+ const result = postRecommendation(opts.repoSlug, pr, opts.operatorMention, body, opts.capabilitySlug);
2769
+ const text = result.ok ? result.posted ? `Recommendation posted on PR #${pr}.` : `Recommendation already exists on PR #${pr}; skipped.` : `Recommendation failed on PR #${pr}: ${result.error}`;
2676
2770
  return { content: [{ type: "text", text }] };
2677
2771
  }
2678
2772
  };
@@ -5836,13 +5930,16 @@ function resolveLitellmTimeoutMs() {
5836
5930
  return DEFAULT_LITELLM_STARTUP_TIMEOUT_SEC * 1e3;
5837
5931
  }
5838
5932
  function generateLitellmConfigYaml(model) {
5839
- const apiKeyVar = providerApiKeyEnvVar(model.provider);
5933
+ const apiKeyVar = model.apiKeyEnvVar ?? providerApiKeyEnvVar(model.provider);
5934
+ const litellmProvider = model.litellmProvider ?? model.provider;
5935
+ const providerParams = model.baseURL ? [["api_base", model.baseURL]] : [];
5840
5936
  return [
5841
5937
  "model_list:",
5842
5938
  ` - model_name: ${model.model}`,
5843
5939
  ` litellm_params:`,
5844
- ` model: ${model.provider}/${model.model}`,
5940
+ ` model: ${litellmProvider}/${model.model}`,
5845
5941
  ` api_key: os.environ/${apiKeyVar}`,
5942
+ ...providerParams.map(([key, value]) => ` ${key}: ${value}`),
5846
5943
  "",
5847
5944
  "litellm_settings:",
5848
5945
  " drop_params: true",
@@ -6878,6 +6975,7 @@ function stateRepoContext(config, goalId, logPath) {
6878
6975
  return {
6879
6976
  repo: state.repo,
6880
6977
  path: state.path,
6978
+ branch: state.branch,
6881
6979
  goalStatePath: `${state.path}/${goalStateLogPath(goalId)}`,
6882
6980
  logPath: `${state.path}/${logPath}`
6883
6981
  };
@@ -6976,16 +7074,17 @@ function linkContext(stateRepo) {
6976
7074
  const server = process.env.GITHUB_SERVER_URL?.trim() || "https://github.com";
6977
7075
  if (runId && repository) links.workflowRun = `${server}/${repository}/actions/runs/${runId}`;
6978
7076
  const repo = stringValue(stateRepo?.repo);
7077
+ const branch = stringValue(stateRepo?.branch);
6979
7078
  const goalStatePath2 = stringValue(stateRepo?.goalStatePath);
6980
7079
  const logPath = stringValue(stateRepo?.logPath);
6981
- if (repo && goalStatePath2) links.goalState = githubBlobUrl(repo, goalStatePath2);
6982
- if (repo && logPath) links.log = githubBlobUrl(repo, logPath);
7080
+ if (repo && branch && goalStatePath2) links.goalState = githubBlobUrl(repo, branch, goalStatePath2);
7081
+ if (repo && branch && logPath) links.log = githubBlobUrl(repo, branch, logPath);
6983
7082
  return Object.keys(links).length > 0 ? links : void 0;
6984
7083
  }
6985
- function githubBlobUrl(repo, filePath) {
7084
+ function githubBlobUrl(repo, branch, filePath) {
6986
7085
  try {
6987
7086
  const parsed = parseStateRepoSlug(repo);
6988
- return `https://github.com/${parsed.owner}/${parsed.repo}/blob/${STATE_BRANCH}/${filePath}`;
7087
+ return `https://github.com/${parsed.owner}/${parsed.repo}/blob/${encodeURIComponent(branch)}/${filePath}`;
6989
7088
  } catch {
6990
7089
  return void 0;
6991
7090
  }
@@ -7068,7 +7167,6 @@ var LOGS_KEY, LOG_RUN_KEY, LOG_STARTED_KEY;
7068
7167
  var init_runLog = __esm({
7069
7168
  "src/goal/runLog.ts"() {
7070
7169
  "use strict";
7071
- init_stateBranch();
7072
7170
  init_stateRepo();
7073
7171
  init_state2();
7074
7172
  LOGS_KEY = "__goalRunLogs";
@@ -17536,7 +17634,7 @@ function overlayDirectoryChildren(cwd, sourceDir, localDir) {
17536
17634
  function hydrateStateWorkspace(config, cwd) {
17537
17635
  if (process.env.VITEST && process.env[TEST_FETCH_ENV] !== "1") return;
17538
17636
  const parsed = parseStateRepo(config);
17539
- const hydrateKey = `${path41.resolve(cwd)}|${parsed.owner}/${parsed.repo}|${parsed.basePath}|${STATE_BRANCH}`;
17637
+ const hydrateKey = `${path41.resolve(cwd)}|${parsed.owner}/${parsed.repo}|${parsed.basePath}|${parsed.branch}`;
17540
17638
  if (hydratedWorkspaces.has(hydrateKey)) return;
17541
17639
  const snapshotRoot = fetchStateSnapshot(parsed);
17542
17640
  for (const mapping of DIR_MAPPINGS) {
@@ -17560,7 +17658,7 @@ function fetchStateSnapshot(parsed) {
17560
17658
  runGit3(["clone", "--no-checkout", "--filter=blob:none", url, cacheDir]);
17561
17659
  }
17562
17660
  runGit3(["-C", cacheDir, "remote", "set-url", "origin", url]);
17563
- runGit3(["-C", cacheDir, "fetch", "--depth=1", "origin", STATE_BRANCH]);
17661
+ runGit3(["-C", cacheDir, "fetch", "--depth=1", "origin", parsed.branch]);
17564
17662
  runGit3(["-C", cacheDir, "sparse-checkout", "init", "--cone"]);
17565
17663
  runGit3(["-C", cacheDir, "sparse-checkout", "set", parsed.basePath]);
17566
17664
  runGit3(["-C", cacheDir, "checkout", "--force", "--detach", "FETCH_HEAD"]);
@@ -17568,7 +17666,7 @@ function fetchStateSnapshot(parsed) {
17568
17666
  } catch (err) {
17569
17667
  const msg = err instanceof Error ? err.message : String(err);
17570
17668
  throw new Error(
17571
- `stateWorkspace: failed to fetch ${parsed.owner}/${parsed.repo}:${parsed.basePath}@${STATE_BRANCH}: ${msg}`
17669
+ `stateWorkspace: failed to fetch ${parsed.owner}/${parsed.repo}:${parsed.basePath}@${parsed.branch}: ${msg}`
17572
17670
  );
17573
17671
  }
17574
17672
  return path41.join(cacheDir, parsed.basePath);
@@ -17577,7 +17675,7 @@ function cacheRoot2() {
17577
17675
  return process.env[CACHE_ENV2]?.trim() || path41.join(os7.homedir(), ".cache", "kody", "state-repo");
17578
17676
  }
17579
17677
  function cacheKey2(parsed) {
17580
- return crypto3.createHash("sha256").update(`${parsed.owner}/${parsed.repo}#${STATE_BRANCH}#${parsed.basePath}`).digest("hex").slice(0, 24);
17678
+ return crypto3.createHash("sha256").update(`${parsed.owner}/${parsed.repo}#${parsed.branch}#${parsed.basePath}`).digest("hex").slice(0, 24);
17581
17679
  }
17582
17680
  function runGit3(args) {
17583
17681
  try {
@@ -17612,7 +17710,6 @@ var DIR_MAPPINGS, FILE_MAPPINGS, CACHE_ENV2, TEST_FETCH_ENV, hydratedWorkspaces;
17612
17710
  var init_stateWorkspace = __esm({
17613
17711
  "src/stateWorkspace.ts"() {
17614
17712
  "use strict";
17615
- init_stateBranch();
17616
17713
  init_stateRepo();
17617
17714
  DIR_MAPPINGS = [
17618
17715
  { stateDir: "executables", localDir: path41.join(".kody", "executables") },
@@ -19367,6 +19464,16 @@ var CHAT_SYSTEM_PROMPT = [
19367
19464
  "Do not invent file paths, commit SHAs, line numbers, or command output. If you",
19368
19465
  "cite something concrete, you must have just read or run it in this session."
19369
19466
  ].join("\n");
19467
+ var OPENAI_CHAT_SYSTEM_PROMPT = [
19468
+ "You are Kody, an AI assistant for the Kody Operations Dashboard. Reply to the",
19469
+ "user's latest message using the full conversation below as context. Keep replies",
19470
+ "short and simple. Use plain terms, not jargon.",
19471
+ "",
19472
+ "This chat is running through an OpenAI-compatible model endpoint. Answer directly",
19473
+ "from the conversation and provided context. Do not claim to run shell commands,",
19474
+ "edit files, inspect the repository, or use tools unless the user has supplied",
19475
+ "the relevant content in the chat."
19476
+ ].join("\n");
19370
19477
  var CROSS_REPO_PROMPT = [
19371
19478
  "# Working across repositories",
19372
19479
  "You are NOT limited to the repository at your current working directory. You",
@@ -19428,7 +19535,7 @@ async function runChatTurn(opts) {
19428
19535
  return { exitCode: 64, error };
19429
19536
  }
19430
19537
  const { turns: promptTurns, imagePaths } = prepareAttachments(turns, opts.cwd, opts.sessionId);
19431
- const basePrompt = opts.systemPrompt ?? CHAT_SYSTEM_PROMPT;
19538
+ const basePrompt = opts.systemPrompt ?? (opts.model.protocol === "openai" ? OPENAI_CHAT_SYSTEM_PROMPT : CHAT_SYSTEM_PROMPT);
19432
19539
  const catalog = buildExecutableCatalog();
19433
19540
  const taskArtifactsPaths = prepareTaskArtifactsDir(opts.cwd, opts.sessionId);
19434
19541
  const artifactAddendum = taskArtifactsPromptAddendum({
@@ -19462,6 +19569,14 @@ async function runChatTurn(opts) {
19462
19569
  artifactAddendum
19463
19570
  ].filter((s) => typeof s === "string" && s.length > 0).join("\n\n");
19464
19571
  const prompt = buildPrompt(promptTurns);
19572
+ if (opts.model.protocol === "openai" && opts.litellmUrl) {
19573
+ return runOpenAIChatTurn({
19574
+ opts,
19575
+ turns: promptTurns,
19576
+ systemPrompt,
19577
+ sessionFile: opts.sessionFile
19578
+ });
19579
+ }
19465
19580
  let progressSeq = 0;
19466
19581
  const invoke = opts.invokeAgent ?? ((p) => runAgent({
19467
19582
  prompt: p,
@@ -19564,6 +19679,74 @@ async function runChatTurn(opts) {
19564
19679
  }
19565
19680
  return { exitCode: 0, reply };
19566
19681
  }
19682
+ async function runOpenAIChatTurn(args) {
19683
+ const { opts, turns, systemPrompt, sessionFile } = args;
19684
+ const doFetch = opts.fetchImpl ?? fetch;
19685
+ const url = `${opts.litellmUrl.replace(/\/+$/, "")}/v1/chat/completions`;
19686
+ try {
19687
+ const response = await doFetch(url, {
19688
+ method: "POST",
19689
+ headers: { "Content-Type": "application/json" },
19690
+ body: JSON.stringify({
19691
+ model: opts.model.model,
19692
+ messages: [
19693
+ { role: "system", content: systemPrompt },
19694
+ ...turns.map((turn) => ({
19695
+ role: turn.role,
19696
+ content: turn.content
19697
+ }))
19698
+ ],
19699
+ stream: false
19700
+ })
19701
+ });
19702
+ if (!response.ok) {
19703
+ const text = await response.text().catch(() => "");
19704
+ const error = `OpenAI-compatible model request failed ${response.status}${text ? `: ${text.slice(0, 500)}` : ""}`;
19705
+ await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
19706
+ return { exitCode: 99, error };
19707
+ }
19708
+ const payload = await response.json();
19709
+ const reply = extractOpenAIReply(payload).trim();
19710
+ if (!reply) {
19711
+ const error = typeof payload.error?.message === "string" ? payload.error.message : "OpenAI-compatible model completed without producing a reply";
19712
+ await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
19713
+ return { exitCode: 99, error };
19714
+ }
19715
+ const now = (/* @__PURE__ */ new Date()).toISOString();
19716
+ appendTurn(sessionFile, {
19717
+ role: "assistant",
19718
+ content: reply,
19719
+ timestamp: now
19720
+ });
19721
+ await emit(opts.sink, "chat.message", opts.sessionId, "message", {
19722
+ sessionId: opts.sessionId,
19723
+ role: "assistant",
19724
+ content: reply,
19725
+ timestamp: now
19726
+ });
19727
+ await emit(opts.sink, "chat.done", opts.sessionId, "done", { sessionId: opts.sessionId });
19728
+ return { exitCode: 0, reply };
19729
+ } catch (err) {
19730
+ const error = err instanceof Error ? err.message : String(err);
19731
+ await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
19732
+ return { exitCode: 99, error };
19733
+ }
19734
+ }
19735
+ function extractOpenAIReply(payload) {
19736
+ const content = payload.choices?.[0]?.message?.content;
19737
+ if (typeof content === "string") return content;
19738
+ if (Array.isArray(content)) {
19739
+ return content.map((part) => {
19740
+ if (typeof part === "string") return part;
19741
+ if (part && typeof part === "object" && "text" in part) {
19742
+ const text = part.text;
19743
+ return typeof text === "string" ? text : "";
19744
+ }
19745
+ return "";
19746
+ }).join("");
19747
+ }
19748
+ return "";
19749
+ }
19567
19750
  function buildPrompt(turns) {
19568
19751
  const body = turns.map((t) => `${t.role === "user" ? "User" : "Assistant"}: ${t.content}`).join("\n\n");
19569
19752
  return `${body}
@@ -21406,7 +21589,10 @@ async function brainServe(opts) {
21406
21589
  }
21407
21590
  const apiKey = getApiKey();
21408
21591
  const port = Number(process.env.PORT ?? DEFAULT_PORT);
21409
- const model = parseProviderModel(process.env.MODEL?.trim() || "claude/claude-haiku-4-5-20251001");
21592
+ const model = parseModelRuntimeConfig(
21593
+ process.env.MODEL?.trim() || "claude/claude-haiku-4-5-20251001",
21594
+ process.env.KODY_MODEL_CONFIG
21595
+ );
21410
21596
  const usesProxy = needsLitellmProxy(model);
21411
21597
  let handle = null;
21412
21598
  if (usesProxy) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.286",
3
+ "version": "0.4.288",
4
4
  "description": "kody — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
5
5
  "license": "MIT",
6
6
  "type": "module",