@mindot/will 0.7.0 → 0.9.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/README.md +87 -22
- package/dist/channels/discord.d.ts +67 -6
- package/dist/channels/discord.js +112 -6
- package/dist/channels/discord.js.map +1 -1
- package/dist/channels/whatsapp.d.ts +1 -1
- package/dist/channels/whatsapp.js +4 -1
- package/dist/channels/whatsapp.js.map +1 -1
- package/dist/cli.js +13733 -11057
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3729 -1199
- package/dist/index.js.map +1 -1
- package/dist/mcp/effectors.d.ts +1 -1
- package/dist/{will-DAW0l-lY.d.ts → will-DbDj_TEH.d.ts} +1214 -93
- package/package.json +1 -1
- package/src/channels/discord.ts +189 -11
- package/src/channels/types.ts +90 -0
- package/src/channels/whatsapp.ts +13 -4
- package/src/cli.ts +9 -4
- package/src/cognition/agency/consequence.ts +122 -1
- package/src/cognition/agency/conversation.aim.ts +260 -0
- package/src/cognition/agency/engines/action.selector.ts +85 -3
- package/src/cognition/agency/engines/affordance.synthesizer.ts +90 -1
- package/src/cognition/agency/engines/motor.schema.executor.ts +152 -10
- package/src/cognition/agency/engines/reafference.engine.ts +129 -2
- package/src/cognition/agency/proactive.communicator.ts +19 -3
- package/src/cognition/agency/reconcile.learning.ts +16 -2
- package/src/cognition/agency/restart.ts +66 -0
- package/src/cognition/agency/schemas/repertoire.ts +12 -5
- package/src/cognition/agency/selection.scoring.ts +33 -0
- package/src/cognition/agency/types.ts +35 -0
- package/src/cognition/cache/composition.ts +232 -0
- package/src/cognition/cache/deliberation.cache.ts +219 -0
- package/src/cognition/cache/fingerprint.ts +120 -0
- package/src/cognition/cache/types.ts +105 -0
- package/src/cognition/config.mirror.entities.ts +109 -1
- package/src/cognition/event.schemas.ts +22 -0
- package/src/cognition/faculties/autobiographical.narrator.ts +5 -10
- package/src/cognition/faculties/episodic.consolidator.ts +59 -3
- package/src/cognition/faculties/executive.engine/commands.ts +189 -14
- package/src/cognition/faculties/executive.engine/context.ts +67 -13
- package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +1 -1
- package/src/cognition/faculties/executive.engine/engine.ts +686 -187
- package/src/cognition/faculties/executive.engine/escalation.buffer.ts +162 -44
- package/src/cognition/faculties/executive.engine/facet.supervisor.ts +310 -65
- package/src/cognition/faculties/executive.engine/facet.ts +89 -26
- package/src/cognition/faculties/executive.engine/gating.ts +14 -14
- package/src/cognition/faculties/executive.engine/parser.ts +21 -1
- package/src/cognition/faculties/executive.engine/prompt.factory.ts +169 -20
- package/src/cognition/faculties/executive.engine/types.ts +69 -0
- package/src/cognition/faculties/goal.manager.ts +94 -14
- package/src/cognition/faculties/known.entity.tracker.ts +267 -28
- package/src/cognition/faculties/moral.evaluator.ts +8 -3
- package/src/cognition/faculties/persona.consolidator.ts +141 -0
- package/src/cognition/faculties/reputation.tracker.ts +66 -2
- package/src/cognition/faculties/self.model.updater.ts +19 -12
- package/src/cognition/faculties/social.perception.ts +47 -3
- package/src/cognition/faculties/threat.evaluator.ts +7 -0
- package/src/cognition/faculties/working.memory.ts +10 -20
- package/src/cognition/identity.entity.ts +205 -0
- package/src/cognition/index.ts +11 -0
- package/src/cognition/memory/vector.adapter.ts +12 -3
- package/src/cognition/memory/vector.embedder.ts +54 -7
- package/src/cognition/persona.prior.ts +6 -0
- package/src/cognition/senses/audition.engine/engine.ts +404 -46
- package/src/cognition/senses/base.sense.engine.ts +1 -1
- package/src/cognition/senses/index.ts +12 -0
- package/src/cognition/social.identity.ts +273 -0
- package/src/cognition/utilities/token.tracker.ts +246 -98
- package/src/core/orchestrator.ts +38 -0
- package/src/host/boot.ts +78 -22
- package/src/index.ts +35 -0
- package/src/llm/index.ts +415 -97
- package/src/llm/routing.ts +204 -0
- package/src/llm/summarizer.ts +5 -1
- package/src/llm/wire.contracts.ts +19 -0
- package/src/pma/index.ts +67 -53
- package/src/runners/thin-shim.runner.ts +18 -6
- package/src/sdk/will.ts +121 -22
- package/src/stem/assembly.audit.ts +1 -0
- package/src/stem/guards/identity.coherence.ts +17 -6
- package/src/stem/index.ts +82 -5
- package/src/stem/mind.ts +327 -79
- package/src/stem/policy/arbiter.ts +49 -14
- package/src/stem/policy/rule.table.ts +2 -2
- package/src/stem/tracts/effector.controller.ts +56 -9
- package/src/stem/tracts/outbox.writer.ts +40 -2
- package/src/cognition/faculties/executive.engine/messages.ts +0 -102
package/src/llm/index.ts
CHANGED
|
@@ -6,72 +6,176 @@ import { logger } from '#core/logger'
|
|
|
6
6
|
import type { Tick } from '#core/types'
|
|
7
7
|
import type { SessionLogger } from '#stem/tracts/session.logger'
|
|
8
8
|
import { writeFileSync, mkdirSync } from 'node:fs'
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
TokenTracker, LLMCallCategory, LLMCallAttribute, LLMCallFunction,
|
|
11
|
+
LLMCallProcess,
|
|
12
|
+
} from '#cognition/utilities/token.tracker'
|
|
13
|
+
import { type ModelRouter, isNullRouter } from '#llm/routing'
|
|
10
14
|
import { getCompletionRecorder, getCompletionSource } from '#core/completion.recorder'
|
|
11
15
|
import type { LLMCompletionRecord } from '#core/completion.recorder'
|
|
12
16
|
import { withGate } from '#llm/gate'
|
|
13
17
|
import { matchConversationFocus, wrapReplyText } from '#llm/wire.contracts'
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
/**
|
|
20
|
+
* The request/response dialect an endpoint speaks. This — not the provider's
|
|
21
|
+
* name — is what the transport actually branches on.
|
|
22
|
+
*/
|
|
23
|
+
export type LLMWire = 'anthropic' | 'openai' | 'google'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The provider and model a mock (test-mode) Will reports.
|
|
27
|
+
*
|
|
28
|
+
* A mock Will never reaches a network, so it needs no credentials — but it
|
|
29
|
+
* still records completions, and the tape should say plainly that nothing real
|
|
30
|
+
* served them rather than borrow some vendor's name.
|
|
31
|
+
*/
|
|
32
|
+
export const MOCK_PROVIDER = 'mock'
|
|
33
|
+
export const MOCK_MODEL = 'mock'
|
|
34
|
+
|
|
35
|
+
/** Providers with built-in defaults. Any other string is equally valid. */
|
|
36
|
+
export type KnownProvider =
|
|
37
|
+
| 'anthropic' // Claude
|
|
38
|
+
| 'glm' // Z.ai
|
|
39
|
+
| 'openai' // GPT + the embedding models
|
|
40
|
+
| 'google' // Gemini
|
|
41
|
+
| 'deepseek'
|
|
42
|
+
| 'moonshot' // Kimi
|
|
43
|
+
| 'qwen' // Alibaba Model Studio / DashScope
|
|
44
|
+
| 'xai' // Grok
|
|
45
|
+
| 'minimax'
|
|
46
|
+
| 'mistral'
|
|
47
|
+
| 'ollama' // local
|
|
48
|
+
| 'vllm' // local / self-hosted
|
|
16
49
|
|
|
17
50
|
/**
|
|
18
|
-
*
|
|
51
|
+
* A provider name. Deliberately open: the field of providers changes monthly,
|
|
52
|
+
* and a closed union meant a host reaching Kimi or Qwen had to masquerade as
|
|
53
|
+
* `openai`, which then lied on the completion tape and in cost attribution.
|
|
19
54
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* deadline, prompt-cache breakpoints, and the structured-output contract. GLM is
|
|
24
|
-
* therefore a second *production* provider, not a fifth scaffold.
|
|
55
|
+
* `(string & {})` keeps editor autocomplete for the known names while accepting
|
|
56
|
+
* anything. A provider outside {@link KNOWN_PROVIDERS} simply has to declare its
|
|
57
|
+
* `wire` and `baseUrl` — see `WillLLMConfig.providers`.
|
|
25
58
|
*/
|
|
26
|
-
|
|
59
|
+
export type LLMProvider = KnownProvider | ( string & {} )
|
|
27
60
|
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Built-in wire + base URL per provider. This is *data*, not support: it saves
|
|
63
|
+
* a host from looking up an endpoint, and nothing more. Any provider absent
|
|
64
|
+
* from this table works identically once the host declares `wire` + `baseUrl`
|
|
65
|
+
* on its `llm.providers` entry.
|
|
66
|
+
*
|
|
67
|
+
* WHY THIS TABLE SURVIVES WHEN THE PRICE TABLE DID NOT. A stale price is
|
|
68
|
+
* invisible: it produces a confident wrong number nobody doubts. A stale base
|
|
69
|
+
* URL fails on the first call, loudly, with the endpoint in the message. They
|
|
70
|
+
* also move on completely different clocks — vendors reprice quarterly, and
|
|
71
|
+
* change an API host about once a decade. Convenience is worth it when being
|
|
72
|
+
* wrong is self-announcing.
|
|
73
|
+
*
|
|
74
|
+
* REGIONAL ENDPOINTS. `moonshot`, `qwen` and `minimax` all run separate
|
|
75
|
+
* mainland-China hosts (`api.moonshot.cn`, `dashscope.aliyuncs.com`,
|
|
76
|
+
* `api.minimaxi.com`). The international host is the default here; a key issued
|
|
77
|
+
* on the other one authenticates nowhere, so a host on a China account must set
|
|
78
|
+
* `baseUrl` explicitly.
|
|
79
|
+
*/
|
|
80
|
+
export const KNOWN_PROVIDERS: Record<string, { wire: LLMWire; baseUrl: string }> = {
|
|
81
|
+
// Never dialled — present so a test-mode Will resolves an endpoint without
|
|
82
|
+
// demanding a provider the run will never use.
|
|
83
|
+
[ MOCK_PROVIDER ]: { wire: 'anthropic', baseUrl: 'http://mock.invalid/v1' },
|
|
84
|
+
|
|
85
|
+
// ── Anthropic wire ──────────────────────────────────────────
|
|
86
|
+
anthropic: { wire: 'anthropic', baseUrl: 'https://api.anthropic.com/v1' },
|
|
87
|
+
// Z.ai documents the base as `…/api/anthropic` because the Anthropic SDK
|
|
88
|
+
// appends `/v1/messages`; this client appends `/messages`, so the version
|
|
89
|
+
// segment belongs here — verified against the live endpoint.
|
|
90
|
+
glm: { wire: 'anthropic', baseUrl: 'https://api.z.ai/api/anthropic/v1' },
|
|
91
|
+
|
|
92
|
+
// ── OpenAI wire ─────────────────────────────────────────────
|
|
93
|
+
openai: { wire: 'openai', baseUrl: 'https://api.openai.com/v1' },
|
|
94
|
+
deepseek: { wire: 'openai', baseUrl: 'https://api.deepseek.com/v1' },
|
|
95
|
+
moonshot: { wire: 'openai', baseUrl: 'https://api.moonshot.ai/v1' },
|
|
96
|
+
qwen: { wire: 'openai', baseUrl: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1' },
|
|
97
|
+
xai: { wire: 'openai', baseUrl: 'https://api.x.ai/v1' },
|
|
98
|
+
minimax: { wire: 'openai', baseUrl: 'https://api.minimax.io/v1' },
|
|
99
|
+
mistral: { wire: 'openai', baseUrl: 'https://api.mistral.ai/v1' },
|
|
100
|
+
// Local runtimes. The port is the project default; a host that moved it sets
|
|
101
|
+
// `baseUrl`. Both still want an `apiKey` — any non-empty string will do,
|
|
102
|
+
// since neither checks it.
|
|
103
|
+
ollama: { wire: 'openai', baseUrl: 'http://localhost:11434/v1' },
|
|
104
|
+
vllm: { wire: 'openai', baseUrl: 'http://localhost:8000/v1' },
|
|
105
|
+
|
|
106
|
+
// ── Google wire ─────────────────────────────────────────────
|
|
107
|
+
// Gemini also exposes an OpenAI-compatible surface; this client speaks the
|
|
108
|
+
// native one, which is where its caching and multimodal parts actually live.
|
|
109
|
+
google: { wire: 'google', baseUrl: 'https://generativelanguage.googleapis.com/v1beta' },
|
|
31
110
|
}
|
|
32
111
|
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
112
|
+
/**
|
|
113
|
+
* The conventional env var holding each provider's key.
|
|
114
|
+
*
|
|
115
|
+
* This is *not* the fallback that W9 removed. That one read `ANTHROPIC_API_KEY`
|
|
116
|
+
* whatever provider was configured, so a Will pointed at another vendor sent it
|
|
117
|
+
* an Anthropic key. This lookup is keyed by the resolved provider: a `moonshot`
|
|
118
|
+
* Will reads `MOONSHOT_API_KEY` and nothing else, and an unknown provider gets
|
|
119
|
+
* nothing rather than someone else's secret.
|
|
120
|
+
*
|
|
121
|
+
* `WILL_LLM_API_KEY` still wins over all of it — it is the explicit statement.
|
|
122
|
+
*/
|
|
123
|
+
export const PROVIDER_KEY_ENV: Record<string, string> = {
|
|
124
|
+
anthropic: 'ANTHROPIC_API_KEY',
|
|
125
|
+
glm: 'ZAI_API_KEY',
|
|
126
|
+
openai: 'OPENAI_API_KEY',
|
|
127
|
+
google: 'GOOGLE_API_KEY',
|
|
128
|
+
deepseek: 'DEEPSEEK_API_KEY',
|
|
129
|
+
moonshot: 'MOONSHOT_API_KEY',
|
|
130
|
+
qwen: 'DASHSCOPE_API_KEY',
|
|
131
|
+
xai: 'XAI_API_KEY',
|
|
132
|
+
minimax: 'MINIMAX_API_KEY',
|
|
133
|
+
mistral: 'MISTRAL_API_KEY',
|
|
45
134
|
}
|
|
46
135
|
|
|
47
136
|
/**
|
|
48
|
-
* The
|
|
137
|
+
* The provider's own key from the environment, if it has a conventional one.
|
|
49
138
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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.
|
|
139
|
+
* An empty or blank value counts as absent. A `.env` that lists every provider
|
|
140
|
+
* and fills in one — which is what the template invites — leaves the rest as
|
|
141
|
+
* `KEY=`, and a present-but-empty key is not a key.
|
|
55
142
|
*/
|
|
56
|
-
export function
|
|
57
|
-
|
|
143
|
+
export function providerKeyFromEnv( provider: LLMProvider ): string | undefined {
|
|
144
|
+
const name = PROVIDER_KEY_ENV[ provider ]
|
|
145
|
+
if( !name ) return undefined
|
|
146
|
+
// Gemini ships under two names in the wild; both mean the same account.
|
|
147
|
+
const value = nonBlank( process.env[ name ] )
|
|
148
|
+
?? ( provider === 'google' ? nonBlank( process.env[ 'GEMINI_API_KEY' ] ) : undefined )
|
|
149
|
+
return value
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const nonBlank = ( v: string | undefined ): string | undefined => v && v.trim() ? v : undefined
|
|
153
|
+
|
|
154
|
+
/** Built-in wire for a known provider, or undefined — the host must declare it. */
|
|
155
|
+
export function knownWireFor( provider: LLMProvider ): LLMWire | undefined {
|
|
156
|
+
return KNOWN_PROVIDERS[ provider ]?.wire
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Built-in base URL for a known provider, or undefined — the host must declare it. */
|
|
160
|
+
export function defaultBaseFor( provider: LLMProvider ): string | undefined {
|
|
161
|
+
return KNOWN_PROVIDERS[ provider ]?.baseUrl
|
|
58
162
|
}
|
|
59
163
|
|
|
60
164
|
/**
|
|
61
165
|
* Auth + version headers for the Anthropic wire.
|
|
62
166
|
*
|
|
63
|
-
* Anthropic authenticates with `x-api-key`. Z.ai's
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
167
|
+
* Anthropic authenticates with `x-api-key`. Compatible endpoints (Z.ai's GLM,
|
|
168
|
+
* and every third-party clone since) generally document
|
|
169
|
+
* `Authorization: Bearer`, and accept either. So we send `x-api-key` always,
|
|
170
|
+
* and add the bearer for everyone *except* Anthropic itself — same secret, same
|
|
171
|
+
* host, and the mind keeps working whichever header the endpoint reads.
|
|
68
172
|
*/
|
|
69
173
|
export function anthropicWireHeaders( provider: LLMProvider, apiKey: string ): Record<string, string> {
|
|
70
174
|
return {
|
|
71
175
|
'Content-Type': 'application/json',
|
|
72
176
|
'anthropic-version': '2023-06-01',
|
|
73
177
|
'x-api-key': apiKey,
|
|
74
|
-
...( provider === '
|
|
178
|
+
...( provider === 'anthropic' ? {} : { Authorization: `Bearer ${ apiKey }` } ),
|
|
75
179
|
}
|
|
76
180
|
}
|
|
77
181
|
export interface LLMDirectorConfig {
|
|
@@ -103,6 +207,49 @@ export interface LLMDirectorConfig {
|
|
|
103
207
|
* replay runs). This replaces the former process-global getTokenTracker().
|
|
104
208
|
*/
|
|
105
209
|
tokenTracker?: TokenTracker | null
|
|
210
|
+
/**
|
|
211
|
+
* MODEL_ROUTING W3 — per-call model selection. Absent (or NULL_ROUTER) means
|
|
212
|
+
* every call uses the default model below, exactly as before the seam existed.
|
|
213
|
+
* A router that throws, or names a provider with no usable credential, falls
|
|
214
|
+
* back to the default: a routing problem must never kill a running mind.
|
|
215
|
+
*/
|
|
216
|
+
router?: ModelRouter | null
|
|
217
|
+
/**
|
|
218
|
+
* Per-provider credentials for routed calls. The top-level `apiKey`/`baseUrl`
|
|
219
|
+
* remain the default entry; a route to a provider absent from this map falls
|
|
220
|
+
* back to the default endpoint.
|
|
221
|
+
*/
|
|
222
|
+
credentials?: Partial<Record<string, ProviderCredential>>
|
|
223
|
+
/**
|
|
224
|
+
* Dialect for the default provider. Required when the provider is not one of
|
|
225
|
+
* {@link KNOWN_PROVIDERS} — the engine will not guess how to talk to an
|
|
226
|
+
* endpoint it has never heard of.
|
|
227
|
+
*/
|
|
228
|
+
wire?: LLMWire
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Everything a single call needs to reach a model. Resolved once per call and
|
|
233
|
+
* threaded through the provider methods — never stored on the instance, because
|
|
234
|
+
* the concurrency gate lets several calls be in flight on one director at once
|
|
235
|
+
* and per-call state on `this` would race between them.
|
|
236
|
+
*/
|
|
237
|
+
/** What a host supplies so a routed provider can be reached. */
|
|
238
|
+
export interface ProviderCredential {
|
|
239
|
+
apiKey: string
|
|
240
|
+
baseUrl?: string
|
|
241
|
+
/** Required for providers outside {@link KNOWN_PROVIDERS}. */
|
|
242
|
+
wire?: LLMWire
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export interface CallEndpoint {
|
|
246
|
+
provider: LLMProvider
|
|
247
|
+
/** The dialect to speak. Resolved once; the transport branches on this. */
|
|
248
|
+
wire: LLMWire
|
|
249
|
+
model: string
|
|
250
|
+
apiKey: string
|
|
251
|
+
baseUrl: string
|
|
252
|
+
maxOutputTokens: number
|
|
106
253
|
}
|
|
107
254
|
|
|
108
255
|
// ── LLM call result ──────────────────────────────────────────
|
|
@@ -124,20 +271,102 @@ export interface LLMCallResult {
|
|
|
124
271
|
* here, letting the TokenTracker break spend down per category for transparency.
|
|
125
272
|
*/
|
|
126
273
|
export interface LLMCallMeta {
|
|
127
|
-
/** Top-level cost bucket
|
|
128
|
-
category:
|
|
129
|
-
/** The actor/subsystem doing the work
|
|
130
|
-
attribute:
|
|
131
|
-
/** The specific cognitive function
|
|
132
|
-
|
|
274
|
+
/** Top-level cost bucket. */
|
|
275
|
+
category: LLMCallCategory
|
|
276
|
+
/** The actor/subsystem doing the work. */
|
|
277
|
+
attribute: LLMCallAttribute
|
|
278
|
+
/** The specific cognitive function. */
|
|
279
|
+
process: LLMCallProcess
|
|
280
|
+
/** The specific cognitive function. */
|
|
281
|
+
function: LLMCallFunction
|
|
133
282
|
/** Optional specific id or namespace: facet id, entity id, model name. */
|
|
134
283
|
scope?: string
|
|
135
284
|
/** Free-form human-readable label. Auto-composed from the axes when omitted. */
|
|
136
285
|
label?: string
|
|
286
|
+
/**
|
|
287
|
+
* How much this call demands, 0..1 — MODEL_ROUTING W0.
|
|
288
|
+
*
|
|
289
|
+
* A *cognitive* measure, never a commercial one: it says how consequential or
|
|
290
|
+
* uncertain this moment is, never who is paying for it.
|
|
291
|
+
*
|
|
292
|
+
* What actually reports it today:
|
|
293
|
+
* · the master and every facet — including the deliberation facet — forward
|
|
294
|
+
* `effortScore`, the a-priori effort gate (uncertainty, prior confidence,
|
|
295
|
+
* novelty, a pending reply, stress load);
|
|
296
|
+
* · structurally background work (summarising, the identity guard) reports a
|
|
297
|
+
* low constant, because it is background whether the mind is calm or in
|
|
298
|
+
* crisis.
|
|
299
|
+
*
|
|
300
|
+
* Note what is NOT here: agency's `stakes( winner, bias )` — the consequence
|
|
301
|
+
* of the specific choice under contest — is computed in
|
|
302
|
+
* `agency/selection.scoring.ts` and never reaches a call. So a deliberation
|
|
303
|
+
* facet currently reports the tick's general effort, not the stakes of the
|
|
304
|
+
* choice it is deliberating. Carrying it is MODEL_ROUTING W1, still open.
|
|
305
|
+
* This comment previously claimed deliberation passed those stakes; it does
|
|
306
|
+
* not, and a doc comment that ships to npm is a bad place to be aspirational.
|
|
307
|
+
*
|
|
308
|
+
* Absent means UNKNOWN, not zero: a consumer must fall back to its default
|
|
309
|
+
* rather than treat a missing value as "cheapest possible".
|
|
310
|
+
*
|
|
311
|
+
* This field is inert with respect to cognition. It rides along to whoever
|
|
312
|
+
* resolves the model for a call; no engine may read it back and behave
|
|
313
|
+
* differently, or the routing layer becomes a hidden input to the mind.
|
|
314
|
+
*/
|
|
315
|
+
demand?: number
|
|
137
316
|
}
|
|
138
317
|
|
|
318
|
+
/** Structurally background work — see `LLMCallMeta.demand`. */
|
|
319
|
+
export const BACKGROUND_DEMAND = 0.1
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Escalation is elevated by construction: the buffer only fires once something
|
|
323
|
+
* has already failed to resolve on its own.
|
|
324
|
+
*/
|
|
325
|
+
export const ESCALATION_DEMAND = 0.7
|
|
326
|
+
|
|
139
327
|
/** Default attribution when a caller does not tag itself (back-compat). */
|
|
140
|
-
const DEFAULT_CALL_META: LLMCallMeta = { category: 'executive', attribute: 'master',
|
|
328
|
+
const DEFAULT_CALL_META: LLMCallMeta = { category: 'executive', attribute: 'master', process: 'decision', function: '-' }
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Fill in wire and base URL, or say clearly what is missing.
|
|
332
|
+
*
|
|
333
|
+
* Known providers supply both from {@link KNOWN_PROVIDERS}; anything else must
|
|
334
|
+
* declare them. The engine refuses to guess how to talk to an endpoint it has
|
|
335
|
+
* never heard of — a wrong guess is a 404 at the worst possible moment, and
|
|
336
|
+
* previously the guess was "Anthropic", which is how a GLM Will could end up
|
|
337
|
+
* asking Z.ai for a Claude model id.
|
|
338
|
+
*/
|
|
339
|
+
export function resolveEndpoint( spec: {
|
|
340
|
+
provider: LLMProvider
|
|
341
|
+
model: string
|
|
342
|
+
apiKey: string
|
|
343
|
+
baseUrl?: string | undefined
|
|
344
|
+
wire?: LLMWire | undefined
|
|
345
|
+
maxOutputTokens: number
|
|
346
|
+
} ): CallEndpoint {
|
|
347
|
+
const wire = spec.wire ?? knownWireFor( spec.provider )
|
|
348
|
+
if( !wire )
|
|
349
|
+
throw new Error(
|
|
350
|
+
`LLM provider "${spec.provider}" has no known wire. Declare it: ` +
|
|
351
|
+
`llm.providers['${spec.provider}'].wire = 'anthropic' | 'openai' | 'google'.`
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
const baseUrl = spec.baseUrl ?? defaultBaseFor( spec.provider )
|
|
355
|
+
if( !baseUrl )
|
|
356
|
+
throw new Error(
|
|
357
|
+
`LLM provider "${spec.provider}" has no known base URL. Declare it: ` +
|
|
358
|
+
`llm.providers['${spec.provider}'].baseUrl.`
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
provider: spec.provider,
|
|
363
|
+
wire,
|
|
364
|
+
model: spec.model,
|
|
365
|
+
apiKey: spec.apiKey,
|
|
366
|
+
baseUrl,
|
|
367
|
+
maxOutputTokens: spec.maxOutputTokens,
|
|
368
|
+
}
|
|
369
|
+
}
|
|
141
370
|
|
|
142
371
|
export class LLMDirector {
|
|
143
372
|
private _willId: string
|
|
@@ -150,6 +379,12 @@ export class LLMDirector {
|
|
|
150
379
|
private _baseUrl: string | null
|
|
151
380
|
private _timeoutMs: number
|
|
152
381
|
private _tokenTracker: TokenTracker | null
|
|
382
|
+
private _router: ModelRouter | null
|
|
383
|
+
private _credentials: Partial<Record<string, ProviderCredential>>
|
|
384
|
+
/** Default endpoint — what every call used before the routing seam existed. */
|
|
385
|
+
private _defaultEndpoint: CallEndpoint
|
|
386
|
+
/** Routes already warned about (missing credential / bad provider) — log once. */
|
|
387
|
+
private _routeWarned = new Set<string>()
|
|
153
388
|
|
|
154
389
|
constructor( config: LLMDirectorConfig ) {
|
|
155
390
|
this._willId = config.willId
|
|
@@ -162,6 +397,75 @@ export class LLMDirector {
|
|
|
162
397
|
this._baseUrl = config.baseUrl ?? null
|
|
163
398
|
this._timeoutMs = config.timeoutMs ?? 90_000
|
|
164
399
|
this._tokenTracker = config.tokenTracker ?? null
|
|
400
|
+
this._router = config.router ?? null
|
|
401
|
+
this._credentials = config.credentials ?? {}
|
|
402
|
+
this._defaultEndpoint = resolveEndpoint( {
|
|
403
|
+
provider: this._provider,
|
|
404
|
+
model: this._model,
|
|
405
|
+
apiKey: this._apiKey,
|
|
406
|
+
baseUrl: this._baseUrl ?? undefined,
|
|
407
|
+
wire: config.wire,
|
|
408
|
+
maxOutputTokens: this._maxOutputTokens,
|
|
409
|
+
} )
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Resolve which model serves this call. Falls back to the default endpoint
|
|
414
|
+
* whenever the router has no opinion, throws, or names a provider we hold no
|
|
415
|
+
* credential for — degrade, never crash.
|
|
416
|
+
*/
|
|
417
|
+
private _resolveEndpoint( meta: LLMCallMeta ): CallEndpoint {
|
|
418
|
+
if( isNullRouter( this._router ) ) return this._defaultEndpoint
|
|
419
|
+
|
|
420
|
+
let route
|
|
421
|
+
try { route = this._router!.route( meta ) }
|
|
422
|
+
catch( err ){
|
|
423
|
+
this._warnRouteOnce(`throw:${this._router!.name}`,
|
|
424
|
+
`router "${this._router!.name}" threw — using the default model`, err )
|
|
425
|
+
return this._defaultEndpoint
|
|
426
|
+
}
|
|
427
|
+
if( !route ) return this._defaultEndpoint
|
|
428
|
+
|
|
429
|
+
// A route with no provider means "same vendor, different model" — the whole
|
|
430
|
+
// shape of the per-role model map, and the common case for a host swapping
|
|
431
|
+
// in a cheaper model for background work.
|
|
432
|
+
const provider = route.provider ?? this._defaultEndpoint.provider
|
|
433
|
+
|
|
434
|
+
// The default provider's credential is reused when the route names it;
|
|
435
|
+
// otherwise the route needs its own entry.
|
|
436
|
+
const cred = provider === this._defaultEndpoint.provider
|
|
437
|
+
? { apiKey: this._defaultEndpoint.apiKey, baseUrl: this._defaultEndpoint.baseUrl, wire: this._defaultEndpoint.wire }
|
|
438
|
+
: this._credentials[ provider ]
|
|
439
|
+
|
|
440
|
+
if( !cred?.apiKey ){
|
|
441
|
+
this._warnRouteOnce(`cred:${provider}`,
|
|
442
|
+
`no credential for routed provider "${provider}" — using the default model` )
|
|
443
|
+
return this._defaultEndpoint
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
return resolveEndpoint( {
|
|
448
|
+
provider,
|
|
449
|
+
model: route.model,
|
|
450
|
+
apiKey: cred.apiKey,
|
|
451
|
+
baseUrl: route.baseUrl ?? cred.baseUrl,
|
|
452
|
+
wire: cred.wire,
|
|
453
|
+
maxOutputTokens: route.maxOutputTokens ?? this._defaultEndpoint.maxOutputTokens,
|
|
454
|
+
} )
|
|
455
|
+
}
|
|
456
|
+
catch( err ){
|
|
457
|
+
// An undeclared wire or base URL for a routed provider is a config gap,
|
|
458
|
+
// not a reason to fail the call.
|
|
459
|
+
this._warnRouteOnce(`resolve:${provider}`,
|
|
460
|
+
`cannot reach routed provider "${provider}" — using the default model`, err )
|
|
461
|
+
return this._defaultEndpoint
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private _warnRouteOnce( key: string, message: string, err?: unknown ): void {
|
|
466
|
+
if( this._routeWarned.has( key ) ) return
|
|
467
|
+
this._routeWarned.add( key )
|
|
468
|
+
logger.warn(`[llm.routing] ${message}`, err instanceof Error ? err.message : '')
|
|
165
469
|
}
|
|
166
470
|
|
|
167
471
|
// ── Mock response (test mode) ────────────────────────────
|
|
@@ -268,20 +572,27 @@ export class LLMDirector {
|
|
|
268
572
|
return { text: replay.text, inputTok: replay.inputTok, outputTok: replay.outputTok }
|
|
269
573
|
}
|
|
270
574
|
|
|
575
|
+
// MODEL_ROUTING W3 — resolve once, then thread it: several calls can be in
|
|
576
|
+
// flight on this director at once, so the endpoint must travel with the call
|
|
577
|
+
// rather than live on `this`. Resolved before the mock branch so a mock run
|
|
578
|
+
// records the endpoint that WOULD have served the call — the tape then says
|
|
579
|
+
// the same thing in mock and live runs.
|
|
580
|
+
const ep = this._resolveEndpoint( meta )
|
|
581
|
+
|
|
271
582
|
if( this._mock ){
|
|
272
583
|
const result = this._mockResponse( tick, userMessage )
|
|
273
584
|
// In mock mode we don't stream raw internal text — the response will be
|
|
274
585
|
// emitted from the outbox content by the SSE layer. onChunk is intentionally
|
|
275
586
|
// not called here so no internal [REPLY] / JSON format leaks to the client.
|
|
276
|
-
this._recordCompletion( systemPrompt, userMessage, tick, result, Date.now() - start, true )
|
|
587
|
+
this._recordCompletion( systemPrompt, userMessage, tick, result, Date.now() - start, true, ep )
|
|
277
588
|
return result
|
|
278
589
|
}
|
|
279
590
|
|
|
280
|
-
const result =
|
|
281
|
-
? await this._callAnthropicStream( systemPrompt, userMessage, onChunk, temperature )
|
|
591
|
+
const result = ep.wire === 'anthropic'
|
|
592
|
+
? await this._callAnthropicStream( ep, systemPrompt, userMessage, onChunk, temperature )
|
|
282
593
|
: await ( async () => {
|
|
283
594
|
// Other providers: fall back to regular call, emit whole response as one chunk
|
|
284
|
-
const r = await this._callProvider( systemPrompt, userMessage, temperature )
|
|
595
|
+
const r = await this._callProvider( ep, systemPrompt, userMessage, temperature )
|
|
285
596
|
onChunk( r.text )
|
|
286
597
|
return r
|
|
287
598
|
} )()
|
|
@@ -289,8 +600,8 @@ export class LLMDirector {
|
|
|
289
600
|
// Token tracking lives here too: streamed calls (conversation facets, the
|
|
290
601
|
// master when broadcasting) previously bypassed the tracker entirely, so all
|
|
291
602
|
// streamed spend was invisible. Record it with the caller's attribution.
|
|
292
|
-
this._track( result, meta, tick, Date.now() - start, this._estPromptTokens( systemPrompt, userMessage ) )
|
|
293
|
-
this._recordCompletion( systemPrompt, userMessage, tick, result, Date.now() - start, false )
|
|
603
|
+
this._track( result, meta, tick, Date.now() - start, this._estPromptTokens( systemPrompt, userMessage ), ep )
|
|
604
|
+
this._recordCompletion( systemPrompt, userMessage, tick, result, Date.now() - start, false, ep )
|
|
294
605
|
return result
|
|
295
606
|
}
|
|
296
607
|
|
|
@@ -300,9 +611,17 @@ export class LLMDirector {
|
|
|
300
611
|
* mock/replay directors, so the call is simply skipped. Cache read/write tokens
|
|
301
612
|
* are forwarded so the tracker prices them at 0.1× / 1.25× input.
|
|
302
613
|
*/
|
|
303
|
-
private _track( result: LLMCallResult, meta: LLMCallMeta, tick: Tick, latencyMs: number, estPromptTokens?: number ): void {
|
|
614
|
+
private _track( result: LLMCallResult, meta: LLMCallMeta, tick: Tick, latencyMs: number, estPromptTokens?: number, ep: CallEndpoint = this._defaultEndpoint ): void {
|
|
304
615
|
this._tokenTracker?.recordUsage({
|
|
305
|
-
|
|
616
|
+
// The endpoint that actually served this call — routed or default.
|
|
617
|
+
// Pricing must follow the real model, or routed spend is attributed
|
|
618
|
+
// wrongly; the provider rides along because the same model id can be
|
|
619
|
+
// reached from several vendors at very different prices.
|
|
620
|
+
model: ep.model,
|
|
621
|
+
provider: ep.provider,
|
|
622
|
+
// The router's own input, kept alongside its output. Without this the
|
|
623
|
+
// ledger records WHERE a call went and never WHY.
|
|
624
|
+
demand: meta.demand,
|
|
306
625
|
promptTokens: result.inputTok,
|
|
307
626
|
completionTokens: result.outputTok,
|
|
308
627
|
totalTokens: result.inputTok + result.outputTok,
|
|
@@ -310,6 +629,7 @@ export class LLMDirector {
|
|
|
310
629
|
cacheWriteTokens: result.cacheWriteTok,
|
|
311
630
|
category: meta.category,
|
|
312
631
|
attribute: meta.attribute,
|
|
632
|
+
process: meta.process,
|
|
313
633
|
function: meta.function,
|
|
314
634
|
scope: meta.scope,
|
|
315
635
|
label: meta.label,
|
|
@@ -337,14 +657,17 @@ export class LLMDirector {
|
|
|
337
657
|
result: LLMCallResult,
|
|
338
658
|
latencyMs: number,
|
|
339
659
|
mock: boolean,
|
|
660
|
+
ep: CallEndpoint = this._defaultEndpoint,
|
|
340
661
|
): void {
|
|
341
662
|
try {
|
|
342
663
|
getCompletionRecorder( this._willId )?.recordCompletion({
|
|
343
664
|
tick,
|
|
344
665
|
willId: this._willId,
|
|
345
|
-
|
|
346
|
-
model
|
|
347
|
-
|
|
666
|
+
// Record the endpoint that actually served the call: the tape is what
|
|
667
|
+
// replay re-feeds, so it must say which model produced this text.
|
|
668
|
+
provider: ep.provider,
|
|
669
|
+
model: ep.model,
|
|
670
|
+
maxOutputTokens: ep.maxOutputTokens,
|
|
348
671
|
systemPrompt,
|
|
349
672
|
userMessage,
|
|
350
673
|
text: result.text,
|
|
@@ -374,6 +697,7 @@ export class LLMDirector {
|
|
|
374
697
|
}
|
|
375
698
|
|
|
376
699
|
private async _callAnthropicStream(
|
|
700
|
+
ep: CallEndpoint,
|
|
377
701
|
systemPrompt: string,
|
|
378
702
|
userMessage: string,
|
|
379
703
|
onChunk: ( chunk: string ) => void,
|
|
@@ -387,12 +711,12 @@ export class LLMDirector {
|
|
|
387
711
|
|
|
388
712
|
let res: Response
|
|
389
713
|
try {
|
|
390
|
-
res = await fetch(`${this._resolvedBase()}/messages`, {
|
|
714
|
+
res = await fetch(`${this._resolvedBase( ep )}/messages`, {
|
|
391
715
|
method: 'POST',
|
|
392
|
-
headers: anthropicWireHeaders(
|
|
716
|
+
headers: anthropicWireHeaders( ep.provider, ep.apiKey ),
|
|
393
717
|
body: JSON.stringify({
|
|
394
|
-
model:
|
|
395
|
-
max_tokens:
|
|
718
|
+
model: ep.model,
|
|
719
|
+
max_tokens: ep.maxOutputTokens,
|
|
396
720
|
...( temperature !== undefined ? { temperature } : {} ),
|
|
397
721
|
stream: true,
|
|
398
722
|
system: this._systemField( systemPrompt ),
|
|
@@ -404,7 +728,7 @@ export class LLMDirector {
|
|
|
404
728
|
catch( err ){
|
|
405
729
|
clearTimeout( timer )
|
|
406
730
|
if( controller.signal.aborted )
|
|
407
|
-
throw new Error(`LLM stream to ${
|
|
731
|
+
throw new Error(`LLM stream to ${ep.provider} timed out after ${this._timeoutMs}ms (no response)`)
|
|
408
732
|
throw err
|
|
409
733
|
}
|
|
410
734
|
|
|
@@ -489,9 +813,11 @@ export class LLMDirector {
|
|
|
489
813
|
if( replay )
|
|
490
814
|
return { text: replay.text, inputTok: replay.inputTok, outputTok: replay.outputTok }
|
|
491
815
|
|
|
816
|
+
const ep = this._resolveEndpoint( meta )
|
|
817
|
+
|
|
492
818
|
if( this._mock ){
|
|
493
819
|
const result = this._mockResponse( tick, userMessage )
|
|
494
|
-
this._recordCompletion( systemPrompt, userMessage, tick, result, Date.now() - llmStart, true )
|
|
820
|
+
this._recordCompletion( systemPrompt, userMessage, tick, result, Date.now() - llmStart, true, ep )
|
|
495
821
|
return result
|
|
496
822
|
}
|
|
497
823
|
|
|
@@ -502,44 +828,36 @@ export class LLMDirector {
|
|
|
502
828
|
// accumulated text; live token chunks go through callStream(). Other
|
|
503
829
|
// providers keep the whole-request deadline.
|
|
504
830
|
const result = await withGate(
|
|
505
|
-
() =>
|
|
506
|
-
? this._callAnthropicStream( systemPrompt, userMessage, () => {}, temperature )
|
|
507
|
-
: this._callProvider( systemPrompt, userMessage, temperature ),
|
|
831
|
+
() => ep.wire === 'anthropic'
|
|
832
|
+
? this._callAnthropicStream( ep, systemPrompt, userMessage, () => {}, temperature )
|
|
833
|
+
: this._callProvider( ep, systemPrompt, userMessage, temperature ),
|
|
508
834
|
'executive/direct',
|
|
509
835
|
)
|
|
510
836
|
|
|
511
837
|
// Record token usage + cost into this Will's injected tracker (R4), tagged
|
|
512
838
|
// with the caller's attribution. Optional — absent on mock/replay directors.
|
|
513
|
-
this._track( result, meta, tick, Date.now() - llmStart, this._estPromptTokens( systemPrompt, userMessage ) )
|
|
839
|
+
this._track( result, meta, tick, Date.now() - llmStart, this._estPromptTokens( systemPrompt, userMessage ), ep )
|
|
514
840
|
|
|
515
|
-
this._recordCompletion( systemPrompt, userMessage, tick, result, Date.now() - llmStart, false )
|
|
841
|
+
this._recordCompletion( systemPrompt, userMessage, tick, result, Date.now() - llmStart, false, ep )
|
|
516
842
|
return result
|
|
517
843
|
}
|
|
518
844
|
|
|
519
845
|
private _callProvider(
|
|
846
|
+
ep: CallEndpoint,
|
|
520
847
|
systemPrompt: string,
|
|
521
848
|
userMessage: string,
|
|
522
849
|
temperature?: number,
|
|
523
850
|
): Promise<LLMCallResult> {
|
|
524
|
-
switch(
|
|
525
|
-
case 'anthropic': return this._callAnthropic( systemPrompt, userMessage, temperature )
|
|
526
|
-
case '
|
|
527
|
-
case '
|
|
528
|
-
|
|
529
|
-
case 'google': return this._callGoogle( systemPrompt, userMessage, temperature )
|
|
530
|
-
default: throw new Error(`Unknown LLM provider: ${this._provider}`)
|
|
851
|
+
switch( ep.wire ){
|
|
852
|
+
case 'anthropic': return this._callAnthropic( ep, systemPrompt, userMessage, temperature )
|
|
853
|
+
case 'openai': return this._callOpenAI( ep, systemPrompt, userMessage, temperature )
|
|
854
|
+
case 'google': return this._callGoogle( ep, systemPrompt, userMessage, temperature )
|
|
855
|
+
default: throw new Error(`Unknown LLM wire: ${ep.wire}`)
|
|
531
856
|
}
|
|
532
857
|
}
|
|
533
858
|
|
|
534
|
-
/** Default API base URL (including version segment) for a provider. */
|
|
535
|
-
private _baseFor( provider: LLMProvider ): string {
|
|
536
|
-
return defaultBaseFor( provider )
|
|
537
|
-
}
|
|
538
|
-
|
|
539
859
|
/** Resolved API base: explicit override wins, else the provider default. */
|
|
540
|
-
private _resolvedBase(): string {
|
|
541
|
-
return this._baseUrl ?? this._baseFor( this._provider )
|
|
542
|
-
}
|
|
860
|
+
private _resolvedBase( ep: CallEndpoint ): string { return ep.baseUrl }
|
|
543
861
|
|
|
544
862
|
/**
|
|
545
863
|
* fetch() with a hard per-request deadline. A hung connection is aborted
|
|
@@ -569,23 +887,23 @@ export class LLMDirector {
|
|
|
569
887
|
return [ { type: 'text', text: systemPrompt, cache_control: { type: 'ephemeral' } } ]
|
|
570
888
|
}
|
|
571
889
|
|
|
572
|
-
private async _callAnthropic( systemPrompt: string, userMessage: string, temperature?: number ): Promise<LLMCallResult> {
|
|
890
|
+
private async _callAnthropic( ep: CallEndpoint, systemPrompt: string, userMessage: string, temperature?: number ): Promise<LLMCallResult> {
|
|
573
891
|
const body = {
|
|
574
|
-
model:
|
|
575
|
-
max_tokens:
|
|
892
|
+
model: ep.model,
|
|
893
|
+
max_tokens: ep.maxOutputTokens,
|
|
576
894
|
...( temperature !== undefined ? { temperature } : {} ),
|
|
577
895
|
system: this._systemField( systemPrompt ),
|
|
578
896
|
messages: [{ role: 'user', content: userMessage }]
|
|
579
897
|
}
|
|
580
898
|
|
|
581
|
-
const res = await this._fetchWithTimeout(`${this._resolvedBase()}/messages`, {
|
|
899
|
+
const res = await this._fetchWithTimeout(`${this._resolvedBase( ep )}/messages`, {
|
|
582
900
|
method: 'POST',
|
|
583
|
-
headers: anthropicWireHeaders(
|
|
901
|
+
headers: anthropicWireHeaders( ep.provider, ep.apiKey ),
|
|
584
902
|
body: JSON.stringify( body )
|
|
585
903
|
})
|
|
586
904
|
|
|
587
905
|
if( !res.ok )
|
|
588
|
-
throw new Error(`${
|
|
906
|
+
throw new Error(`${ep.provider} API ${res.status}: ${( await res.text() ).slice(0, 300)}`)
|
|
589
907
|
|
|
590
908
|
const
|
|
591
909
|
data = await res.json() as {
|
|
@@ -603,10 +921,10 @@ export class LLMDirector {
|
|
|
603
921
|
}
|
|
604
922
|
}
|
|
605
923
|
|
|
606
|
-
private async _callOpenAI( systemPrompt: string, userMessage: string, temperature?: number ): Promise<LLMCallResult> {
|
|
924
|
+
private async _callOpenAI( ep: CallEndpoint, systemPrompt: string, userMessage: string, temperature?: number ): Promise<LLMCallResult> {
|
|
607
925
|
const body = {
|
|
608
|
-
model:
|
|
609
|
-
max_completion_tokens:
|
|
926
|
+
model: ep.model,
|
|
927
|
+
max_completion_tokens: ep.maxOutputTokens,
|
|
610
928
|
...( temperature !== undefined ? { temperature } : {} ),
|
|
611
929
|
messages: [
|
|
612
930
|
{ role: 'system', content: systemPrompt },
|
|
@@ -614,11 +932,11 @@ export class LLMDirector {
|
|
|
614
932
|
]
|
|
615
933
|
}
|
|
616
934
|
|
|
617
|
-
const res = await this._fetchWithTimeout(`${this._resolvedBase()}/chat/completions`, {
|
|
935
|
+
const res = await this._fetchWithTimeout(`${this._resolvedBase( ep )}/chat/completions`, {
|
|
618
936
|
method: 'POST',
|
|
619
937
|
headers: {
|
|
620
938
|
'Content-Type': 'application/json',
|
|
621
|
-
'Authorization': `Bearer ${
|
|
939
|
+
'Authorization': `Bearer ${ep.apiKey}`,
|
|
622
940
|
},
|
|
623
941
|
body: JSON.stringify(body),
|
|
624
942
|
})
|
|
@@ -640,25 +958,25 @@ export class LLMDirector {
|
|
|
640
958
|
}
|
|
641
959
|
}
|
|
642
960
|
|
|
643
|
-
private async _callGoogle( systemPrompt: string, userMessage: string, temperature?: number ): Promise<LLMCallResult> {
|
|
961
|
+
private async _callGoogle( ep: CallEndpoint, systemPrompt: string, userMessage: string, temperature?: number ): Promise<LLMCallResult> {
|
|
644
962
|
// Gemini carries the system prompt in a dedicated `systemInstruction`
|
|
645
963
|
// field and the conversation in `contents`.
|
|
646
964
|
const body = {
|
|
647
965
|
systemInstruction: { parts: [ { text: systemPrompt } ] },
|
|
648
966
|
contents: [ { role: 'user', parts: [ { text: userMessage } ] } ],
|
|
649
967
|
generationConfig: {
|
|
650
|
-
maxOutputTokens:
|
|
968
|
+
maxOutputTokens: ep.maxOutputTokens,
|
|
651
969
|
...( temperature !== undefined ? { temperature } : {} ),
|
|
652
970
|
},
|
|
653
971
|
}
|
|
654
972
|
|
|
655
973
|
const res = await this._fetchWithTimeout(
|
|
656
|
-
`${this._resolvedBase()}/models/${
|
|
974
|
+
`${this._resolvedBase( ep )}/models/${ep.model}:generateContent`,
|
|
657
975
|
{
|
|
658
976
|
method: 'POST',
|
|
659
977
|
headers: {
|
|
660
978
|
'Content-Type': 'application/json',
|
|
661
|
-
'x-goog-api-key':
|
|
979
|
+
'x-goog-api-key': ep.apiKey,
|
|
662
980
|
},
|
|
663
981
|
body: JSON.stringify( body ),
|
|
664
982
|
}
|