@yeaft/webchat-agent 0.1.412 → 0.1.414

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/unify/init.js CHANGED
@@ -23,56 +23,28 @@ const SUBDIRS = [
23
23
  'skills',
24
24
  ];
25
25
 
26
- /** Default config.md content (YAML frontmatter + markdown body). */
27
- const DEFAULT_CONFIG = `---
28
- model: claude-sonnet-4-20250514
29
- language: en
30
- debug: false
31
- maxContextTokens: 200000
32
- ---
33
-
34
- # Yeaft Config
35
-
36
- Edit the YAML frontmatter above to change settings.
37
- The \`model\` field is a model ID (e.g. \`gpt-5\`, \`claude-sonnet-4-20250514\`).
38
- Yeaft auto-detects the correct API adapter and endpoint from the model ID.
39
-
40
- ## Language
41
-
42
- Set \`language\` in frontmatter or \`YEAFT_LANGUAGE\` env var:
43
- - \`en\` — English system prompts (default)
44
- - \`zh\` — Chinese system prompts (中文系统提示)
45
-
46
- ## Model IDs
47
-
48
- - \`claude-sonnet-4-20250514\` (default)
49
- - \`claude-opus-4-20250514\`
50
- - \`gpt-5\`, \`gpt-5.4\`, \`gpt-4.1\`, \`gpt-4.1-mini\`
51
- - \`o3\`, \`o4-mini\`
52
- - \`deepseek-chat\`, \`deepseek-reasoner\`
53
- - \`gemini-2.5-pro\`, \`gemini-2.5-flash\`
54
-
55
- ## API Keys
56
-
57
- Store API keys in \`~/.yeaft/.env\` (recommended) or export as env vars:
58
-
59
- \`\`\`bash
60
- # ~/.yeaft/.env
61
- YEAFT_API_KEY=sk-ant-... # Anthropic
62
- YEAFT_OPENAI_API_KEY=sk-... # OpenAI / DeepSeek / Gemini
63
- \`\`\`
64
-
65
- ## Environment Variables
66
-
67
- Shell env vars take precedence over .env and config.md:
68
-
69
- - \`YEAFT_MODEL\` — override model ID
70
- - \`YEAFT_LANGUAGE\` — language for system prompts (en/zh)
71
- - \`YEAFT_API_KEY\` — Anthropic API key
72
- - \`YEAFT_OPENAI_API_KEY\` — OpenAI-compatible API key
73
- - \`YEAFT_PROXY_URL\` — CopilotProxy URL (default: http://localhost:6628)
74
- - \`YEAFT_DEBUG\` — enable debug mode (1/true)
75
- - \`YEAFT_DIR\` — data directory (default: ~/.yeaft)
26
+ /** Default config.json — generated on first init as default configuration. */
27
+ const DEFAULT_CONFIG_JSON = `{
28
+ "providers": [
29
+ {
30
+ "name": "my-proxy",
31
+ "baseUrl": "http://localhost:6628/v1",
32
+ "apiKey": "proxy",
33
+ "models": [
34
+ "claude-sonnet-4-20250514",
35
+ "claude-haiku-3-20250414",
36
+ "gpt-5",
37
+ "deepseek-chat"
38
+ ]
39
+ }
40
+ ],
41
+ "primaryModel": "my-proxy/claude-sonnet-4-20250514",
42
+ "fastModel": "my-proxy/claude-sonnet-4-20250514",
43
+ "language": "en",
44
+ "debug": false,
45
+ "maxContextTokens": 200000,
46
+ "messageTokenBudget": 8192
47
+ }
76
48
  `;
77
49
 
78
50
  /** Default MEMORY.md content. */
@@ -88,6 +60,23 @@ This file stores persistent memory entries. The agent will read and update this
88
60
 
89
61
  `;
90
62
 
63
+ /** Default mcp.json example — generated as reference for MCP server configuration. */
64
+ const DEFAULT_MCP_EXAMPLE = `{
65
+ "_comment": "MCP server configuration. Rename to mcp.json to enable.",
66
+ "_docs": "Each server needs 'name' + 'command'. Optional: 'args', 'env'.",
67
+ "servers": [
68
+ {
69
+ "name": "example-github",
70
+ "command": "npx",
71
+ "args": ["@modelcontextprotocol/server-github"],
72
+ "env": {
73
+ "GITHUB_TOKEN": "ghp_your_token_here"
74
+ }
75
+ }
76
+ ]
77
+ }
78
+ `;
79
+
91
80
  /** Default conversation/index.md content. */
92
81
  const DEFAULT_CONVERSATION_INDEX = `---
93
82
  lastMessageId: null
@@ -125,10 +114,11 @@ export function initYeaftDir(dir) {
125
114
  }
126
115
 
127
116
  // Create default files if they don't exist
128
- const configPath = join(root, 'config.md');
129
- if (!existsSync(configPath)) {
130
- writeFileSync(configPath, DEFAULT_CONFIG, 'utf8');
131
- created.push(configPath);
117
+ // config.json — default configuration (user edits this directly)
118
+ const configJsonPath = join(root, 'config.json');
119
+ if (!existsSync(configJsonPath)) {
120
+ writeFileSync(configJsonPath, DEFAULT_CONFIG_JSON, 'utf8');
121
+ created.push(configJsonPath);
132
122
  }
133
123
 
134
124
  const memoryPath = join(root, 'memory', 'MEMORY.md');
@@ -143,5 +133,12 @@ export function initYeaftDir(dir) {
143
133
  created.push(indexPath);
144
134
  }
145
135
 
136
+ // mcp.json.example — reference template for MCP server configuration
137
+ const mcpExamplePath = join(root, 'mcp.json.example');
138
+ if (!existsSync(mcpExamplePath)) {
139
+ writeFileSync(mcpExamplePath, DEFAULT_MCP_EXAMPLE, 'utf8');
140
+ created.push(mcpExamplePath);
141
+ }
142
+
146
143
  return { dir: root, created };
147
144
  }
@@ -143,10 +143,22 @@ export class LLMAdapter {
143
143
  /**
144
144
  * Create an LLM adapter based on configuration.
145
145
  *
146
+ * If config.providers exists (config.json path), creates an AdapterRouter
147
+ * that routes requests to the correct provider based on model ID.
148
+ *
149
+ * Otherwise falls back to legacy single-adapter creation from env vars.
150
+ *
146
151
  * @param {object} config — From loadConfig()
147
152
  * @returns {Promise<LLMAdapter>}
148
153
  */
149
154
  export async function createLLMAdapter(config) {
155
+ // ─── New path: config.json with providers ─────────────
156
+ if (config.providers && config.providers.length > 0) {
157
+ const { AdapterRouter } = await import('./router.js');
158
+ return new AdapterRouter({ providers: config.providers });
159
+ }
160
+
161
+ // ─── Legacy path: single adapter from env vars ────────
150
162
  // Normalize adapter name — accept 'chat-completions' as alias for 'openai'
151
163
  const adapter = config.adapter === 'chat-completions' ? 'openai' : config.adapter;
152
164
 
@@ -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
  }