@yeaft/webchat-agent 0.1.411 → 0.1.413

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.
@@ -0,0 +1,148 @@
1
+ /**
2
+ * router.js — AdapterRouter: routes LLM calls to the correct provider adapter
3
+ *
4
+ * Given a providers array from config.json:
5
+ * [{ name, baseUrl, apiKey, protocol?, models[] }, ...]
6
+ *
7
+ * The router resolves model → provider, lazy-creates the right adapter
8
+ * (AnthropicAdapter or ChatCompletionsAdapter based on protocol), caches it,
9
+ * and forwards stream()/call() to the resolved adapter.
10
+ *
11
+ * protocol defaults to "openai" (Chat Completions API).
12
+ * Set protocol: "anthropic" only for direct Anthropic API connections.
13
+ */
14
+
15
+ import { LLMAdapter } from './adapter.js';
16
+
17
+ /**
18
+ * AdapterRouter — Implements LLMAdapter, routes by model → provider.
19
+ */
20
+ export class AdapterRouter extends LLMAdapter {
21
+ /** @type {Map<string, object>} modelId → provider config */
22
+ #modelToProvider;
23
+
24
+ /** @type {Map<string, LLMAdapter>} providerName → cached adapter */
25
+ #adapterCache;
26
+
27
+ /** @type {object[]} raw providers array */
28
+ #providers;
29
+
30
+ /**
31
+ * @param {{ providers: object[], config?: object }} params
32
+ * @param {object[]} params.providers — Array of { name, baseUrl, apiKey, protocol?, models[] }
33
+ */
34
+ constructor({ providers }) {
35
+ super();
36
+ this.#providers = providers;
37
+ this.#modelToProvider = new Map();
38
+ this.#adapterCache = new Map();
39
+
40
+ // Build model → provider index
41
+ // First provider wins if model appears in multiple providers
42
+ for (const provider of providers) {
43
+ if (!Array.isArray(provider.models)) continue;
44
+ for (const modelId of provider.models) {
45
+ if (!this.#modelToProvider.has(modelId)) {
46
+ this.#modelToProvider.set(modelId, provider);
47
+ }
48
+ }
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Resolve a model ID to its provider's adapter (lazy-created, cached).
54
+ *
55
+ * @param {string} modelId
56
+ * @returns {Promise<LLMAdapter>}
57
+ */
58
+ async #resolveAdapter(modelId) {
59
+ const provider = this.#modelToProvider.get(modelId);
60
+ if (!provider) {
61
+ throw new Error(
62
+ `Model "${modelId}" not found in any provider. ` +
63
+ `Available models: ${[...this.#modelToProvider.keys()].join(', ') || '(none)'}. ` +
64
+ `Check your config.json providers[].models arrays.`
65
+ );
66
+ }
67
+
68
+ // Check cache
69
+ const cached = this.#adapterCache.get(provider.name);
70
+ if (cached) return cached;
71
+
72
+ // Create adapter based on protocol
73
+ const protocol = provider.protocol || 'openai';
74
+ let adapter;
75
+
76
+ if (protocol === 'anthropic') {
77
+ const { AnthropicAdapter } = await import('./anthropic.js');
78
+ adapter = new AnthropicAdapter({
79
+ apiKey: provider.apiKey,
80
+ baseUrl: provider.baseUrl,
81
+ });
82
+ } else {
83
+ // Default: openai (Chat Completions API) — covers proxy, OpenAI, DeepSeek, Gemini, etc.
84
+ const { ChatCompletionsAdapter } = await import('./chat-completions.js');
85
+ adapter = new ChatCompletionsAdapter({
86
+ apiKey: provider.apiKey,
87
+ baseUrl: provider.baseUrl,
88
+ });
89
+ }
90
+
91
+ this.#adapterCache.set(provider.name, adapter);
92
+ return adapter;
93
+ }
94
+
95
+ /**
96
+ * Stream a model response — routes to the correct provider adapter.
97
+ *
98
+ * @param {import('./adapter.js').StreamParams} params
99
+ * @returns {AsyncGenerator<import('./adapter.js').StreamEvent>}
100
+ */
101
+ async *stream(params) {
102
+ const adapter = await this.#resolveAdapter(params.model);
103
+ yield* adapter.stream(params);
104
+ }
105
+
106
+ /**
107
+ * Make a single model call — routes to the correct provider adapter.
108
+ *
109
+ * @param {import('./adapter.js').CallParams} params
110
+ * @returns {Promise<{ text: string, usage: { inputTokens: number, outputTokens: number } }>}
111
+ */
112
+ async call(params) {
113
+ const adapter = await this.#resolveAdapter(params.model);
114
+ return adapter.call(params);
115
+ }
116
+
117
+ /**
118
+ * Get the provider config for a given model.
119
+ *
120
+ * @param {string} modelId
121
+ * @returns {object|null} — Provider config or null
122
+ */
123
+ getProviderForModel(modelId) {
124
+ return this.#modelToProvider.get(modelId) || null;
125
+ }
126
+
127
+ /**
128
+ * List all available models across all providers.
129
+ *
130
+ * @returns {{ modelId: string, providerName: string }[]}
131
+ */
132
+ listAvailableModels() {
133
+ const result = [];
134
+ for (const [modelId, provider] of this.#modelToProvider) {
135
+ result.push({ modelId, providerName: provider.name });
136
+ }
137
+ return result;
138
+ }
139
+
140
+ /**
141
+ * Get the raw providers config.
142
+ *
143
+ * @returns {object[]}
144
+ */
145
+ get providers() {
146
+ return this.#providers;
147
+ }
148
+ }
package/unify/models.js CHANGED
@@ -14,8 +14,9 @@
14
14
 
15
15
  /**
16
16
  * @typedef {Object} ModelInfo
17
- * @property {'anthropic' | 'chat-completions'} adapter — Which adapter to use
18
- * @property {string} baseUrl — API endpoint base URL
17
+ * @property {'anthropic' | 'openai' | 'deepseek' | 'google'} provider — Which provider this model belongs to
18
+ * @property {'anthropic' | 'chat-completions'} adapter — Which wire protocol to use
19
+ * @property {string} baseUrl — Official API endpoint base URL
19
20
  * @property {number} contextWindow — Max context tokens
20
21
  * @property {number} maxOutputTokens — Max output tokens
21
22
  * @property {string} displayName — Human-readable model name
@@ -25,6 +26,7 @@
25
26
  export const MODEL_REGISTRY = new Map([
26
27
  // ── Anthropic ──────────────────────────────────────────────────
27
28
  ['claude-sonnet-4-20250514', {
29
+ provider: 'anthropic',
28
30
  adapter: 'anthropic',
29
31
  baseUrl: 'https://api.anthropic.com',
30
32
  contextWindow: 200000,
@@ -32,6 +34,7 @@ export const MODEL_REGISTRY = new Map([
32
34
  displayName: 'Claude Sonnet 4',
33
35
  }],
34
36
  ['claude-opus-4-20250514', {
37
+ provider: 'anthropic',
35
38
  adapter: 'anthropic',
36
39
  baseUrl: 'https://api.anthropic.com',
37
40
  contextWindow: 200000,
@@ -39,6 +42,7 @@ export const MODEL_REGISTRY = new Map([
39
42
  displayName: 'Claude Opus 4',
40
43
  }],
41
44
  ['claude-haiku-3-20250414', {
45
+ provider: 'anthropic',
42
46
  adapter: 'anthropic',
43
47
  baseUrl: 'https://api.anthropic.com',
44
48
  contextWindow: 200000,
@@ -48,6 +52,7 @@ export const MODEL_REGISTRY = new Map([
48
52
 
49
53
  // ── OpenAI ─────────────────────────────────────────────────────
50
54
  ['gpt-5', {
55
+ provider: 'openai',
51
56
  adapter: 'chat-completions',
52
57
  baseUrl: 'https://api.openai.com/v1',
53
58
  contextWindow: 256000,
@@ -55,6 +60,7 @@ export const MODEL_REGISTRY = new Map([
55
60
  displayName: 'GPT-5',
56
61
  }],
57
62
  ['gpt-5.4', {
63
+ provider: 'openai',
58
64
  adapter: 'chat-completions',
59
65
  baseUrl: 'https://api.openai.com/v1',
60
66
  contextWindow: 272000,
@@ -62,6 +68,7 @@ export const MODEL_REGISTRY = new Map([
62
68
  displayName: 'GPT-5.4',
63
69
  }],
64
70
  ['gpt-4.1', {
71
+ provider: 'openai',
65
72
  adapter: 'chat-completions',
66
73
  baseUrl: 'https://api.openai.com/v1',
67
74
  contextWindow: 1047576,
@@ -69,6 +76,7 @@ export const MODEL_REGISTRY = new Map([
69
76
  displayName: 'GPT-4.1',
70
77
  }],
71
78
  ['gpt-4.1-mini', {
79
+ provider: 'openai',
72
80
  adapter: 'chat-completions',
73
81
  baseUrl: 'https://api.openai.com/v1',
74
82
  contextWindow: 1047576,
@@ -76,6 +84,7 @@ export const MODEL_REGISTRY = new Map([
76
84
  displayName: 'GPT-4.1 Mini',
77
85
  }],
78
86
  ['gpt-4.1-nano', {
87
+ provider: 'openai',
79
88
  adapter: 'chat-completions',
80
89
  baseUrl: 'https://api.openai.com/v1',
81
90
  contextWindow: 1047576,
@@ -83,6 +92,7 @@ export const MODEL_REGISTRY = new Map([
83
92
  displayName: 'GPT-4.1 Nano',
84
93
  }],
85
94
  ['o3', {
95
+ provider: 'openai',
86
96
  adapter: 'chat-completions',
87
97
  baseUrl: 'https://api.openai.com/v1',
88
98
  contextWindow: 200000,
@@ -90,6 +100,7 @@ export const MODEL_REGISTRY = new Map([
90
100
  displayName: 'o3',
91
101
  }],
92
102
  ['o4-mini', {
103
+ provider: 'openai',
93
104
  adapter: 'chat-completions',
94
105
  baseUrl: 'https://api.openai.com/v1',
95
106
  contextWindow: 200000,
@@ -99,6 +110,7 @@ export const MODEL_REGISTRY = new Map([
99
110
 
100
111
  // ── DeepSeek ───────────────────────────────────────────────────
101
112
  ['deepseek-chat', {
113
+ provider: 'deepseek',
102
114
  adapter: 'chat-completions',
103
115
  baseUrl: 'https://api.deepseek.com',
104
116
  contextWindow: 131072,
@@ -106,6 +118,7 @@ export const MODEL_REGISTRY = new Map([
106
118
  displayName: 'DeepSeek Chat',
107
119
  }],
108
120
  ['deepseek-reasoner', {
121
+ provider: 'deepseek',
109
122
  adapter: 'chat-completions',
110
123
  baseUrl: 'https://api.deepseek.com',
111
124
  contextWindow: 131072,
@@ -115,6 +128,7 @@ export const MODEL_REGISTRY = new Map([
115
128
 
116
129
  // ── Google (via OpenAI-compatible API) ─────────────────────────
117
130
  ['gemini-2.5-pro', {
131
+ provider: 'google',
118
132
  adapter: 'chat-completions',
119
133
  baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
120
134
  contextWindow: 1048576,
@@ -122,6 +136,7 @@ export const MODEL_REGISTRY = new Map([
122
136
  displayName: 'Gemini 2.5 Pro',
123
137
  }],
124
138
  ['gemini-2.5-flash', {
139
+ provider: 'google',
125
140
  adapter: 'chat-completions',
126
141
  baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
127
142
  contextWindow: 1048576,
@@ -165,3 +180,32 @@ export function listModels() {
165
180
  export function isKnownModel(modelName) {
166
181
  return MODEL_REGISTRY.has(modelName);
167
182
  }
183
+
184
+ /**
185
+ * Get the provider name for a model.
186
+ *
187
+ * @param {string} modelName
188
+ * @returns {string|null} — Provider name ('anthropic', 'openai', 'deepseek', 'google') or null
189
+ */
190
+ export function getProviderForModel(modelName) {
191
+ const info = MODEL_REGISTRY.get(modelName);
192
+ return info ? info.provider : null;
193
+ }
194
+
195
+ /**
196
+ * Parse a model reference in "providerName/modelId" format.
197
+ *
198
+ * @param {string} ref — e.g. "my-proxy/claude-sonnet-4-20250514" or "claude-sonnet-4-20250514"
199
+ * @returns {{ providerName: string|null, modelId: string }}
200
+ */
201
+ export function parseModelRef(ref) {
202
+ if (!ref) return { providerName: null, modelId: '' };
203
+ const slashIdx = ref.indexOf('/');
204
+ if (slashIdx === -1) {
205
+ return { providerName: null, modelId: ref };
206
+ }
207
+ return {
208
+ providerName: ref.slice(0, slashIdx),
209
+ modelId: ref.slice(slashIdx + 1),
210
+ };
211
+ }
package/unify/session.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * a fully wired Session ready for queries.
14
14
  */
15
15
 
16
- import { initYeaftDir } from './init.js';
16
+ import { initYeaftDir, DEFAULT_YEAFT_DIR } from './init.js';
17
17
  import { loadConfig, loadMCPConfig } from './config.js';
18
18
  import { createTrace } from './debug-trace.js';
19
19
  import { createLLMAdapter } from './llm/adapter.js';
@@ -46,6 +46,7 @@ import exitWorktree from './tools/exit-worktree.js';
46
46
  /**
47
47
  * @typedef {Object} Session
48
48
  * @property {Engine} engine — The wired engine, ready for .query()
49
+ * @property {import('./llm/adapter.js').LLMAdapter} adapter — The LLM adapter
49
50
  * @property {object} config — Resolved configuration
50
51
  * @property {ConversationStore} conversationStore — Conversation persistence
51
52
  * @property {MemoryStore} memoryStore — Memory persistence
@@ -80,18 +81,21 @@ export async function loadSession(options = {}) {
80
81
  configOverrides = {},
81
82
  } = options;
82
83
 
83
- // ─── 1. Load config (determines yeaftDir) ──────────────
84
+ // ─── 1. Determine yeaftDir + ensure directory structure ──
85
+ // Must happen BEFORE loadConfig so that first-run
86
+ // generates a default config.json that loadConfig can read.
84
87
  const overrides = { ...configOverrides };
85
88
  if (dir) overrides.dir = dir;
86
89
  if (model) overrides.model = model;
87
90
  if (language) overrides.language = language;
88
91
  if (debug !== undefined) overrides.debug = debug;
89
92
 
90
- const config = loadConfig(overrides);
91
- const yeaftDir = config.dir;
92
-
93
- // ─── 2. Ensure directory structure ─────────────────────
93
+ const yeaftDir = overrides.dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
94
94
  initYeaftDir(yeaftDir);
95
+ overrides.dir = yeaftDir;
96
+
97
+ // ─── 2. Load config ───────────────────────────────────
98
+ const config = loadConfig(overrides);
95
99
 
96
100
  // ─── 3. Create debug trace ─────────────────────────────
97
101
  const trace = createTrace({
@@ -177,6 +181,7 @@ export async function loadSession(options = {}) {
177
181
 
178
182
  return {
179
183
  engine,
184
+ adapter,
180
185
  config,
181
186
  conversationStore,
182
187
  memoryStore,
@@ -23,6 +23,7 @@ import { checkDreamGate, incrementQueryCount, dream } from './memory/dream.js';
23
23
  * memoryStore: import('./memory/store.js').MemoryStore,
24
24
  * adapter: object,
25
25
  * config: object,
26
+ * primaryModel?: string,
26
27
  * messages?: object[],
27
28
  * taskId?: string,
28
29
  * workerId?: string,
@@ -38,11 +39,15 @@ export async function runStopHooks(context) {
38
39
  memoryStore,
39
40
  adapter,
40
41
  config,
42
+ primaryModel,
41
43
  messages = [],
42
44
  taskId,
43
45
  trace,
44
46
  } = context;
45
47
 
48
+ // Model name for persisted messages: use primaryModel if provided, else config.model
49
+ const persistModel = primaryModel || config?.model;
50
+
46
51
  const result = {
47
52
  messagesPersisted: 0,
48
53
  consolidated: false,
@@ -65,7 +70,7 @@ export async function runStopHooks(context) {
65
70
  role: msg.role,
66
71
  content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
67
72
  mode,
68
- model: config.model,
73
+ model: persistModel,
69
74
  });
70
75
  result.messagesPersisted++;
71
76
  }