@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.
- package/package.json +3 -2
- package/service/index.js +10 -0
- package/unify/cli.js +208 -106
- package/unify/config.js +174 -181
- package/unify/engine.js +22 -4
- package/unify/eval/cases/e2e.js +154 -0
- package/unify/eval/cases/memory.js +182 -0
- package/unify/eval/cases/skills.js +51 -0
- package/unify/eval/cases/tool-use.js +356 -0
- package/unify/eval/run-eval.js +250 -0
- package/unify/eval/runner.js +525 -0
- package/unify/index.js +2 -1
- package/unify/init.js +51 -54
- package/unify/llm/adapter.js +12 -0
- package/unify/llm/router.js +148 -0
- package/unify/models.js +46 -2
- package/unify/session.js +11 -6
- package/unify/stop-hooks.js +6 -1
|
@@ -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' | '
|
|
18
|
-
* @property {
|
|
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.
|
|
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
|
|
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,
|
package/unify/stop-hooks.js
CHANGED
|
@@ -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:
|
|
73
|
+
model: persistModel,
|
|
69
74
|
});
|
|
70
75
|
result.messagesPersisted++;
|
|
71
76
|
}
|