agent-accelerator 0.5.8 → 0.6.0

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 (67) hide show
  1. package/README.md +26 -5
  2. package/examples/prompts/SYSTEM_PROMPT.md +2 -0
  3. package/examples/prompts/SYSTEM_PROMPT_AGENT.md +4 -0
  4. package/examples/prompts/SYSTEM_PROMPT_TOOLS.md +1 -0
  5. package/package.json +4 -1
  6. package/src/agent/agent.ts +374 -167
  7. package/src/agent/architecture.md +14 -12
  8. package/src/agent/config-blocks.ts +39 -12
  9. package/src/agent/context.ts +8 -3
  10. package/src/agent/delegation.ts +82 -29
  11. package/src/agent/loop.ts +449 -373
  12. package/src/agent/subagent.ts +1 -3
  13. package/src/agent/subagents-tool.ts +45 -7
  14. package/src/agent/subagents.md +3 -0
  15. package/src/agent/turns.ts +3 -1
  16. package/src/agent/worker.ts +58 -28
  17. package/src/agent/workers.ts +113 -20
  18. package/src/architecture.md +16 -15
  19. package/src/index.ts +34 -10
  20. package/src/models/architecture.md +4 -3
  21. package/src/models/catalog-cache.ts +6 -5
  22. package/src/models/catalog.ts +46 -49
  23. package/src/providers/architecture.md +10 -8
  24. package/src/providers/google.ts +150 -62
  25. package/src/providers/openai-compat.ts +60 -58
  26. package/src/providers/openai.ts +57 -39
  27. package/src/providers/openrouter.ts +87 -32
  28. package/src/providers/registry.ts +12 -26
  29. package/src/providers/shared.ts +89 -39
  30. package/src/providers.ts +56 -115
  31. package/src/scheduler/architecture.md +9 -8
  32. package/src/scheduler/scheduled-actions.md +4 -0
  33. package/src/scheduler/scheduler.ts +186 -60
  34. package/src/scheduler/store.ts +23 -10
  35. package/src/scheduler/time.ts +17 -3
  36. package/src/scheduler/tool.ts +33 -7
  37. package/src/session/architecture.md +7 -6
  38. package/src/session/journal.ts +38 -12
  39. package/src/session/store.ts +122 -61
  40. package/src/session/usage.ts +58 -9
  41. package/src/streaming/architecture.md +4 -3
  42. package/src/streaming/event-stream.ts +26 -12
  43. package/src/telemetry.ts +6 -2
  44. package/src/tools/architecture.md +8 -7
  45. package/src/tools/executor.ts +50 -48
  46. package/src/tools/schema.ts +10 -6
  47. package/src/tools/subagent-result.ts +6 -1
  48. package/src/tools/tool.ts +9 -11
  49. package/src/types/agent.ts +2 -21
  50. package/src/types/architecture.md +11 -10
  51. package/src/types/core.ts +1 -9
  52. package/src/types/errors.ts +26 -4
  53. package/src/types/model.ts +0 -4
  54. package/src/types/tool.ts +12 -2
  55. package/src/update-models.ts +5 -5
  56. package/src/utils/architecture.md +16 -15
  57. package/src/utils/cache.ts +1 -3
  58. package/src/utils/durable.ts +17 -5
  59. package/src/utils/env.ts +9 -7
  60. package/src/utils/errors.ts +50 -44
  61. package/src/utils/headers.ts +3 -10
  62. package/src/utils/media.ts +23 -8
  63. package/src/utils/raw-capture.ts +9 -2
  64. package/src/utils/retry.ts +17 -31
  65. package/src/utils/safe-io.ts +29 -7
  66. package/src/utils/serialization.ts +1 -4
  67. package/src/utils/session.ts +1 -2
package/src/index.ts CHANGED
@@ -37,7 +37,14 @@ export { zodToJsonSchema, cleanJsonSchema } from "./tools/schema.ts";
37
37
  */
38
38
  export { executeToolCalls } from "./tools/executor.ts";
39
39
 
40
- export { assertPathAllowed, enforceFileAccess, fetchRemoteBytes, isPrivateAddress, RemoteFetchError, MAX_REMOTE_FETCH_BYTES } from "./utils/safe-io.ts";
40
+ export {
41
+ assertPathAllowed,
42
+ enforceFileAccess,
43
+ fetchRemoteBytes,
44
+ isPrivateAddress,
45
+ RemoteFetchError,
46
+ MAX_REMOTE_FETCH_BYTES,
47
+ } from "./utils/safe-io.ts";
41
48
 
42
49
  // Multi-Agent Delegation & Tools
43
50
  export {
@@ -126,12 +133,8 @@ export {
126
133
  GoogleInteractionsProvider,
127
134
  clearInteractionChains,
128
135
  } from "./providers/google.ts";
129
- export {
130
- OpenRouterChatCompletionsProvider,
131
- } from "./providers/openrouter.ts";
132
- export {
133
- OpenAIResponsesProvider,
134
- } from "./providers/openai.ts";
136
+ export { OpenRouterChatCompletionsProvider } from "./providers/openrouter.ts";
137
+ export { OpenAIResponsesProvider } from "./providers/openai.ts";
135
138
  /**
136
139
  * @deprecated Internal provider mappers/routing helpers. Prefer the
137
140
  * provider-agnostic registry (`resolveModel`/`getProvider`); direct mapper
@@ -251,14 +254,36 @@ export {
251
254
  emptyTotals,
252
255
  } from "./session/store.ts";
253
256
  export type { UsageReport, UsageLine, UsageTotals, AgentUsage, WorkerUsage, CostSource } from "./session/usage.ts";
254
- export type { SessionLimits, SessionPayloadStatus, PersistedAgentSession, SessionTotals, SessionAgentLike, LoadedSessionDir, PersistedSubAgentTrace, SessionRunMark, SessionRunEnd, SessionPending, SessionPendingPrompt, SessionToolRecord } from "./session/store.ts";
257
+ export type {
258
+ SessionLimits,
259
+ SessionPayloadStatus,
260
+ PersistedAgentSession,
261
+ SessionTotals,
262
+ SessionAgentLike,
263
+ LoadedSessionDir,
264
+ PersistedSubAgentTrace,
265
+ SessionRunMark,
266
+ SessionRunEnd,
267
+ SessionPending,
268
+ SessionPendingPrompt,
269
+ SessionToolRecord,
270
+ } from "./session/store.ts";
255
271
 
256
272
  // Utilities
257
273
  /**
258
274
  * @deprecated Session-routing internals. Prefer agent-level `sessionId`;
259
275
  * direct use moves to the future `/internal` entry.
260
276
  */
261
- export { createSessionId, hashSessionPart, createTrackingId, nowMs, newRunId, newTurnId, previewPrompt, parseModelRef } from "./utils/session.ts";
277
+ export {
278
+ createSessionId,
279
+ hashSessionPart,
280
+ createTrackingId,
281
+ nowMs,
282
+ newRunId,
283
+ newTurnId,
284
+ previewPrompt,
285
+ parseModelRef,
286
+ } from "./utils/session.ts";
262
287
  export type { ParsedModelRef } from "./utils/session.ts";
263
288
  /**
264
289
  * @deprecated Raw env access. Prefer explicit `Agent` config (which wins over
@@ -344,7 +369,6 @@ export type {
344
369
  RetryPolicyConfig,
345
370
  AgentLogger,
346
371
  ToolChoiceOption,
347
- StopOption,
348
372
  Budget,
349
373
  BudgetState,
350
374
  SubAgentRunIds,
@@ -2,12 +2,13 @@
2
2
 
3
3
  The model catalog: context limits, prices, modalities and reasoning options come from models.dev, not from code.
4
4
 
5
- | File | What it is |
6
- | :-- | :-- |
5
+ | File | What it is |
6
+ | :----------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
7
7
  | `catalog-cache.ts` | Downloads and caches `models.dev/api.json` in `<home>/catalog/` (or `AGENT_CACHE_DIR`). Stale-while-revalidate: an expired catalog is served at once and refreshed in the background (8 s timeout, 10 min backoff after a failure); only a cold start waits. `AGENT_CATALOG_REFRESH=off` stops automatic refreshes. |
8
- | `catalog.ts` | Lookups (`getModelFromCatalog`), `registerModel`, thinking validation (`validateModelThinking`), and the exported `GOOGLE_/OPENAI_/OPENROUTER_MODELS` arrays (rebuilt in place when the catalog changes). |
8
+ | `catalog.ts` | Lookups (`getModelFromCatalog`), `registerModel`, thinking validation (`validateModelThinking`, which throws `ThinkingLevelError`, a one-line `ConfigError`), and the exported `GOOGLE_/OPENAI_/OPENROUTER_MODELS` arrays (rebuilt in place when the catalog changes). |
9
9
 
10
10
  ## Invariants
11
+
11
12
  - Never hand-edit the cache file; register custom models with `registerModel()`. Verified modality fixes go in `MODALITY_OVERRIDES`.
12
13
  - The global bare-id fallback applies to first-class providers and explicit bare-id lookups only. A custom prefix never borrows another provider's price or modalities: unknown stays unknown.
13
14
  - A payload that is not a catalog (fewer than 3 providers with models) never overwrites the cache.
@@ -313,7 +313,7 @@ export async function refreshModelCatalog(options: RefreshCatalogOptions = {}):
313
313
  fallbackToDiskCache();
314
314
  return getCatalogStatus();
315
315
  }
316
- } catch (err: any) {
316
+ } catch {
317
317
  clearTimeout(timer);
318
318
  // If download fails, retain disk cache if available
319
319
  fallbackToDiskCache();
@@ -356,7 +356,6 @@ export async function refreshModelCatalog(options: RefreshCatalogOptions = {}):
356
356
  return getCatalogStatus();
357
357
  }
358
358
 
359
-
360
359
  /** True when `AGENT_CATALOG_REFRESH` is `off`/`0`/`false`: never touch the network automatically. */
361
360
  function automaticRefreshDisabled(): boolean {
362
361
  const v = getEnv("AGENT_CATALOG_REFRESH")?.trim().toLowerCase();
@@ -388,9 +387,11 @@ export async function ensureModelCatalogFresh(options: RefreshCatalogOptions = {
388
387
  const start = (): Promise<CatalogStatus> => {
389
388
  if (!inFlightRefresh) {
390
389
  const timeoutMs = options.timeoutMs ?? (current.loaded ? BACKGROUND_REFRESH_TIMEOUT_MS : undefined);
391
- inFlightRefresh = refreshModelCatalog({ ...options, ...(timeoutMs !== undefined ? { timeoutMs } : {}) }).finally(() => {
392
- inFlightRefresh = null;
393
- });
390
+ inFlightRefresh = refreshModelCatalog({ ...options, ...(timeoutMs !== undefined ? { timeoutMs } : {}) }).finally(
391
+ () => {
392
+ inFlightRefresh = null;
393
+ }
394
+ );
394
395
  }
395
396
  return inFlightRefresh;
396
397
  };
@@ -5,20 +5,10 @@
5
5
  * rebuilt in place whenever it changes.
6
6
  */
7
7
 
8
- import {
9
- getActiveCatalog,
10
- registerCatalogUpdateListener,
11
- refreshModelCatalog,
12
- ensureModelCatalogFresh,
13
- getCatalogStatus,
14
- setCatalogTTL,
15
- getCatalogTTL,
16
- DEFAULT_CATALOG_TTL_MS,
17
- type CatalogStatus,
18
- type RefreshCatalogOptions,
19
- } from "./catalog-cache.ts";
8
+ import { getActiveCatalog, registerCatalogUpdateListener } from "./catalog-cache.ts";
20
9
  import type { ModelSpec, ProviderId, ModelLimit, ModelCost, ModelModalities } from "../types/model.ts";
21
10
  import type { ThinkingLevel } from "../types/core.ts";
11
+ import { ConfigError, ValidationError } from "../types/errors.ts";
22
12
 
23
13
  // ---------------------------------------------------------------------------
24
14
  // Provider aliases — maps our internal ids to models.dev keys
@@ -48,8 +38,8 @@ function applyModalityOverrides(provider: string, modelId: string, base: ModelMo
48
38
  MODALITY_OVERRIDES[`${provider}/${normalizeModelIdForLookup(modelId)}`];
49
39
  if (!hit) return base;
50
40
  return {
51
- input: ((hit.input ?? base.input) as ModelModalities["input"]),
52
- output: ((hit.output ?? base.output) as ModelModalities["output"]),
41
+ input: (hit.input ?? base.input) as ModelModalities["input"],
42
+ output: (hit.output ?? base.output) as ModelModalities["output"],
53
43
  };
54
44
  }
55
45
 
@@ -106,7 +96,7 @@ function customRegistryKey(provider: string, modelId: string): string {
106
96
  */
107
97
  export function registerModel(spec: ModelSpec): void {
108
98
  if (!spec || typeof spec.id !== "string" || typeof spec.provider !== "string") {
109
- throw new Error("[Agent Accelerator] registerModel requires spec.id and spec.provider strings");
99
+ throw new ValidationError("[Agent Accelerator] registerModel requires spec.id and spec.provider strings");
110
100
  }
111
101
  customModelRegistry.set(customRegistryKey(String(spec.provider), String(spec.id)), { ...spec });
112
102
  // Invalidate derived caches so the custom model is visible immediately.
@@ -135,30 +125,38 @@ export function getModelSource(provider: string, modelId: string): "catalog" | "
135
125
  if (customModelRegistry.has(customRegistryKey(p, m)) || customModelRegistry.has(customRegistryKey(p, norm))) {
136
126
  return "override";
137
127
  }
138
- if (
139
- MODALITY_OVERRIDES[`${p}/${m}`] !== undefined ||
140
- MODALITY_OVERRIDES[`${p}/${norm}`] !== undefined
141
- ) {
128
+ if (MODALITY_OVERRIDES[`${p}/${m}`] !== undefined || MODALITY_OVERRIDES[`${p}/${norm}`] !== undefined) {
142
129
  return "override";
143
130
  }
144
131
  return getModelFromCatalog(provider, modelId) ? "catalog" : "unknown";
145
132
  }
146
133
 
147
134
  // Derives capability flags (thinking, caching, tools, structured output, modalities) from a raw catalog entry; in: mapModelSpec, out: ModelCapabilities.
148
- function mapCapabilities(raw: any, cost: ModelCost, reasoning: boolean, toolCall: boolean, modalities: ModelModalities) {
135
+ function mapCapabilities(
136
+ raw: any,
137
+ cost: ModelCost,
138
+ reasoning: boolean,
139
+ toolCall: boolean,
140
+ modalities: ModelModalities
141
+ ) {
149
142
  return {
150
143
  supportsThinking: reasoning,
151
144
  supportsThinkingLevel: !!raw.reasoning_options,
152
- supportsThinkingBudget: Array.isArray(raw.reasoning_options) && raw.reasoning_options.some((o: any) => o.type === "budget_tokens"),
145
+ supportsThinkingBudget:
146
+ Array.isArray(raw.reasoning_options) && raw.reasoning_options.some((o: any) => o.type === "budget_tokens"),
153
147
  supportsImplicitCaching: cost.cache_read !== undefined || cost.cache_write !== undefined,
154
148
  supportsLongCacheRetention: cost.cache_read !== undefined,
155
149
  supportsExplicitCaching: cost.cache_read !== undefined,
156
150
  supportsParallelToolCalls: toolCall,
157
151
  supportsStreaming: true,
158
152
  supportsStructuredOutput: raw.structured_output === undefined ? true : !!raw.structured_output,
159
- supportsReasoningToggle: Array.isArray(raw.reasoning_options) && raw.reasoning_options.some((o: any) => o.type === "toggle"),
160
- supportsReasoningEffort: Array.isArray(raw.reasoning_options) && raw.reasoning_options.some((o: any) => o.type === "effort"),
161
- modalities: [...(modalities.input || []), ...(modalities.output || [])].filter((v: any, i: any, a: any) => a.indexOf(v) === i),
153
+ supportsReasoningToggle:
154
+ Array.isArray(raw.reasoning_options) && raw.reasoning_options.some((o: any) => o.type === "toggle"),
155
+ supportsReasoningEffort:
156
+ Array.isArray(raw.reasoning_options) && raw.reasoning_options.some((o: any) => o.type === "effort"),
157
+ modalities: [...(modalities.input || []), ...(modalities.output || [])].filter(
158
+ (v: any, i: any, a: any) => a.indexOf(v) === i
159
+ ),
162
160
  };
163
161
  }
164
162
 
@@ -267,7 +265,9 @@ export function getModelFromCatalog(providerInput: string, modelIdInput: string)
267
265
  }
268
266
 
269
267
  const aliasProviders = PROVIDER_ALIASES[provider] || [provider];
270
- const tryProviders = [...new Set([...aliasProviders, provider, provider.replace("-zen", ""), provider.replace("-go", "")])];
268
+ const tryProviders = [
269
+ ...new Set([...aliasProviders, provider, provider.replace("-zen", ""), provider.replace("-go", "")]),
270
+ ];
271
271
  const catalog = getActiveCatalog();
272
272
 
273
273
  for (const p of tryProviders) {
@@ -291,7 +291,11 @@ export function getModelFromCatalog(providerInput: string, modelIdInput: string)
291
291
  // or normalized equality. The old loose `modelId.endsWith(key)` is removed
292
292
  // (e.g. "gpt-4o-mini-extra" must not match "mini").
293
293
  for (const [key, raw] of Object.entries(providerData.models as Record<string, any>)) {
294
- if (key === modelId || key.endsWith(`/${modelId}`) || normalizeModelIdForLookup(key) === normalizeModelIdForLookup(modelId)) {
294
+ if (
295
+ key === modelId ||
296
+ key.endsWith(`/${modelId}`) ||
297
+ normalizeModelIdForLookup(key) === normalizeModelIdForLookup(modelId)
298
+ ) {
295
299
  result = mapModelSpec(p as ProviderId, key, raw as any);
296
300
  setLookupCache(cacheKey, result);
297
301
  return result;
@@ -410,7 +414,8 @@ export function getModelThinkingInfo(provider: string, modelId: string): ModelTh
410
414
  allowed.add("dynamic");
411
415
 
412
416
  const allowedLevels = Array.from(allowed);
413
- const supportsDisable = allowed.has("none") || !!toggleOpt || Boolean(budgetOpt && (budgetOpt.min === undefined || budgetOpt.min <= 0));
417
+ const supportsDisable =
418
+ allowed.has("none") || !!toggleOpt || Boolean(budgetOpt && (budgetOpt.min === undefined || budgetOpt.min <= 0));
414
419
 
415
420
  return {
416
421
  supportsThinking: true,
@@ -421,8 +426,8 @@ export function getModelThinkingInfo(provider: string, modelId: string): ModelTh
421
426
  };
422
427
  }
423
428
 
424
- /** Thrown when a requested ThinkingLevel is unsupported by the selected model. */
425
- export class ThinkingLevelError extends Error {
429
+ /** Thrown when a requested ThinkingLevel is unsupported by the selected model: a `ConfigError` (one line, never retried) with `allowedLevels`. */
430
+ export class ThinkingLevelError extends ConfigError {
426
431
  readonly provider: string;
427
432
  readonly modelId: string;
428
433
  readonly requestedLevel: string;
@@ -443,31 +448,19 @@ export class ThinkingLevelError extends Error {
443
448
  reason: string;
444
449
  remedy?: string;
445
450
  }) {
446
- const remedyLines = opts.remedy
447
- ? `\n\n How to fix:\n ${opts.remedy}`
448
- : "";
449
- const allowedLine = opts.allowedLevels.length > 0
450
- ? `\n Allowed Levels: [${opts.allowedLevels.map((l) => `"${l}"`).join(", ")}]`
451
- : "";
452
-
453
- const formattedMessage =
454
- `[Agent Accelerator] ThinkingLevel Mismatch for "${opts.provider}/${opts.modelId}":\n` +
455
- ` Requested Level: "${opts.requestedLevel}"\n` +
456
- ` Issue: ${opts.reason}` +
457
- allowedLine +
458
- remedyLines;
459
-
460
- super(formattedMessage);
451
+ const allowed =
452
+ opts.allowedLevels.length > 0 ? ` Allowed levels: ${opts.allowedLevels.map((l) => `"${l}"`).join(", ")}.` : "";
453
+ const remedy = opts.remedy ? ` How to fix: ${opts.remedy}` : "";
454
+ super(
455
+ `[Agent Accelerator] Thinking level "${opts.requestedLevel}" does not fit "${opts.provider}/${opts.modelId}": ${opts.reason}${allowed}${remedy}`,
456
+ { provider: opts.provider, model: opts.modelId }
457
+ );
461
458
  this.name = "ThinkingLevelError";
462
459
  this.provider = opts.provider;
463
460
  this.modelId = opts.modelId;
464
461
  this.requestedLevel = opts.requestedLevel;
465
462
  this.allowedLevels = opts.allowedLevels;
466
463
  this.supportsThinking = opts.supportsThinking;
467
-
468
- if (Error.captureStackTrace) {
469
- Error.captureStackTrace(this, ThinkingLevelError);
470
- }
471
464
  }
472
465
  }
473
466
 
@@ -476,7 +469,11 @@ export class ThinkingLevelError extends Error {
476
469
  *
477
470
  * @example `validateModelThinking("google", "gemini-3.5-flash-lite", "medium");`
478
471
  */
479
- export function validateModelThinking(provider: string, modelId: string, requestedLevel?: ThinkingLevel | string): void {
472
+ export function validateModelThinking(
473
+ provider: string,
474
+ modelId: string,
475
+ requestedLevel?: ThinkingLevel | string
476
+ ): void {
480
477
  if (!requestedLevel) return;
481
478
  const level = String(requestedLevel).toLowerCase().trim();
482
479
  const info = getModelThinkingInfo(provider, modelId);
@@ -2,19 +2,21 @@
2
2
 
3
3
  All HTTP and wire parsing. Canonical mappers (thinking, cache, tool choice, structured output) live in `../providers.ts`, never here.
4
4
 
5
- | File | Wire | Notes |
6
- | :-- | :-- | :-- |
7
- | `google.ts` | Google Interactions | Stateful chaining (TTL gate, self-heal on `not_found`), exact replay of raw `steps`, media consts at the top, Gemini 4 long decode as background interactions. |
8
- | `openai.ts` | OpenAI Responses | Stateless (`store:false`), full history every turn. |
9
- | `openrouter.ts` | OpenRouter Chat Completions | Every request streams (so it can be cancelled); `generate()` collects its own stream. |
10
- | `openai-compat.ts` | OpenAI-compatible Chat Completions | Serves every custom prefix; strict subset of fields; no default base URL. |
11
- | `registry.ts` | - | Routes `provider/model` to an adapter; the registry is immutable; `ModelProvider` helpers. |
12
- | `shared.ts` | - | Helpers every adapter uses: signals and timers, error payloads, ids, retry-after, SSE scaffold (`linkStreamAbort`, `withAbortTimer`, `readSseData`, `incompleteStreamCheck`, `failStream`). |
5
+ | File | Wire | Notes |
6
+ | :----------------- | :--------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
7
+ | `google.ts` | Google Interactions | Stateful chaining (TTL gate, self-heal on `not_found`), exact replay of raw `steps`, media consts at the top, Gemini 4 long decode as background interactions. |
8
+ | `openai.ts` | OpenAI Responses | Stateless (`store:false`), full history every turn. |
9
+ | `openrouter.ts` | OpenRouter Chat Completions | Every request streams (so it can be cancelled); `generate()` collects its own stream. |
10
+ | `openai-compat.ts` | OpenAI-compatible Chat Completions | Serves every custom prefix; strict subset of fields; no default base URL. |
11
+ | `registry.ts` | - | Routes `provider/model` to an adapter; the registry is immutable; `ModelProvider` helpers. |
12
+ | `shared.ts` | - | Helpers every adapter uses: signals and timers, error payloads, ids, retry-after, SSE scaffold (`linkStreamAbort`, `withAbortTimer`, `readSseData`, `incompleteStreamCheck`, `failStream`). |
13
13
 
14
14
  ## Contract (`Provider`, in `../types/model.ts`)
15
+
15
16
  `generate(model, context, options)` returns a `ProviderGenerateResult`; `stream(...)` returns an `AssistantMessageEventStream` that ends with an `AgentResponse`; optional `isLongRunning()` tells the loop to skip its whole-call timeouts.
16
17
 
17
18
  ## Invariants
19
+
18
20
  - Fix shared streaming behaviour in `shared.ts`, never per adapter. Event parsing and request building stay in each adapter.
19
21
  - OpenRouter and compat chat chunk handlers are deliberately not merged.
20
22
  - A custom prefix never gets another provider's key or base URL.