chainlesschain 0.166.132 → 0.166.135

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 (32) hide show
  1. package/README.md +44 -18
  2. package/package.json +1 -1
  3. package/src/assets/linux-subreaper/linux-arm64/manifest.json +1 -1
  4. package/src/assets/linux-subreaper/linux-x64/manifest.json +1 -1
  5. package/src/data/changelog.json +26 -0
  6. package/src/gateways/ws/llm-chat-protocol.js +1 -1
  7. package/src/gateways/ws/personal-data-hub-protocol.js +27 -11
  8. package/src/gateways/ws/session-core-protocol.js +22 -17
  9. package/src/gateways/ws/session-protocol.js +19 -8
  10. package/src/gateways/ws/ws-agent-handler.js +30 -0
  11. package/src/lib/background-launch-profile.js +37 -1
  12. package/src/lib/chat-core.js +40 -35
  13. package/src/lib/coding-task-guidance.js +10 -9
  14. package/src/lib/context-memory-kernel/message-adapter.js +5 -1
  15. package/src/lib/evolution/agent-model-projection.js +0 -1
  16. package/src/lib/evolution/governed-hub-llm.js +5 -1
  17. package/src/lib/evolution/governed-hub-resolver.js +15 -6
  18. package/src/lib/evolution/governed-hub-skill.js +2 -0
  19. package/src/lib/evolution/optional-ui-model-turn.js +13 -2
  20. package/src/lib/llm-config-defaults.js +17 -5
  21. package/src/lib/llm-connection-config.js +10 -2
  22. package/src/lib/llm-connection-probe.js +5 -2
  23. package/src/lib/llm-connection-resolution.js +7 -4
  24. package/src/lib/llm-providers.js +24 -11
  25. package/src/lib/openai-responses.js +2 -1
  26. package/src/lib/personal-data-hub-wiring.js +69 -36
  27. package/src/lib/provider-protocol.js +35 -3
  28. package/src/lib/provider-stream.js +6 -4
  29. package/src/lib/runnable-provider.js +4 -0
  30. package/src/lib/ws-chat-handler.js +22 -9
  31. package/src/runtime/agent-core.js +182 -34
  32. package/src/runtime/headless-runner.js +3 -3
@@ -10,9 +10,13 @@ import { providerFetch } from "./formal-budget-provider-scope.js";
10
10
  * - streamOllama / streamOpenAI — low-level streaming helpers
11
11
  */
12
12
 
13
- import { BUILT_IN_PROVIDERS } from "./llm-providers.js";
13
+ import {
14
+ BUILT_IN_PROVIDERS,
15
+ resolveProviderDefinition,
16
+ } from "./llm-providers.js";
14
17
  import { normalizeOllamaBaseUrl } from "./ollama-base-url.js";
15
18
  import {
19
+ assertCustomProviderConnection,
16
20
  geminiGenerateContentUrl,
17
21
  geminiRequestBody,
18
22
  geminiUsage,
@@ -262,22 +266,22 @@ export async function streamOpenAI(
262
266
  response = await providerFetch(
263
267
  `${normalizeProviderBaseUrl(baseUrl)}/chat/completions`,
264
268
  {
265
- method: "POST",
266
- headers: {
267
- "Content-Type": "application/json",
268
- Authorization: `Bearer ${apiKey}`,
269
- },
270
- body: JSON.stringify({
271
- model,
272
- messages,
273
- stream: true,
274
- // Opt-in token usage in the terminal chunk (OpenAI-compatible).
275
- // Servers that don't understand it simply ignore it.
276
- stream_options: { include_usage: true },
277
- }),
278
- signal: options.signal
279
- ? AbortSignal.any([guard.signal, options.signal])
280
- : guard.signal,
269
+ method: "POST",
270
+ headers: {
271
+ "Content-Type": "application/json",
272
+ ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
273
+ },
274
+ body: JSON.stringify({
275
+ model,
276
+ messages,
277
+ stream: true,
278
+ // Opt-in token usage in the terminal chunk (OpenAI-compatible).
279
+ // Servers that don't understand it simply ignore it.
280
+ stream_options: { include_usage: true },
281
+ }),
282
+ signal: options.signal
283
+ ? AbortSignal.any([guard.signal, options.signal])
284
+ : guard.signal,
281
285
  },
282
286
  );
283
287
  } catch (e) {
@@ -410,22 +414,22 @@ export async function streamAnthropic(
410
414
  response = await providerFetch(
411
415
  `${normalizeProviderBaseUrl(baseUrl)}/messages`,
412
416
  {
413
- method: "POST",
414
- headers: {
415
- "Content-Type": "application/json",
416
- "x-api-key": apiKey,
417
- "anthropic-version": "2023-06-01",
418
- },
419
- body: JSON.stringify({
420
- model,
421
- max_tokens: 4096,
422
- stream: true,
423
- ...(system ? { system } : {}),
424
- messages: convo,
425
- }),
426
- signal: options.signal
427
- ? AbortSignal.any([guard.signal, options.signal])
428
- : guard.signal,
417
+ method: "POST",
418
+ headers: {
419
+ "Content-Type": "application/json",
420
+ "x-api-key": apiKey,
421
+ "anthropic-version": "2023-06-01",
422
+ },
423
+ body: JSON.stringify({
424
+ model,
425
+ max_tokens: 4096,
426
+ stream: true,
427
+ ...(system ? { system } : {}),
428
+ messages: convo,
429
+ }),
430
+ signal: options.signal
431
+ ? AbortSignal.any([guard.signal, options.signal])
432
+ : guard.signal,
429
433
  },
430
434
  );
431
435
  } catch (e) {
@@ -666,6 +670,7 @@ export async function streamGemini(
666
670
  * @param {object} options - provider, model, baseUrl, apiKey
667
671
  */
668
672
  export async function* chatStream(messages, options) {
673
+ assertCustomProviderConnection(options.provider, options);
669
674
  const { provider, model, baseUrl, apiKey, sessionId } = options;
670
675
 
671
676
  // Token queue + waiter promise. onToken (called synchronously by each
@@ -765,14 +770,14 @@ export async function* chatStream(messages, options) {
765
770
  options,
766
771
  );
767
772
  } else {
768
- const providerDef = BUILT_IN_PROVIDERS[provider];
773
+ const providerDef = resolveProviderDefinition(provider);
769
774
  const url = normalizeProviderBaseUrl(
770
775
  baseUrl || providerDef?.baseUrl || "https://api.openai.com/v1",
771
776
  );
772
777
  const key =
773
778
  apiKey ||
774
779
  (providerDef?.apiKeyEnv ? process.env[providerDef.apiKeyEnv] : null);
775
- if (!key) {
780
+ if (!key && provider !== "custom") {
776
781
  throw new Error(
777
782
  `API key required for ${provider} (set ${providerDef?.apiKeyEnv || "API key"})`,
778
783
  );
@@ -1,14 +1,15 @@
1
1
  /** Shared correctness guidance for normal and file-only coding sessions. */
2
2
  export const CODING_TASK_QUALITY_GUIDANCE = `Coding task quality:
3
- - Read the actual implementation and its exported API before writing a caller or test. Check parameter order, return shapes, asynchronous lifecycle and fixture requirements against the source; an example in another directory can have different relative imports.
4
- - Keep progress explanations to one or two sentences. Once the relevant API and behavior are clear, make the next tool call; resolve a remaining uncertainty with a targeted read. Put implementation code directly in write/edit tool arguments instead of rehearsing the code or repeatedly analyzing the same source in ordinary assistant text.
5
- - Resolve each relative import from the destination file's directory. Read back the completed file and fix newDiagnostics from write/edit results before finishing. A successful write confirms persistence, not syntax, test execution or acceptance.
6
- - Before finishing a regression task, map each explicit acceptance requirement to a concrete fixture, production call path and observable assertion in the deliverable. Positive and negative controls must exercise the same requested behavior and differ in the intended condition. Resolve uncovered requirements with permitted tools, or report the specific gap. Avoid mocks or alternate calls that bypass the behavior under test. Complete the current worker/request when testing cancellation and replacement.
7
- - Allocate limited test cases from the user's explicit requirements, not from the tests already written. A test-count cap is a prioritization budget, not permission to omit a requirement. Merge or replace equivalent cases that reach the same decision so uncovered requirements take priority. Repair known coverage gaps within the authorized scope and remaining budgets; if a gap cannot be resolved, report the task as incomplete.
8
- - For boundary regressions, derive expected defaults, computed values and rejection rules from the implementation or the explicit requested contract. Inject mismatched, tampered or stale input through the real entry point while keeping other prerequisites valid; verify that the fixture reaches the intended check rather than failing during setup. Compare observable results with independent expected values or authoritative state, not a helper that merely recomputes the same result.
9
- - Trace rejection before asserting its aftermath: a request can be rejected before registration or persistence. Check whether state stays unchanged, a failed record is saved, or an event is emitted; do not assume every rejection creates a failed task. When testing attribution, verify independently recorded identities and provenance as well as message contents; identical text can still be attributed to the wrong source.
10
- - Use available execution tools for a focused check when permitted. When execution tools are unavailable, inspect imports, API contracts and fixtures using the available file tools, and clearly say that tests were not run. Never describe a read-back or static check as a passing test or end-to-end verification.
11
- - Finish requested file changes before the final response. Reserve output and remaining requests for writing, read-back and focused validation. For large files, aim for a few thousand characters of new content per call: write a small complete section, then extend it with edit_file using a unique existing anchor and a replacement containing that anchor plus the next complete section. A list of available tool names is not a limit on the number of tool calls; use further calls within the actual request budget to finish and inspect the file. Keep each tool call complete and wait for its result before extending the file again; do not repeatedly rewrite the growing file. State what was delivered, what was checked and what remains unverified, without repeating that account in multiple drafts.`;
3
+ - Read the actual implementation and its exported API before writing a caller or test. Verify parameter order, return shapes, fixture requirements and the delegated owner of each lifecycle transition.
4
+ - Keep progress explanations to one or two sentences. Resolve uncertainty with targeted reads, reuse unchanged source already inspected, and put code directly in write/edit arguments.
5
+ - Resolve each relative import from the destination file's directory. Read back completed files and fix newDiagnostics. A successful write proves persistence, not test execution or acceptance.
6
+ - Map every acceptance requirement to a fixture, real production call path and observable assertion. Positive and negative controls must reach the same boundary with unrelated prerequisites valid. Avoid mocks that bypass it; identify which assertion would fail without that boundary.
7
+ - Derive case allocation from the user's requirements. A test-count cap does not waive coverage: merge redundant cases to cover gaps within the cap, authorized scope and remaining budget. Report unresolved gaps as incomplete.
8
+ - For races, control the intended transition and complete the current worker/request when testing cancellation or replacement. An earlier unrelated rejection does not prove the target guard. Derive expectations independently from the contract or authoritative state, not by calling the function under test.
9
+ - Trace rejection before asserting its aftermath: it may precede registration or persistence. Inspect whether state stays unchanged, a failed record exists or an event is emitted. Compare independently recorded identities and provenance; equal text does not prove ownership.
10
+ - Inspect persistence projections before comparing histories. Host notices, lineage and imports may accompany user/assistant messages; verify their provenance separately. Report defects that cannot be repaired within the permitted scope instead of inventing passing behavior.
11
+ - Run focused checks with permitted execution tools. With file tools only, inspect imports, contracts and fixtures and say tests were not run. Read-back or static inspection is not a passing test.
12
+ - Finish requested edits and read-back before the final reply; reserve the last available model request for a concise terminal account. For large files, write a small complete section, await its result, then extend it with a unique edit anchor rather than rewriting the growing file. Report delivery, actual checks and limitations once.`;
12
13
 
13
14
  export const HERMETIC_CODING_SYSTEM_PROMPT = `You are a hermetic coding assistant. The exposed file tools are the complete tool surface. Inspect and mutate only the exact paths authorized by the user request. Do not invoke hidden tools, commands, language servers or other execution paths.
14
15
 
@@ -549,7 +549,11 @@ export function createSummaryContextItem({
549
549
  Math.ceil((Buffer.byteLength(content, "utf8") - image.bytes) / 4) +
550
550
  image.tokens,
551
551
  ),
552
- priority: Math.max(...parents.map((item) => item.priority)),
552
+ // Keep the new handoff ahead of ordinary conversation in the final plan.
553
+ // Otherwise newer messages can fill the partition and evict the summary
554
+ // of the already-dropped history. This is selection priority only: the
555
+ // summary stays unpinned, data-only and subject to the same token ceilings.
556
+ priority: Math.max(600_000, ...parents.map((item) => item.priority)),
553
557
  pinned: false,
554
558
  createdAt: new Date(now).toISOString(),
555
559
  ...contentFields,
@@ -221,7 +221,6 @@ function openAIReasoningItem(value) {
221
221
  if (Object.hasOwn(value, "summary")) {
222
222
  if (
223
223
  !Array.isArray(value.summary) ||
224
- value.summary.length < 1 ||
225
224
  value.summary.length > 16 ||
226
225
  value.summary.some(
227
226
  (part) =>
@@ -264,7 +264,10 @@ export function createGovernedHubLlm(llm, factory, options = {}, sdkPorts) {
264
264
  "A Hub model client with declared locality is required",
265
265
  );
266
266
  }
267
- if (typeof factory !== "function")
267
+ if (
268
+ typeof factory !== "function" &&
269
+ !(factory === null && options.evolutionDisabled === true)
270
+ )
268
271
  throw new TypeError("A Hub evolution factory is required");
269
272
  if (options.onFailure != null && typeof options.onFailure !== "function")
270
273
  throw new TypeError("onFailure must be a function");
@@ -298,6 +301,7 @@ export function createGovernedHubLlm(llm, factory, options = {}, sdkPorts) {
298
301
  mode: "hub-analysis",
299
302
  messages,
300
303
  signal: chatOptions.signal,
304
+ evolutionDisabled: options.evolutionDisabled === true,
301
305
  });
302
306
  },
303
307
  async transport(messages, chatOptions = {}) {
@@ -77,8 +77,16 @@ function captureEmbeddingTransport(stage, sdk) {
77
77
  };
78
78
  }
79
79
 
80
- export function createGovernedHubResolver(ports, factory, sdkPorts) {
81
- if (typeof factory !== "function")
80
+ export function createGovernedHubResolver(
81
+ ports,
82
+ factory,
83
+ sdkPorts,
84
+ options = {},
85
+ ) {
86
+ if (
87
+ typeof factory !== "function" &&
88
+ !(factory === null && options.evolutionDisabled === true)
89
+ )
82
90
  throw new TypeError("A resolver evolution factory is required");
83
91
  const {
84
92
  resolver,
@@ -102,13 +110,14 @@ export function createGovernedHubResolver(ports, factory, sdkPorts) {
102
110
  const turn = await prepareGovernedModelTurn(factory, {
103
111
  mode: "hub-embedding",
104
112
  messages: [{ role: "user", content: profile }],
113
+ evolutionDisabled: options.evolutionDisabled === true,
105
114
  });
106
115
  // Chat projection prepends host provenance. Embed only the projected
107
116
  // profile: per-Run evidence IDs must not change semantic similarity.
108
- const profileMessage = turn.messages[1];
117
+ const profileMessage = turn.messages[turn.governed ? 1 : 0];
109
118
  if (
110
- turn.messages.length !== 2 ||
111
- turn.messages[0].role !== "system" ||
119
+ turn.messages.length !== (turn.governed ? 2 : 1) ||
120
+ (turn.governed && turn.messages[0].role !== "system") ||
112
121
  profileMessage?.role !== "user" ||
113
122
  typeof profileMessage.content !== "string"
114
123
  )
@@ -133,7 +142,7 @@ export function createGovernedHubResolver(ports, factory, sdkPorts) {
133
142
  const model =
134
143
  resolver._llmStage &&
135
144
  new LLMStage({
136
- llm: createGovernedHubLlm(llm, factory, {}, sdk),
145
+ llm: createGovernedHubLlm(llm, factory, options, sdk),
137
146
  acceptNonLocal: false,
138
147
  });
139
148
  return new EntityResolver({
@@ -9,6 +9,7 @@ export async function runGovernedHubSkill(
9
9
  name,
10
10
  options,
11
11
  sdkPorts,
12
+ modelTurnOptions = {},
12
13
  ) {
13
14
  let failure;
14
15
  let failed = false;
@@ -16,6 +17,7 @@ export async function runGovernedHubSkill(
16
17
  hub.llm,
17
18
  factory,
18
19
  {
20
+ evolutionDisabled: modelTurnOptions.evolutionDisabled === true,
19
21
  onFailure(error) {
20
22
  failure = error;
21
23
  failed = true;
@@ -12,9 +12,20 @@ export async function loadConfiguredUiEvolutionFactory(options = {}) {
12
12
  }
13
13
 
14
14
  export async function resolveServerEvolutionFactory(server) {
15
- return typeof server.resolveEvolutionCompositionFactory === "function"
15
+ return typeof server?.resolveEvolutionCompositionFactory === "function"
16
16
  ? server.resolveEvolutionCompositionFactory()
17
- : (server.evolutionCompositionFactory ?? null);
17
+ : (server?.evolutionCompositionFactory ?? null);
18
+ }
19
+
20
+ /** Apply the host's current UI policy for every turn, including old sessions. */
21
+ export async function prepareServerUiModelTurn(server, context) {
22
+ if (typeof server?.prepareUiModelTurn === "function") {
23
+ return server.prepareUiModelTurn(context);
24
+ }
25
+ return prepareGovernedModelTurn(
26
+ await resolveServerEvolutionFactory(server),
27
+ context,
28
+ );
18
29
  }
19
30
 
20
31
  export async function prepareConfiguredUiModelTurn(context, options = {}) {
@@ -35,7 +35,12 @@
35
35
  * so an intentionally-custom endpoint is left untouched.
36
36
  */
37
37
  export async function reconcileConfigLlmProvider(cfgLlm = {}) {
38
- if (!cfgLlm || !cfgLlm.provider || !cfgLlm.baseUrl) {
38
+ if (
39
+ !cfgLlm ||
40
+ !cfgLlm.provider ||
41
+ !cfgLlm.baseUrl ||
42
+ cfgLlm.provider === "custom"
43
+ ) {
39
44
  return { llm: cfgLlm, changed: false };
40
45
  }
41
46
  const { inferProviderFromBaseUrl, modelForeignToProvider } = await import(
@@ -71,22 +76,29 @@ export function applyConfigLlmDefaults(options = {}, cfgLlm = {}, opts = {}) {
71
76
  // credentials are exactly right. Mixing config into a DIFFERENT explicit
72
77
  // provider is still avoided (handled by the early return below).
73
78
  if (options.provider && configMatchesTarget) {
74
- if (!options.baseUrl && cfgLlm.baseUrl) options.baseUrl = connection.baseUrl;
79
+ if (!options.baseUrl && cfgLlm.baseUrl)
80
+ options.baseUrl = connection.baseUrl;
75
81
  if (options.serviceTier == null && cfgLlm.serviceTier != null)
76
82
  options.serviceTier = cfgLlm.serviceTier;
77
- if (!options.apiKey && connection.apiKey) options.apiKey = connection.apiKey;
83
+ if (!options.apiKey && connection.apiKey)
84
+ options.apiKey = connection.apiKey;
78
85
  if (!options.model && !opts.explicitModel && cfgLlm.model) {
79
86
  options.model = cfgLlm.model;
80
87
  }
81
88
  return options;
82
89
  }
83
90
  if (options.provider) {
84
- if (!options.apiKey && connection.apiKey) options.apiKey = connection.apiKey;
91
+ if (!options.apiKey && connection.apiKey)
92
+ options.apiKey = connection.apiKey;
85
93
  return options;
86
94
  }
87
95
  if (!cfgLlm.provider) return options;
88
96
  options.provider = connection.provider;
89
- if (configMatchesTarget && options.serviceTier == null && cfgLlm.serviceTier != null)
97
+ if (
98
+ configMatchesTarget &&
99
+ options.serviceTier == null &&
100
+ cfgLlm.serviceTier != null
101
+ )
90
102
  options.serviceTier = cfgLlm.serviceTier;
91
103
  if (opts.explicitModel) {
92
104
  options.model = opts.explicitModel;
@@ -31,7 +31,10 @@ export function normalizeLlmConnection(input) {
31
31
  if (value[key].length > (key === "apiKey" ? 8192 : 2048))
32
32
  throw new Error(`${key} is too long`);
33
33
  }
34
- if (!Object.hasOwn(BUILT_IN_PROVIDERS, value.provider))
34
+ if (
35
+ value.provider !== "custom" &&
36
+ !Object.hasOwn(BUILT_IN_PROVIDERS, value.provider)
37
+ )
35
38
  throw new Error("Choose a supported provider/protocol");
36
39
  if (!value.model || /[\r\n\0]/.test(value.model + value.visionModel))
37
40
  throw new Error("A valid model name is required");
@@ -84,7 +87,7 @@ export function saveLlmConnection(input, { storage = "auto" } = {}) {
84
87
  current.baseUrl || BUILT_IN_PROVIDERS[current.provider]?.baseUrl || "",
85
88
  ).replace(/\/+$/, "") === value.baseUrl;
86
89
  if (
87
- value.provider !== "ollama" &&
90
+ !["ollama", "custom"].includes(value.provider) &&
88
91
  !value.apiKey &&
89
92
  (!sameEndpoint || !current.apiKey)
90
93
  )
@@ -98,6 +101,11 @@ export function saveLlmConnection(input, { storage = "auto" } = {}) {
98
101
  baseUrl: value.baseUrl,
99
102
  visionModel: value.visionModel || null,
100
103
  };
104
+ // A keyless custom endpoint must never receive the previous target's key.
105
+ if (!sameEndpoint && !value.apiKey && value.provider === "custom") {
106
+ delete config.llm.apiKey;
107
+ delete config.llm.apiKeyHelper;
108
+ }
101
109
  };
102
110
  // A single lock and atomic rename bind endpoint and credential together.
103
111
  // An interrupted save cannot route the old credential to the new endpoint.
@@ -3,6 +3,7 @@ import { estimateCost } from "./llm-pricing.js";
3
3
  import { providerFetch } from "./formal-budget-provider-scope.js";
4
4
  import { isFormalBudgetError } from "./formal-budget-errors.js";
5
5
  import { normalizeOllamaBaseUrl } from "./ollama-base-url.js";
6
+ import { assertCustomProviderConnection } from "./provider-protocol.js";
6
7
  import { resolveModelCapabilityProfile } from "./model-capabilities.js";
7
8
  import {
8
9
  projectEnvelopePricingContext,
@@ -159,6 +160,7 @@ export async function probeLlmConnection(
159
160
  target,
160
161
  { fetchImpl = fetch, timeoutMs = 20_000, includeUsage = false } = {},
161
162
  ) {
163
+ assertCustomProviderConnection(target.provider, target);
162
164
  const base =
163
165
  target.provider === "ollama"
164
166
  ? normalizeOllamaBaseUrl(target.baseUrl)
@@ -183,7 +185,8 @@ export async function probeLlmConnection(
183
185
  options: { num_predict: PROBE_OUTPUT_TOKENS },
184
186
  };
185
187
  } else {
186
- if (!target.apiKey) throw new Error("API key required");
188
+ if (!target.apiKey && target.provider !== "custom")
189
+ throw new Error("API key required");
187
190
  if (target.provider === "anthropic") {
188
191
  url = `${base}/messages`;
189
192
  headers["x-api-key"] = target.apiKey;
@@ -211,7 +214,7 @@ export async function probeLlmConnection(
211
214
  });
212
215
  } else {
213
216
  url = `${base}/chat/completions`;
214
- headers.Authorization = `Bearer ${target.apiKey}`;
217
+ if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
215
218
  body = {
216
219
  model: target.model,
217
220
  messages: [{ role: "user", content: "Say hi in one word." }],
@@ -1,6 +1,9 @@
1
1
  import { BUILT_IN_PROVIDERS } from "./llm-providers.js";
2
2
  import { normalizeOllamaBaseUrl } from "./ollama-base-url.js";
3
- import { normalizeProviderBaseUrl } from "./provider-protocol.js";
3
+ import {
4
+ assertCustomProviderConnection,
5
+ normalizeProviderBaseUrl,
6
+ } from "./provider-protocol.js";
4
7
  import { resolveApiKeyFromHelper } from "./api-key-helper.js";
5
8
 
6
9
  const DEFAULT_PROVIDER = "ollama";
@@ -33,9 +36,8 @@ export function resolveLlmConnection(
33
36
  defaultBaseUrl = null,
34
37
  } = {},
35
38
  ) {
36
- const sources = (Array.isArray(configuredSources)
37
- ? configuredSources
38
- : [configuredSources]
39
+ const sources = (
40
+ Array.isArray(configuredSources) ? configuredSources : [configuredSources]
39
41
  ).filter((source) => source && typeof source === "object");
40
42
  const provider =
41
43
  (nonEmpty(options.provider) && options.provider) ||
@@ -67,6 +69,7 @@ export function resolveLlmConnection(
67
69
  defaultModel ||
68
70
  definition?.models?.[0] ||
69
71
  null;
72
+ assertCustomProviderConnection(provider, { baseUrl, model });
70
73
  let apiKey =
71
74
  (nonEmpty(options.apiKey) && options.apiKey) ||
72
75
  (nonEmpty(env?.CC_API_KEY) && env.CC_API_KEY) ||
@@ -127,6 +127,19 @@ export const BUILT_IN_PROVIDERS = {
127
127
  },
128
128
  };
129
129
 
130
+ /** Custom platforms use Chat Completions with an explicit endpoint and model. */
131
+ export function resolveProviderDefinition(provider) {
132
+ return provider === "custom"
133
+ ? {
134
+ name: "custom",
135
+ baseUrl: null,
136
+ models: [],
137
+ apiKeyEnv: "CC_API_KEY",
138
+ custom: true,
139
+ }
140
+ : BUILT_IN_PROVIDERS[provider];
141
+ }
142
+
130
143
  // A connectivity probe (`testProvider`) sends a tiny "Hi" and expects a quick
131
144
  // reply, so it must fail fast — a probe that hangs forever defeats its purpose
132
145
  // (it backs `cc llm test`, provider selection, doctor). Bound it with a total
@@ -402,17 +415,17 @@ export class LLMProviderRegistry {
402
415
  const res = await _fetchWithTimeout(
403
416
  `${normalizeProviderBaseUrl(provider.baseUrl)}/messages`,
404
417
  {
405
- method: "POST",
406
- headers: {
407
- "Content-Type": "application/json",
408
- "x-api-key": key,
409
- "anthropic-version": "2023-06-01",
410
- },
411
- body: JSON.stringify({
412
- model: testModel,
413
- max_tokens: 10,
414
- messages: [{ role: "user", content: "Hi" }],
415
- }),
418
+ method: "POST",
419
+ headers: {
420
+ "Content-Type": "application/json",
421
+ "x-api-key": key,
422
+ "anthropic-version": "2023-06-01",
423
+ },
424
+ body: JSON.stringify({
425
+ model: testModel,
426
+ max_tokens: 10,
427
+ messages: [{ role: "user", content: "Hi" }],
428
+ }),
416
429
  },
417
430
  );
418
431
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
@@ -103,7 +103,8 @@ function sanitizeReasoningItems(items) {
103
103
  type: "reasoning",
104
104
  ...(id ? { id } : {}),
105
105
  ...(encryptedContent ? { encrypted_content: encryptedContent } : {}),
106
- ...(summary.length ? { summary } : {}),
106
+ // Responses requires this field even when no summary was requested.
107
+ summary,
107
108
  ...(status ? { status } : {}),
108
109
  });
109
110
  }