@mindot/will 0.4.0 → 0.5.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.
package/src/llm/index.ts CHANGED
@@ -12,7 +12,68 @@ import type { LLMCompletionRecord } from '#core/completion.recorder'
12
12
  import { withGate } from '#llm/gate'
13
13
  import { matchConversationFocus, wrapReplyText } from '#llm/wire.contracts'
14
14
 
15
- export type LLMProvider = 'anthropic' | 'deepseek' | 'openai' | 'google'
15
+ export type LLMProvider = 'anthropic' | 'glm' | 'deepseek' | 'openai' | 'google'
16
+
17
+ /**
18
+ * Providers that speak the Anthropic Messages wire.
19
+ *
20
+ * Z.ai ships a real Anthropic-compatible endpoint for GLM — it is what Claude
21
+ * Code itself targets — so GLM rides this path rather than the OpenAI scaffold.
22
+ * That buys it everything the path already has: token streaming, the first-byte
23
+ * deadline, prompt-cache breakpoints, and the structured-output contract. GLM is
24
+ * therefore a second *production* provider, not a fifth scaffold.
25
+ */
26
+ const ANTHROPIC_WIRE = new Set<LLMProvider>( [ 'anthropic', 'glm' ] )
27
+
28
+ /** Does this provider accept Anthropic-shaped requests? */
29
+ export function speaksAnthropicWire( provider: LLMProvider ): boolean {
30
+ return ANTHROPIC_WIRE.has( provider )
31
+ }
32
+
33
+ /** Official API base URL (including version segment) for a provider. */
34
+ export function defaultBaseFor( provider: LLMProvider ): string {
35
+ switch( provider ){
36
+ case 'anthropic': return 'https://api.anthropic.com/v1'
37
+ // Z.ai documents the base as `…/api/anthropic` because the Anthropic SDK
38
+ // appends `/v1/messages`; this client appends `/messages`, so the version
39
+ // segment belongs here — verified against the live endpoint.
40
+ case 'glm': return 'https://api.z.ai/api/anthropic/v1'
41
+ case 'openai': return 'https://api.openai.com/v1'
42
+ case 'deepseek': return 'https://api.deepseek.com/v1'
43
+ case 'google': return 'https://generativelanguage.googleapis.com/v1beta'
44
+ }
45
+ }
46
+
47
+ /**
48
+ * The model the executive recruits when none is pinned.
49
+ *
50
+ * Provider-specific because the default is *sent* — a GLM Will with no
51
+ * `WILL_LLM_MODEL` would otherwise ask Z.ai for a Claude id and get a 404 it
52
+ * could do nothing with. The scaffolded providers (openai/deepseek/google) keep
53
+ * today's value: they need an explicit `WILL_LLM_MODEL` to work at all, and
54
+ * inventing ids for them here would look like support that does not exist.
55
+ */
56
+ export function defaultModelFor( provider: LLMProvider ): string {
57
+ return provider === 'glm' ? 'glm-5.2' : 'claude-sonnet-4-5-20250929'
58
+ }
59
+
60
+ /**
61
+ * Auth + version headers for the Anthropic wire.
62
+ *
63
+ * Anthropic authenticates with `x-api-key`. Z.ai's compat endpoint accepts
64
+ * either that or the `Authorization: Bearer` its own docs describe (both were
65
+ * probed against the live endpoint; each is read and validated). GLM sends both
66
+ * — same secret, same host — so the mind keeps working whichever one Z.ai
67
+ * eventually settles on.
68
+ */
69
+ export function anthropicWireHeaders( provider: LLMProvider, apiKey: string ): Record<string, string> {
70
+ return {
71
+ 'Content-Type': 'application/json',
72
+ 'anthropic-version': '2023-06-01',
73
+ 'x-api-key': apiKey,
74
+ ...( provider === 'glm' ? { Authorization: `Bearer ${ apiKey }` } : {} ),
75
+ }
76
+ }
16
77
  export interface LLMDirectorConfig {
17
78
  willId: string
18
79
  model: string
@@ -216,7 +277,7 @@ export class LLMDirector {
216
277
  return result
217
278
  }
218
279
 
219
- const result = this._provider === 'anthropic'
280
+ const result = speaksAnthropicWire( this._provider )
220
281
  ? await this._callAnthropicStream( systemPrompt, userMessage, onChunk, temperature )
221
282
  : await ( async () => {
222
283
  // Other providers: fall back to regular call, emit whole response as one chunk
@@ -328,11 +389,7 @@ export class LLMDirector {
328
389
  try {
329
390
  res = await fetch(`${this._resolvedBase()}/messages`, {
330
391
  method: 'POST',
331
- headers: {
332
- 'Content-Type': 'application/json',
333
- 'anthropic-version': '2023-06-01',
334
- 'x-api-key': this._apiKey,
335
- },
392
+ headers: anthropicWireHeaders( this._provider, this._apiKey ),
336
393
  body: JSON.stringify({
337
394
  model: this._model,
338
395
  max_tokens: this._maxOutputTokens,
@@ -438,13 +495,14 @@ export class LLMDirector {
438
495
  return result
439
496
  }
440
497
 
441
- // Anthropic routes through the streaming path so the deadline is first-byte
442
- // (TTFT), not whole-request: a long-but-healthy executive completion (often
443
- // 20–40s on Sonnet) no longer trips the timeout mid-generation. onChunk is a
444
- // no-op here — call() returns the full accumulated text; live token chunks go
445
- // through callStream(). Other providers keep the whole-request deadline.
498
+ // The Anthropic-wire providers route through the streaming path so the
499
+ // deadline is first-byte (TTFT), not whole-request: a long-but-healthy
500
+ // executive completion (often 20–40s on Sonnet) no longer trips the timeout
501
+ // mid-generation. onChunk is a no-op here — call() returns the full
502
+ // accumulated text; live token chunks go through callStream(). Other
503
+ // providers keep the whole-request deadline.
446
504
  const result = await withGate(
447
- () => this._provider === 'anthropic'
505
+ () => speaksAnthropicWire( this._provider )
448
506
  ? this._callAnthropicStream( systemPrompt, userMessage, () => {}, temperature )
449
507
  : this._callProvider( systemPrompt, userMessage, temperature ),
450
508
  'executive/direct',
@@ -465,6 +523,7 @@ export class LLMDirector {
465
523
  ): Promise<LLMCallResult> {
466
524
  switch( this._provider ){
467
525
  case 'anthropic': return this._callAnthropic( systemPrompt, userMessage, temperature )
526
+ case 'glm': return this._callAnthropic( systemPrompt, userMessage, temperature )
468
527
  case 'deepseek': return this._callOpenAI( systemPrompt, userMessage, temperature )
469
528
  case 'openai': return this._callOpenAI( systemPrompt, userMessage, temperature )
470
529
  case 'google': return this._callGoogle( systemPrompt, userMessage, temperature )
@@ -474,12 +533,7 @@ export class LLMDirector {
474
533
 
475
534
  /** Default API base URL (including version segment) for a provider. */
476
535
  private _baseFor( provider: LLMProvider ): string {
477
- switch( provider ){
478
- case 'anthropic': return 'https://api.anthropic.com/v1'
479
- case 'openai': return 'https://api.openai.com/v1'
480
- case 'deepseek': return 'https://api.deepseek.com/v1'
481
- case 'google': return 'https://generativelanguage.googleapis.com/v1beta'
482
- }
536
+ return defaultBaseFor( provider )
483
537
  }
484
538
 
485
539
  /** Resolved API base: explicit override wins, else the provider default. */
@@ -526,16 +580,12 @@ export class LLMDirector {
526
580
 
527
581
  const res = await this._fetchWithTimeout(`${this._resolvedBase()}/messages`, {
528
582
  method: 'POST',
529
- headers: {
530
- 'Content-Type': 'application/json',
531
- 'anthropic-version': '2023-06-01',
532
- 'x-api-key': this._apiKey
533
- },
583
+ headers: anthropicWireHeaders( this._provider, this._apiKey ),
534
584
  body: JSON.stringify( body )
535
585
  })
536
586
 
537
587
  if( !res.ok )
538
- throw new Error(`Anthropic API ${res.status}: ${( await res.text() ).slice(0, 300)}`)
588
+ throw new Error(`${this._provider} API ${res.status}: ${( await res.text() ).slice(0, 300)}`)
539
589
 
540
590
  const
541
591
  data = await res.json() as {
package/src/sdk/will.ts CHANGED
@@ -171,12 +171,14 @@ export interface CreateWillOptions {
171
171
  * Unset fields fall back to WILL_LLM_* envs. apiKey stays in memory only.
172
172
  * (Named llmConfig because `llm` is the mock/anthropic MODE switch.) */
173
173
  llmConfig?: WillLLMConfig
174
- /**
175
- * LLM mode. 'mock' (default when no ANTHROPIC_API_KEY) runs a deterministic
176
- * canned executive — zero keys, zero cost. 'anthropic' calls the real model
177
- * (needs ANTHROPIC_API_KEY / WILL_LLM_* env). Omit to auto-detect.
174
+ /**
175
+ * LLM mode. 'mock' (default when no key is present) runs a deterministic
176
+ * canned executive — zero keys, zero cost. 'anthropic' calls Claude (needs
177
+ * ANTHROPIC_API_KEY / WILL_LLM_* env); 'glm' calls Z.ai's GLM over its
178
+ * Anthropic-compatible endpoint (needs ZAI_API_KEY / WILL_LLM_*). Omit to
179
+ * auto-detect from whichever key is set.
178
180
  */
179
- llm?: 'mock' | 'anthropic'
181
+ llm?: 'mock' | 'anthropic' | 'glm'
180
182
  /**
181
183
  * Abilities the Will can choose to enact. `name → handler`, or
182
184
  * `name → { handler, description?, cost?, valence?, preconditions? }` to seed
@@ -459,7 +461,14 @@ export class Will {
459
461
  // ── Internals ──────────────────────────────────────────────
460
462
 
461
463
  private _buildConfig( id: string, opts: CreateWillOptions ): WillConfig {
462
- const useMock = ( opts.llm ?? ( process.env.ANTHROPIC_API_KEY ? 'anthropic' : 'mock' ) ) === 'mock'
464
+ // Auto-detect from whichever provider key is present; explicit `llm` wins.
465
+ const mode = opts.llm
466
+ ?? ( process.env.ANTHROPIC_API_KEY ? 'anthropic' : process.env.ZAI_API_KEY ? 'glm' : 'mock' )
467
+ const useMock = mode === 'mock'
468
+ // `llm: 'glm'` selects the provider; an explicit llmConfig still overrides it.
469
+ const llmConfig = mode === 'glm'
470
+ ? { provider: 'glm' as const, ...opts.llmConfig }
471
+ : opts.llmConfig
463
472
  return {
464
473
  id, name: opts.name,
465
474
  identity: {
@@ -470,7 +479,7 @@ export class Will {
470
479
  },
471
480
  anatomy: opts.anatomy ?? 'mind',
472
481
  model: opts.model,
473
- llm: opts.llmConfig,
482
+ llm: llmConfig,
474
483
  testMode: useMock,
475
484
  persistentMemory: opts.persist ?? false,
476
485
  snapshotInterval: 100,
@@ -18,7 +18,7 @@
18
18
  // ─────────────────────────────────────────────────────────────
19
19
 
20
20
  import type { WillIdentity } from '#stem/mind'
21
- import { LLMDirector, type LLMProvider, type LLMCallMeta } from '#llm/index'
21
+ import { LLMDirector, defaultModelFor, type LLMProvider, type LLMCallMeta } from '#llm/index'
22
22
  import type { TokenTracker } from '#cognition/utilities/token.tracker'
23
23
 
24
24
  export interface CoherenceIssue {
@@ -112,12 +112,13 @@ export async function reviewIdentityCoherence(
112
112
  input: CoherenceInput,
113
113
  opts: { willId?: string; tokenTracker?: TokenTracker | null } = {},
114
114
  ): Promise<CoherenceResult> {
115
+ const provider = ( process.env.WILL_LLM_PROVIDER ?? 'anthropic' ) as LLMProvider
115
116
  const director = new LLMDirector({
116
117
  willId: opts.willId ?? 'identity-coherence',
117
- model: process.env.WILL_LLM_MODEL ?? 'claude-sonnet-4-5-20250929',
118
+ model: process.env.WILL_LLM_MODEL ?? defaultModelFor( provider ),
118
119
  maxOutputTokens: 512,
119
120
  apiKey: process.env.WILL_LLM_API_KEY ?? process.env.ANTHROPIC_API_KEY ?? '',
120
- provider: ( process.env.WILL_LLM_PROVIDER ?? 'anthropic' ) as LLMProvider,
121
+ provider,
121
122
  sessionLogger: null,
122
123
  baseUrl: process.env.WILL_LLM_BASE_URL ?? process.env.OPENAI_BASE_URL,
123
124
  // When a per-Will tracker is supplied, the creation-time review records under