melchizedek-agents 0.16.2 → 0.17.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 (82) hide show
  1. package/.env.example +17 -0
  2. package/AGENT_SETUP.md +2 -1
  3. package/CHANGELOG.md +142 -0
  4. package/DOCUMENTATION.md +237 -7
  5. package/QUICKSTART.md +2 -2
  6. package/README.md +125 -149
  7. package/config/agents/examples/harness.yaml +135 -0
  8. package/config/agents/examples/model_zoo.yaml +23 -9
  9. package/config/agents/examples/pipeline.yaml +120 -0
  10. package/config/agents/examples/specs/open-meteo-forecast.json +77 -0
  11. package/config/agents/examples/specs/open-meteo-geocoding.json +53 -0
  12. package/config/agents/examples/weather.yaml +59 -0
  13. package/config/agents/syndicateSchema.yaml +112 -1
  14. package/db/migrations/0011_erase_expired.sql +198 -0
  15. package/dist/lib/a2a/app.d.ts +7 -0
  16. package/dist/lib/a2a/app.js +7 -1
  17. package/dist/lib/a2a/executor.js +41 -0
  18. package/dist/lib/compile.d.ts +40 -1
  19. package/dist/lib/compile.js +132 -14
  20. package/dist/lib/config.d.ts +17 -0
  21. package/dist/lib/config.js +17 -0
  22. package/dist/lib/doctor.js +17 -4
  23. package/dist/lib/index.d.ts +2 -0
  24. package/dist/lib/index.js +1 -0
  25. package/dist/lib/loadSyndicate.d.ts +46 -0
  26. package/dist/lib/loadSyndicate.js +19 -3
  27. package/dist/lib/memory/erase.d.ts +5 -0
  28. package/dist/lib/memory/erase.js +5 -0
  29. package/dist/lib/models/capabilities.js +10 -0
  30. package/dist/lib/models/endpoints.d.ts +1 -1
  31. package/dist/lib/models/endpoints.js +4 -1
  32. package/dist/lib/models/gateway.d.ts +2 -2
  33. package/dist/lib/models/gateway.js +4 -4
  34. package/dist/lib/models/kimiLlm.d.ts +123 -0
  35. package/dist/lib/models/kimiLlm.js +171 -0
  36. package/dist/lib/models/providerMap.d.ts +2 -2
  37. package/dist/lib/models/providerMap.js +4 -1
  38. package/dist/lib/models/registry.d.ts +1 -1
  39. package/dist/lib/models/registry.js +11 -3
  40. package/dist/lib/net/redirects.d.ts +14 -0
  41. package/dist/lib/net/redirects.js +94 -0
  42. package/dist/lib/observability/tracer.d.ts +1 -1
  43. package/dist/lib/persistence/supabaseProvider.d.ts +2 -6
  44. package/dist/lib/persistence/supabaseProvider.js +2 -31
  45. package/dist/lib/runtime/questions.d.ts +55 -0
  46. package/dist/lib/runtime/questions.js +107 -0
  47. package/dist/lib/runtime/syndicateTurn.d.ts +42 -4
  48. package/dist/lib/runtime/syndicateTurn.js +162 -16
  49. package/dist/lib/storage/postgres/index.d.ts +6 -0
  50. package/dist/lib/storage/postgres/index.js +11 -0
  51. package/dist/lib/storage/rlsStatus.d.ts +45 -0
  52. package/dist/lib/storage/rlsStatus.js +69 -0
  53. package/dist/lib/syndicateSchema.d.ts +169 -0
  54. package/dist/lib/syndicateSchema.js +327 -2
  55. package/dist/lib/toolRegistry.js +9 -0
  56. package/dist/lib/tools/openapiTools.d.ts +46 -0
  57. package/dist/lib/tools/openapiTools.js +195 -0
  58. package/dist/lib/tools/skillToolset.d.ts +135 -0
  59. package/dist/lib/tools/skillToolset.js +247 -0
  60. package/dist/lib/tools/urlContextTool.d.ts +29 -0
  61. package/dist/lib/tools/urlContextTool.js +39 -0
  62. package/dist/lib/workflow.d.ts +72 -0
  63. package/dist/lib/workflow.js +180 -0
  64. package/dist/lib/workflowConfig.d.ts +95 -0
  65. package/dist/lib/workflowConfig.js +93 -0
  66. package/dist/scripts/syndicate_chat.js +39 -5
  67. package/package.json +4 -1
  68. package/skills/briefs/_shared.md +1 -1
  69. package/skills/briefs/melchizedek-author.md +8 -4
  70. package/skills/briefs/melchizedek-memory.md +1 -1
  71. package/skills/briefs/melchizedek-models.md +1 -1
  72. package/skills/briefs/melchizedek-scribe.md +4 -4
  73. package/skills/briefs/melchizedek-serve.md +1 -1
  74. package/skills/briefs/melchizedek.md +4 -1
  75. package/skills/melchizedek/SKILL.md +4 -1
  76. package/skills/melchizedek-author/SKILL.md +7 -3
  77. package/skills/melchizedek-memory/SKILL.md +1 -1
  78. package/skills/melchizedek-models/SKILL.md +1 -1
  79. package/skills/melchizedek-scribe/SKILL.md +3 -3
  80. package/skills/melchizedek-serve/SKILL.md +1 -1
  81. /package/skills/melchizedek-author/{templates → assets}/minimal.yaml +0 -0
  82. /package/skills/melchizedek-scribe/{templates → assets}/brief.md +0 -0
@@ -8,6 +8,11 @@
8
8
  * markers, and (without a namespace) its task tools' list and jobs
9
9
  * (migration 0010), in one transaction. Returns what each store lost.
10
10
  *
11
+ * With a namespace, ledger turns and A2A tasks are kept only when their
12
+ * conversation is still live in another namespace (migration 0011): they do
13
+ * not record the namespace, and a conversation whose session expired must
14
+ * not survive an erase.
15
+ *
11
16
  * Throws on any failure: a deletion request that silently did nothing is
12
17
  * worse than an error the caller can surface and retry.
13
18
  */
@@ -37,6 +37,7 @@ function directPlatform(provider) {
37
37
  export const SERVER_SIDE_TOOLS = {
38
38
  web_search: ['gemini', 'anthropic', 'openai', 'xai'],
39
39
  google_search: ['gemini'],
40
+ url_context: ['gemini'],
40
41
  x_search: ['xai'],
41
42
  collections_search: ['xai'],
42
43
  };
@@ -171,6 +172,15 @@ export const CAPABILITY_MATRIX = {
171
172
  vision: ok('test', 'user-turn images only'),
172
173
  native_search: nativeSearch('xai'),
173
174
  },
175
+ moonshot: {
176
+ delegation: ok(),
177
+ memory_tools: ok(),
178
+ structured_output: ok('test', 'strict json_schema; kimi-k2.6 is documented as unstable on complex schemas ($ref, oneOf)'),
179
+ thinking_with_tools: degraded('reasoning_content is not replayed across tool calls, which Moonshot asks for on kimi-k3, so the model re-reasons each step; effort travels as reasoning_effort (K3) or a thinking switch (K2.x)'),
180
+ streaming: ok(),
181
+ vision: ok('test', 'user-turn images only, sent as base64 (Moonshot takes no public image URLs)'),
182
+ native_search: unsupported("Moonshot's model-side $web_search retires 2026-10-20 and its successor is a REST call, not a request field; the web_search sentinel is dropped (use web_extract)"),
183
+ },
174
184
  ollama: {
175
185
  delegation: ok(),
176
186
  memory_tools: ok(),
@@ -11,7 +11,7 @@
11
11
  * gemini direct | vertex Google Application Default Credentials
12
12
  * anthropic direct | bedrock | vertex AWS credential chain / Google ADC
13
13
  * openai direct | azure AZURE_OPENAI_API_KEY, else Entra ID
14
- * xai, ollama direct only
14
+ * xai, moonshot, ollama direct only (moonshot: MOONSHOT_BASE_URL for a proxy)
15
15
  *
16
16
  * Configuration comes from the environment (`endpointFromEnv`) or, per
17
17
  * request, from the server's `credentials` plug point, which may return an
@@ -11,7 +11,7 @@
11
11
  * gemini direct | vertex Google Application Default Credentials
12
12
  * anthropic direct | bedrock | vertex AWS credential chain / Google ADC
13
13
  * openai direct | azure AZURE_OPENAI_API_KEY, else Entra ID
14
- * xai, ollama direct only
14
+ * xai, moonshot, ollama direct only (moonshot: MOONSHOT_BASE_URL for a proxy)
15
15
  *
16
16
  * Configuration comes from the environment (`endpointFromEnv`) or, per
17
17
  * request, from the server's `credentials` plug point, which may return an
@@ -35,6 +35,7 @@ export const PLATFORMS_FOR = {
35
35
  anthropic: ['direct', 'bedrock', 'vertex'],
36
36
  openai: ['direct', 'azure'],
37
37
  xai: ['direct'],
38
+ moonshot: ['direct'],
38
39
  ollama: ['direct'],
39
40
  };
40
41
  const PLATFORM_ENV = {
@@ -99,6 +100,8 @@ export function endpointFromEnv(provider, env = process.env) {
99
100
  return { ...base, ...(trimmed(env.ANTHROPIC_BASE_URL) ? { baseURL: trimmed(env.ANTHROPIC_BASE_URL) } : {}) };
100
101
  case 'openai/direct':
101
102
  return { ...base, ...(trimmed(env.OPENAI_BASE_URL) ? { baseURL: trimmed(env.OPENAI_BASE_URL) } : {}) };
103
+ case 'moonshot/direct':
104
+ return { ...base, ...(trimmed(env.MOONSHOT_BASE_URL) ? { baseURL: trimmed(env.MOONSHOT_BASE_URL) } : {}) };
102
105
  case 'openai/azure': {
103
106
  const endpoint = trimmed(env.AZURE_OPENAI_ENDPOINT);
104
107
  const key = trimmed(env.AZURE_OPENAI_API_KEY);
@@ -4,7 +4,7 @@
4
4
  * WHY this file exists:
5
5
  * A newcomer should be able to run every starter-pack syndicate with ONE
6
6
  * credential when they choose to. A hosted gateway (Vercel AI Gateway,
7
- * OpenRouter) fronts OpenAI, Anthropic, Google and xAI behind a single
7
+ * OpenRouter) fronts OpenAI, Anthropic, Google, xAI and Moonshot behind a single
8
8
  * OpenAI-compatible endpoint, so a model id whose direct key is absent can
9
9
  * still be served. This module owns that decision and nothing else.
10
10
  *
@@ -80,7 +80,7 @@ export declare function gatewayUsable(): boolean;
80
80
  * Gateways prefix every id with the upstream provider's slug and spell
81
81
  * Anthropic's version segment with a dot ("claude-sonnet-4-6" is
82
82
  * "anthropic/claude-sonnet-4.6" on both Vercel and OpenRouter). Gemini,
83
- * GPT and Grok ids already carry dots, so only the prefix is added. An id
83
+ * GPT, Grok and Kimi ids already carry dots, so only the prefix is added. An id
84
84
  * the mapper gets wrong is fixed in MODEL_GATEWAY_MODEL_MAP, not in code.
85
85
  */
86
86
  export declare function gatewayWireModel(model: string, gateway: GatewayInfo): string;
@@ -4,7 +4,7 @@
4
4
  * WHY this file exists:
5
5
  * A newcomer should be able to run every starter-pack syndicate with ONE
6
6
  * credential when they choose to. A hosted gateway (Vercel AI Gateway,
7
- * OpenRouter) fronts OpenAI, Anthropic, Google and xAI behind a single
7
+ * OpenRouter) fronts OpenAI, Anthropic, Google, xAI and Moonshot behind a single
8
8
  * OpenAI-compatible endpoint, so a model id whose direct key is absent can
9
9
  * still be served. This module owns that decision and nothing else.
10
10
  *
@@ -49,14 +49,14 @@ export const GATEWAYS = {
49
49
  label: 'Vercel AI Gateway',
50
50
  baseUrl: 'https://ai-gateway.vercel.sh/v1',
51
51
  consoleUrl: 'https://vercel.com/ai-gateway',
52
- slugs: { gemini: 'google', anthropic: 'anthropic', openai: 'openai', xai: 'xai' },
52
+ slugs: { gemini: 'google', anthropic: 'anthropic', openai: 'openai', xai: 'xai', moonshot: 'moonshotai' },
53
53
  },
54
54
  openrouter: {
55
55
  id: 'openrouter',
56
56
  label: 'OpenRouter',
57
57
  baseUrl: 'https://openrouter.ai/api/v1',
58
58
  consoleUrl: 'https://openrouter.ai/keys',
59
- slugs: { gemini: 'google', anthropic: 'anthropic', openai: 'openai', xai: 'x-ai' },
59
+ slugs: { gemini: 'google', anthropic: 'anthropic', openai: 'openai', xai: 'x-ai', moonshot: 'moonshotai' },
60
60
  },
61
61
  };
62
62
  /**
@@ -119,7 +119,7 @@ function modelMapFromEnv() {
119
119
  * Gateways prefix every id with the upstream provider's slug and spell
120
120
  * Anthropic's version segment with a dot ("claude-sonnet-4-6" is
121
121
  * "anthropic/claude-sonnet-4.6" on both Vercel and OpenRouter). Gemini,
122
- * GPT and Grok ids already carry dots, so only the prefix is added. An id
122
+ * GPT, Grok and Kimi ids already carry dots, so only the prefix is added. An id
123
123
  * the mapper gets wrong is fixed in MODEL_GATEWAY_MODEL_MAP, not in code.
124
124
  */
125
125
  export function gatewayWireModel(model, gateway) {
@@ -0,0 +1,123 @@
1
+ /**
2
+ * lib/models/kimiLlm.ts — Moonshot AI (Kimi) provider for the ADK LLMRegistry.
3
+ *
4
+ * WHY this file exists:
5
+ * Model optionality is a primary driver of this framework: the agent YAML
6
+ * declares `model`, and the registry routes it to the right provider.
7
+ * Kimi K3 (July 2026) is the strongest open-weight model on the public
8
+ * coding and research boards, and Moonshot serves it first-party with
9
+ * tool calling, strict structured output and vision, so it earns a
10
+ * direct adapter rather than only the gateway path.
11
+ *
12
+ * WHY it subclasses the chat-completions base (lib/models/openAiCompatibleLlm.ts):
13
+ * Moonshot's API is OpenAI Chat Completions at https://api.moonshot.ai/v1
14
+ * (it also serves Responses on the same base and Anthropic Messages at
15
+ * /anthropic; neither adds a capability the chat dialect lacks here).
16
+ * The base provides tool calling, strict `json_schema` structured output
17
+ * (Moonshot documents `strict: true`), SSE streaming with usage, image
18
+ * parts as base64 data URIs (Moonshot rejects public image URLs, which is
19
+ * what the base never sends), `reasoning_content` surfaced as THINKING
20
+ * output, retries, and the llm.request span.
21
+ *
22
+ * HOW TO ENABLE:
23
+ * 1. Create a key at https://platform.moonshot.ai (international; the
24
+ * .cn console serves mainland China) and put it in .env as
25
+ * MOONSHOT_API_KEY.
26
+ * 2. Set model: "kimi-k3" (or any kimi-* id) in your YAML.
27
+ * registerAvailableProviders() registers this adapter when the key is set.
28
+ * Optional: MOONSHOT_BASE_URL for a proxy that speaks the same dialect.
29
+ *
30
+ * THE FAMILY (platform.moonshot.ai › Model list, October 2026; USD per 1M tokens):
31
+ *
32
+ * model context in / cache hit / out use it for
33
+ * ──────────────────────────────────────────────────────────────────────
34
+ * kimi-k3 1M $3.00 / $0.30 / $15 DEFAULT — the flagship: 2.8T MoE, vision,
35
+ * always-on thinking, effort low|high|max
36
+ * kimi-k2.6 256K $0.95 / $0.16 / $4 the cheaper general tier: vision, thinking
37
+ * switchable; fragile on complex schemas
38
+ * kimi-k2.7-code 256K $0.95 / $0.19 / $4 coding; thinking cannot be turned off
39
+ * kimi-k2.7-code-highspeed 256K $1.90 / $0.38 / $8 the same model at ~180 tokens/s
40
+ *
41
+ * CHOOSING A MODEL (cost is a design property; see the model-routing skill):
42
+ * K3 is priced like a mid-tier closed model, not like an open-weight
43
+ * bargain: $3 / $15 is Claude Sonnet 4.6's price, and Claude Sonnet 5.5
44
+ * ($2 / $10, cache reads $0.20) undercuts it on every token while sitting
45
+ * above it on the coding boards. Two things make K3 dearer than its list
46
+ * price: thinking is always on and billed as output, and Moonshot's
47
+ * default effort is max — hence the pinned DEFAULT_KIMI_REASONING_EFFORT
48
+ * and the zoo agent at "low". What K3 buys is the strongest open-weight
49
+ * model (a self-host option) and a 1M context at a flat rate. For cost,
50
+ * reach for kimi-k2.6 (a quarter of K3's price, vision and tools) on
51
+ * subagent and bulk work, or serve K3 through the gateway: OpenRouter's
52
+ * endpoints range from about $0.88 to $3.45 per 1M input, the cheap ones
53
+ * quantized, with fidelity as the trade.
54
+ *
55
+ * There is no small or "flash" K3; K2.6 is the light tier. K3's weights
56
+ * are open (moonshotai/Kimi-K3 on Hugging Face), so the id is also served
57
+ * by OpenRouter, Together, Fireworks, Vercel AI Gateway and others; with
58
+ * no direct key, MODEL_GATEWAY serves it as `moonshotai/kimi-k3`
59
+ * (lib/models/gateway.ts). kimi-k2.5 and the moonshot-v1 ids were retired
60
+ * on 2026-08-31 and are not routed here on purpose.
61
+ *
62
+ * REASONING:
63
+ * K3 always thinks; the lever is the top-level `reasoning_effort`
64
+ * (low | high | max, Moonshot's default max). Requests pin
65
+ * DEFAULT_KIMI_REASONING_EFFORT (lib/config.ts) unless the agent sets
66
+ * generateContentConfig.reasoningEffort; "none" is mapped to "low" on
67
+ * K3, which cannot switch thinking off. K2.x models take no
68
+ * reasoning_effort: a `thinking: { type }` switch instead, so on those
69
+ * ids reasoningEffort "none" or thinkingBudget 0 sends `disabled` and
70
+ * any effort value is dropped from the body (K2.7 Code rejects
71
+ * disabled thinking; Moonshot's own error says so).
72
+ *
73
+ * LIMITATIONS:
74
+ * - web_search: Moonshot's model-side `$web_search` built-in retires on
75
+ * 2026-10-20 and its replacement is a separate REST API
76
+ * (POST /v1/tools/search, billed per call), not a tool the model
77
+ * enables in the request. The sentinel is therefore dropped with a
78
+ * warning, like on Ollama; `web_extract` works, and `MOONSHOT_API_KEY`
79
+ * would fund a client-side search tool over that REST API if one is
80
+ * added (lib/tools/).
81
+ * - Thinking with tools: Moonshot asks that K3's `reasoning_content` be
82
+ * passed back with the assistant message on a tool loop. This base
83
+ * keeps scratchpads out of history, so K3 re-reasons each step
84
+ * (degraded, stated in lib/models/capabilities.ts); if Moonshot ever
85
+ * enforces it, the turn ends with MOONSHOT_HTTP_ERROR naming the field.
86
+ * - Every cell of its capability row is asserted against the request body
87
+ * the adapter sends (tests/capabilityMatrix.test.ts), as for every other
88
+ * provider. Verified live on 2026-10-03 through `npm run demo:models`:
89
+ * kimi-k3 at low effort answered the zoo's explainer prompt in 17.9 s,
90
+ * 440 tokens in, 486 out, 262 of them thinking, the scratchpad surfaced
91
+ * from reasoning_content; tool loops and search were not exercised.
92
+ */
93
+ import type { LlmRequest, LlmResponse } from '@google/adk';
94
+ import { OpenAiCompatibleLlm } from './openAiCompatibleLlm.ts';
95
+ export declare const MOONSHOT_BASE_URL = "https://api.moonshot.ai/v1";
96
+ /** The flagship takes `reasoning_effort`; the K2 generation takes a `thinking` switch. */
97
+ export declare function isKimiK3(model: string): boolean;
98
+ export declare class KimiLlm extends OpenAiCompatibleLlm {
99
+ /** Any model: "kimi-*" in a YAML config routes here after registration. */
100
+ static readonly supportedModels: Array<string | RegExp>;
101
+ private readonly apiKey?;
102
+ private readonly baseUrl;
103
+ constructor({ model, apiKey, baseUrl }: {
104
+ model: string;
105
+ apiKey?: string;
106
+ baseUrl?: string;
107
+ });
108
+ protected providerId(): string;
109
+ protected endpointUrl(): string;
110
+ private key;
111
+ protected headers(): Record<string, string>;
112
+ protected missingRequirement(): LlmResponse | undefined;
113
+ protected httpError(status: number, detail: string): LlmResponse;
114
+ /**
115
+ * The reasoning controls, per generation (see the header). The base has
116
+ * already put `reasoning_effort` in the body when the agent set one;
117
+ * these fields are merged after it, so an `undefined` here removes it
118
+ * (JSON.stringify drops undefined keys).
119
+ */
120
+ protected extraBodyFields(llmRequest: LlmRequest): Record<string, unknown>;
121
+ }
122
+ /** Registers KimiLlm with the ADK LLMRegistry (called when MOONSHOT_API_KEY is set). */
123
+ export declare function registerKimiLlm(): void;
@@ -0,0 +1,171 @@
1
+ /**
2
+ * lib/models/kimiLlm.ts — Moonshot AI (Kimi) provider for the ADK LLMRegistry.
3
+ *
4
+ * WHY this file exists:
5
+ * Model optionality is a primary driver of this framework: the agent YAML
6
+ * declares `model`, and the registry routes it to the right provider.
7
+ * Kimi K3 (July 2026) is the strongest open-weight model on the public
8
+ * coding and research boards, and Moonshot serves it first-party with
9
+ * tool calling, strict structured output and vision, so it earns a
10
+ * direct adapter rather than only the gateway path.
11
+ *
12
+ * WHY it subclasses the chat-completions base (lib/models/openAiCompatibleLlm.ts):
13
+ * Moonshot's API is OpenAI Chat Completions at https://api.moonshot.ai/v1
14
+ * (it also serves Responses on the same base and Anthropic Messages at
15
+ * /anthropic; neither adds a capability the chat dialect lacks here).
16
+ * The base provides tool calling, strict `json_schema` structured output
17
+ * (Moonshot documents `strict: true`), SSE streaming with usage, image
18
+ * parts as base64 data URIs (Moonshot rejects public image URLs, which is
19
+ * what the base never sends), `reasoning_content` surfaced as THINKING
20
+ * output, retries, and the llm.request span.
21
+ *
22
+ * HOW TO ENABLE:
23
+ * 1. Create a key at https://platform.moonshot.ai (international; the
24
+ * .cn console serves mainland China) and put it in .env as
25
+ * MOONSHOT_API_KEY.
26
+ * 2. Set model: "kimi-k3" (or any kimi-* id) in your YAML.
27
+ * registerAvailableProviders() registers this adapter when the key is set.
28
+ * Optional: MOONSHOT_BASE_URL for a proxy that speaks the same dialect.
29
+ *
30
+ * THE FAMILY (platform.moonshot.ai › Model list, October 2026; USD per 1M tokens):
31
+ *
32
+ * model context in / cache hit / out use it for
33
+ * ──────────────────────────────────────────────────────────────────────
34
+ * kimi-k3 1M $3.00 / $0.30 / $15 DEFAULT — the flagship: 2.8T MoE, vision,
35
+ * always-on thinking, effort low|high|max
36
+ * kimi-k2.6 256K $0.95 / $0.16 / $4 the cheaper general tier: vision, thinking
37
+ * switchable; fragile on complex schemas
38
+ * kimi-k2.7-code 256K $0.95 / $0.19 / $4 coding; thinking cannot be turned off
39
+ * kimi-k2.7-code-highspeed 256K $1.90 / $0.38 / $8 the same model at ~180 tokens/s
40
+ *
41
+ * CHOOSING A MODEL (cost is a design property; see the model-routing skill):
42
+ * K3 is priced like a mid-tier closed model, not like an open-weight
43
+ * bargain: $3 / $15 is Claude Sonnet 4.6's price, and Claude Sonnet 5.5
44
+ * ($2 / $10, cache reads $0.20) undercuts it on every token while sitting
45
+ * above it on the coding boards. Two things make K3 dearer than its list
46
+ * price: thinking is always on and billed as output, and Moonshot's
47
+ * default effort is max — hence the pinned DEFAULT_KIMI_REASONING_EFFORT
48
+ * and the zoo agent at "low". What K3 buys is the strongest open-weight
49
+ * model (a self-host option) and a 1M context at a flat rate. For cost,
50
+ * reach for kimi-k2.6 (a quarter of K3's price, vision and tools) on
51
+ * subagent and bulk work, or serve K3 through the gateway: OpenRouter's
52
+ * endpoints range from about $0.88 to $3.45 per 1M input, the cheap ones
53
+ * quantized, with fidelity as the trade.
54
+ *
55
+ * There is no small or "flash" K3; K2.6 is the light tier. K3's weights
56
+ * are open (moonshotai/Kimi-K3 on Hugging Face), so the id is also served
57
+ * by OpenRouter, Together, Fireworks, Vercel AI Gateway and others; with
58
+ * no direct key, MODEL_GATEWAY serves it as `moonshotai/kimi-k3`
59
+ * (lib/models/gateway.ts). kimi-k2.5 and the moonshot-v1 ids were retired
60
+ * on 2026-08-31 and are not routed here on purpose.
61
+ *
62
+ * REASONING:
63
+ * K3 always thinks; the lever is the top-level `reasoning_effort`
64
+ * (low | high | max, Moonshot's default max). Requests pin
65
+ * DEFAULT_KIMI_REASONING_EFFORT (lib/config.ts) unless the agent sets
66
+ * generateContentConfig.reasoningEffort; "none" is mapped to "low" on
67
+ * K3, which cannot switch thinking off. K2.x models take no
68
+ * reasoning_effort: a `thinking: { type }` switch instead, so on those
69
+ * ids reasoningEffort "none" or thinkingBudget 0 sends `disabled` and
70
+ * any effort value is dropped from the body (K2.7 Code rejects
71
+ * disabled thinking; Moonshot's own error says so).
72
+ *
73
+ * LIMITATIONS:
74
+ * - web_search: Moonshot's model-side `$web_search` built-in retires on
75
+ * 2026-10-20 and its replacement is a separate REST API
76
+ * (POST /v1/tools/search, billed per call), not a tool the model
77
+ * enables in the request. The sentinel is therefore dropped with a
78
+ * warning, like on Ollama; `web_extract` works, and `MOONSHOT_API_KEY`
79
+ * would fund a client-side search tool over that REST API if one is
80
+ * added (lib/tools/).
81
+ * - Thinking with tools: Moonshot asks that K3's `reasoning_content` be
82
+ * passed back with the assistant message on a tool loop. This base
83
+ * keeps scratchpads out of history, so K3 re-reasons each step
84
+ * (degraded, stated in lib/models/capabilities.ts); if Moonshot ever
85
+ * enforces it, the turn ends with MOONSHOT_HTTP_ERROR naming the field.
86
+ * - Every cell of its capability row is asserted against the request body
87
+ * the adapter sends (tests/capabilityMatrix.test.ts), as for every other
88
+ * provider. Verified live on 2026-10-03 through `npm run demo:models`:
89
+ * kimi-k3 at low effort answered the zoo's explainer prompt in 17.9 s,
90
+ * 440 tokens in, 486 out, 262 of them thinking, the scratchpad surfaced
91
+ * from reasoning_content; tool loops and search were not exercised.
92
+ */
93
+ import { LLMRegistry } from '@google/adk';
94
+ import { DEFAULT_KIMI_REASONING_EFFORT } from '../config.js';
95
+ import { OpenAiCompatibleLlm } from './openAiCompatibleLlm.js';
96
+ export const MOONSHOT_BASE_URL = 'https://api.moonshot.ai/v1';
97
+ /** The flagship takes `reasoning_effort`; the K2 generation takes a `thinking` switch. */
98
+ export function isKimiK3(model) {
99
+ return /^kimi-k3\b/.test(model);
100
+ }
101
+ // ── KimiLlm ───────────────────────────────────────────────────────────────────
102
+ export class KimiLlm extends OpenAiCompatibleLlm {
103
+ /** Any model: "kimi-*" in a YAML config routes here after registration. */
104
+ static supportedModels = [/^kimi-.+/];
105
+ apiKey;
106
+ baseUrl;
107
+ constructor({ model, apiKey, baseUrl }) {
108
+ super({ model });
109
+ this.apiKey = apiKey;
110
+ this.baseUrl = (baseUrl || process.env.MOONSHOT_BASE_URL || MOONSHOT_BASE_URL).replace(/\/+$/, '');
111
+ }
112
+ providerId() {
113
+ return 'moonshot';
114
+ }
115
+ endpointUrl() {
116
+ return `${this.baseUrl}/chat/completions`;
117
+ }
118
+ key() {
119
+ return this.apiKey ?? process.env.MOONSHOT_API_KEY;
120
+ }
121
+ headers() {
122
+ return { Authorization: `Bearer ${this.key() ?? ''}` };
123
+ }
124
+ missingRequirement() {
125
+ if (this.key())
126
+ return undefined;
127
+ return {
128
+ errorCode: 'MOONSHOT_MISSING_KEY',
129
+ errorMessage: 'MOONSHOT_API_KEY is not set in environment.',
130
+ };
131
+ }
132
+ httpError(status, detail) {
133
+ const hint = status === 401
134
+ ? ' Check MOONSHOT_API_KEY (platform.moonshot.ai).'
135
+ : status === 404
136
+ ? ` Is "${this.model}" a current Kimi id? kimi-k2.5 and moonshot-v1-* were retired on 2026-08-31.`
137
+ : '';
138
+ return {
139
+ errorCode: 'MOONSHOT_HTTP_ERROR',
140
+ errorMessage: `Moonshot returned ${status}: ${detail.slice(0, 4000)}.${hint}`,
141
+ };
142
+ }
143
+ /**
144
+ * The reasoning controls, per generation (see the header). The base has
145
+ * already put `reasoning_effort` in the body when the agent set one;
146
+ * these fields are merged after it, so an `undefined` here removes it
147
+ * (JSON.stringify drops undefined keys).
148
+ */
149
+ extraBodyFields(llmRequest) {
150
+ const cfg = llmRequest.config ?? {};
151
+ const effort = cfg.reasoningEffort;
152
+ if (isKimiK3(this.model)) {
153
+ if (effort === undefined)
154
+ return { reasoning_effort: DEFAULT_KIMI_REASONING_EFFORT };
155
+ if (effort === 'none')
156
+ return { reasoning_effort: 'low' }; // K3 cannot switch thinking off
157
+ return {};
158
+ }
159
+ const budget = cfg.thinkingConfig?.thinkingBudget;
160
+ const off = effort === 'none' || budget === 0;
161
+ return {
162
+ reasoning_effort: undefined,
163
+ ...(off ? { thinking: { type: 'disabled' } } : {}),
164
+ };
165
+ }
166
+ }
167
+ // ── Registration helper ───────────────────────────────────────────────────────
168
+ /** Registers KimiLlm with the ADK LLMRegistry (called when MOONSHOT_API_KEY is set). */
169
+ export function registerKimiLlm() {
170
+ LLMRegistry.register(KimiLlm);
171
+ }
@@ -10,9 +10,9 @@
10
10
  * Model-name conventions (see DOCUMENTATION.md §7):
11
11
  * claude-* → anthropic gpt-* / o<digit>* → openai
12
12
  * grok-* → xai ollama/<model> → ollama (local, keyless)
13
- * everything else → gemini (the ADK-native default)
13
+ * kimi-* → moonshot everything else → gemini (the ADK-native default)
14
14
  */
15
- export type ProviderId = 'gemini' | 'anthropic' | 'openai' | 'xai' | 'ollama';
15
+ export type ProviderId = 'gemini' | 'anthropic' | 'openai' | 'xai' | 'moonshot' | 'ollama';
16
16
  interface ProviderInfo {
17
17
  /** Env var holding the API key; null = keyless (local). */
18
18
  keyEnv: string | null;
@@ -10,13 +10,14 @@
10
10
  * Model-name conventions (see DOCUMENTATION.md §7):
11
11
  * claude-* → anthropic gpt-* / o<digit>* → openai
12
12
  * grok-* → xai ollama/<model> → ollama (local, keyless)
13
- * everything else → gemini (the ADK-native default)
13
+ * kimi-* → moonshot everything else → gemini (the ADK-native default)
14
14
  */
15
15
  export const PROVIDERS = {
16
16
  gemini: { keyEnv: 'GOOGLE_GENAI_API_KEY', label: 'Google Gemini' },
17
17
  anthropic: { keyEnv: 'ANTHROPIC_API_KEY', label: 'Anthropic Claude' },
18
18
  openai: { keyEnv: 'OPENAI_API_KEY', label: 'OpenAI GPT' },
19
19
  xai: { keyEnv: 'XAI_API_KEY', label: 'xAI Grok' },
20
+ moonshot: { keyEnv: 'MOONSHOT_API_KEY', label: 'Moonshot Kimi' },
20
21
  ollama: { keyEnv: null, label: 'Ollama (local)' },
21
22
  };
22
23
  /** Maps a model id to its provider. Unknown ids default to gemini — the
@@ -30,6 +31,8 @@ export function providerForModel(model) {
30
31
  return 'openai';
31
32
  if (/^grok-/.test(model))
32
33
  return 'xai';
34
+ if (/^kimi-/.test(model))
35
+ return 'moonshot';
33
36
  return 'gemini';
34
37
  }
35
38
  /** True when the provider's key is present (always true for local Ollama). */
@@ -11,7 +11,7 @@
11
11
  * registry then string-matches supportedModels patterns:
12
12
  * claude-* → ClaudeLlm gpt-* / o<digit>* → GptLlm
13
13
  * grok-* → GrokLlm ollama/<model> → OllamaLlm
14
- * gemini-* → TracedGemini (ADK's Gemini + llm.request spans)
14
+ * kimi-* → KimiLlm gemini-* → TracedGemini (ADK's Gemini + llm.request spans)
15
15
  *
16
16
  * 2. resolveModel() — instance factory for BYOK paths (scripts/
17
17
  * a2a_server.ts), where a per-request API key from an HTTP header must
@@ -11,7 +11,7 @@
11
11
  * registry then string-matches supportedModels patterns:
12
12
  * claude-* → ClaudeLlm gpt-* / o<digit>* → GptLlm
13
13
  * grok-* → GrokLlm ollama/<model> → OllamaLlm
14
- * gemini-* → TracedGemini (ADK's Gemini + llm.request spans)
14
+ * kimi-* → KimiLlm gemini-* → TracedGemini (ADK's Gemini + llm.request spans)
15
15
  *
16
16
  * 2. resolveModel() — instance factory for BYOK paths (scripts/
17
17
  * a2a_server.ts), where a per-request API key from an HTTP header must
@@ -26,11 +26,12 @@ import { Gemini, LLMRegistry } from '@google/adk';
26
26
  import { endpointFromEnv, endpointProblems, mergeEndpoint, platformModel, providerReady } from './endpoints.js';
27
27
  import { currentTurnSignal } from '../runtime/turnControl.js';
28
28
  import { errorStatus, retryUntilFirstYield } from './retry.js';
29
- import { DEFAULT_CLAUDE_MODEL, DEFAULT_GEMINI_MODEL, DEFAULT_GPT_MODEL, DEFAULT_GROK_MODEL, DEFAULT_OLLAMA_MODEL, } from '../config.js';
29
+ import { DEFAULT_CLAUDE_MODEL, DEFAULT_GEMINI_MODEL, DEFAULT_GPT_MODEL, DEFAULT_GROK_MODEL, DEFAULT_KIMI_MODEL, DEFAULT_OLLAMA_MODEL, } from '../config.js';
30
30
  import { traceLlmGeneration, setLlmSpanAttribute, } from '../observability/tracer.js';
31
31
  import { ClaudeLlm, registerClaudeLlm } from './claudeLlm.js';
32
32
  import { GptLlm, registerGptLlm } from './gptLlm.js';
33
33
  import { GrokLlm, registerGrokLlm } from './grokLlm.js';
34
+ import { KimiLlm, registerKimiLlm } from './kimiLlm.js';
34
35
  import { OllamaLlm, registerOllamaLlm } from './ollamaLlm.js';
35
36
  import { GatewayLlm, registerGatewayLlm } from './gatewayLlm.js';
36
37
  import { gatewayConfig, gatewayProblem, gatewayUsable, planTransport, } from './gateway.js';
@@ -144,6 +145,7 @@ const REGISTRARS = {
144
145
  anthropic: registerClaudeLlm,
145
146
  openai: registerGptLlm,
146
147
  xai: registerGrokLlm,
148
+ moonshot: registerKimiLlm,
147
149
  ollama: registerOllamaLlm,
148
150
  };
149
151
  /**
@@ -159,6 +161,7 @@ const PATTERNS = {
159
161
  anthropic: ClaudeLlm.supportedModels,
160
162
  openai: GptLlm.supportedModels,
161
163
  xai: GrokLlm.supportedModels,
164
+ moonshot: KimiLlm.supportedModels,
162
165
  };
163
166
  let providersRegistered = false;
164
167
  /**
@@ -209,6 +212,8 @@ function modelHint(provider) {
209
212
  return 'gpt-*/o*';
210
213
  case 'xai':
211
214
  return 'grok-*';
215
+ case 'moonshot':
216
+ return 'kimi-*';
212
217
  case 'ollama':
213
218
  return 'ollama/*';
214
219
  case 'gemini':
@@ -220,6 +225,7 @@ const DEFAULT_MODEL_FOR = {
220
225
  anthropic: DEFAULT_CLAUDE_MODEL,
221
226
  openai: DEFAULT_GPT_MODEL,
222
227
  xai: DEFAULT_GROK_MODEL,
228
+ moonshot: DEFAULT_KIMI_MODEL,
223
229
  ollama: DEFAULT_OLLAMA_MODEL,
224
230
  };
225
231
  /**
@@ -257,13 +263,15 @@ export function resolveModel(model, options = {}) {
257
263
  return new GptLlm({ model: resolved, apiKey, endpoint });
258
264
  case 'xai':
259
265
  return new GrokLlm({ model: resolved, apiKey });
266
+ case 'moonshot':
267
+ return new KimiLlm({ model: resolved, apiKey, baseUrl: endpoint?.baseURL });
260
268
  case 'gemini':
261
269
  return new TracedGemini({ model: resolved, apiKey, endpoint });
262
270
  }
263
271
  }
264
272
  function normalizeProvider(provider) {
265
273
  const p = provider?.toLowerCase();
266
- if (p === 'ollama' || p === 'anthropic' || p === 'openai' || p === 'xai') {
274
+ if (p === 'ollama' || p === 'anthropic' || p === 'openai' || p === 'xai' || p === 'moonshot') {
267
275
  return p;
268
276
  }
269
277
  return 'gemini';
@@ -0,0 +1,14 @@
1
+ export declare const MAX_REDIRECTS = 5;
2
+ export interface RedirectPolicy {
3
+ /** Why the request may not follow a redirect to `url`, or null to follow it. */
4
+ hopProblem(url: URL, crossOrigin: boolean): Promise<string | null>;
5
+ }
6
+ type Fetch = typeof globalThis.fetch;
7
+ /**
8
+ * `fetch(input, init)` with redirects followed manually under `policy`.
9
+ * Throws when a hop is refused or the chain is too long.
10
+ */
11
+ export declare function fetchWithRedirectPolicy(input: string | URL | Request, init: RequestInit | undefined, policy: RedirectPolicy, fetchImpl: Fetch): Promise<Response>;
12
+ /** Run `fn` with every globalThis.fetch it makes following redirects under `policy`. */
13
+ export declare function withRedirectGuard<T>(policy: RedirectPolicy, fn: () => Promise<T>): Promise<T>;
14
+ export {};
@@ -0,0 +1,94 @@
1
+ /**
2
+ * lib/net/redirects.ts — follow HTTP redirects one hop at a time, each hop
3
+ * held to a policy, for code that calls `globalThis.fetch` itself.
4
+ *
5
+ * WHY: fetch follows redirects by default, so a guard that vets only the URL
6
+ * a request starts at is bypassed by any server that answers 302: an allowed
7
+ * public API with an open redirect can send a call to 169.254.169.254, and
8
+ * fetch forwards every header but Authorization and Cookie (an API key in a
9
+ * custom header crosses origins). web_extract follows redirects manually for
10
+ * this reason; ADK's RestApiTool (the OpenAPI tools) calls globalThis.fetch
11
+ * with no hook to pass a fetch of our own.
12
+ *
13
+ * HOW: `withRedirectGuard(policy, fn)` runs `fn` in an AsyncLocalStorage
14
+ * context. A wrapper installed on globalThis.fetch (once, and again if
15
+ * something replaced it) applies the guard only inside such a context and
16
+ * calls the original fetch untouched everywhere else, so no other request in
17
+ * the process changes behaviour and concurrent calls never see each other's
18
+ * policy. Inside the context every request uses `redirect: 'manual'`; each
19
+ * hop's URL goes to `policy.hopProblem` before it is fetched, a hop to
20
+ * another origin keeps only content-negotiation headers, and a chain longer
21
+ * than MAX_REDIRECTS is an error. 303, and 301/302 after a POST, continue as
22
+ * a GET without a body, as browsers do.
23
+ */
24
+ import { AsyncLocalStorage } from 'node:async_hooks';
25
+ export const MAX_REDIRECTS = 5;
26
+ /** The headers a request keeps when a redirect moves it to another origin. */
27
+ const CROSS_ORIGIN_HEADERS = new Set(['accept', 'accept-language', 'content-language', 'content-type', 'user-agent']);
28
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
29
+ /**
30
+ * `fetch(input, init)` with redirects followed manually under `policy`.
31
+ * Throws when a hop is refused or the chain is too long.
32
+ */
33
+ export async function fetchWithRedirectPolicy(input, init, policy, fetchImpl) {
34
+ let url = new URL(input instanceof Request ? input.url : String(input));
35
+ let method = (init?.method ?? (input instanceof Request ? input.method : 'GET')).toUpperCase();
36
+ let headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
37
+ let body = init?.body;
38
+ for (let hop = 0;; hop++) {
39
+ const res = await fetchImpl(url, { ...init, method, headers, body, redirect: 'manual' });
40
+ const location = res.headers.get('location');
41
+ if (!REDIRECT_STATUSES.has(res.status) || !location)
42
+ return res;
43
+ await res.body?.cancel().catch(() => { });
44
+ if (hop >= MAX_REDIRECTS)
45
+ throw new Error(`more than ${MAX_REDIRECTS} redirects from ${url.host}`);
46
+ let next;
47
+ try {
48
+ next = new URL(location, url);
49
+ }
50
+ catch {
51
+ throw new Error(`redirect to an unparseable URL from ${url.host}`);
52
+ }
53
+ const crossOrigin = next.origin !== url.origin;
54
+ const problem = await policy.hopProblem(next, crossOrigin);
55
+ if (problem)
56
+ throw new Error(`redirect refused: ${problem}`);
57
+ if (res.status === 303 || ((res.status === 301 || res.status === 302) && method === 'POST')) {
58
+ if (method !== 'HEAD')
59
+ method = 'GET';
60
+ body = undefined;
61
+ headers.delete('content-type');
62
+ headers.delete('content-length');
63
+ }
64
+ if (crossOrigin) {
65
+ const kept = new Headers();
66
+ headers.forEach((value, name) => {
67
+ if (CROSS_ORIGIN_HEADERS.has(name.toLowerCase()))
68
+ kept.set(name, value);
69
+ });
70
+ headers = kept;
71
+ }
72
+ url = next;
73
+ }
74
+ }
75
+ const guardContext = new AsyncLocalStorage();
76
+ const WRAPPED = Symbol.for('melchizedek.redirectGuardedFetch');
77
+ /** Wrap whatever globalThis.fetch is now, unless it is already our wrapper. */
78
+ function ensureInstalled() {
79
+ const current = globalThis.fetch;
80
+ if (current[WRAPPED])
81
+ return;
82
+ const inner = current;
83
+ const wrapped = ((input, init) => {
84
+ const policy = guardContext.getStore();
85
+ return policy ? fetchWithRedirectPolicy(input, init, policy, inner) : inner(input, init);
86
+ });
87
+ wrapped[WRAPPED] = true;
88
+ globalThis.fetch = wrapped;
89
+ }
90
+ /** Run `fn` with every globalThis.fetch it makes following redirects under `policy`. */
91
+ export function withRedirectGuard(policy, fn) {
92
+ ensureInstalled();
93
+ return guardContext.run(policy, fn);
94
+ }