@yeaft/webchat-agent 0.1.998 → 0.1.999
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 +1 -1
- package/yeaft/config.js +7 -1
- package/yeaft/engine.js +3 -1
- package/yeaft/llm/anthropic.js +5 -5
- package/yeaft/llm/openai-responses.js +5 -5
- package/yeaft/llm/router.js +7 -5
- package/yeaft/models.js +38 -5
- package/yeaft/sessions/session-config.js +14 -2
- package/yeaft/web-bridge.js +14 -1
package/package.json
CHANGED
package/yeaft/config.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import { existsSync, readFileSync } from 'fs';
|
|
23
23
|
import { join } from 'path';
|
|
24
24
|
import { DEFAULT_YEAFT_DIR } from './init.js';
|
|
25
|
-
import { resolveModel, parseModelRef, normalizeProviderModels, resolveContextWindow, resolveMaxOutputTokens } from './models.js';
|
|
25
|
+
import { getModelEffortOptions, modelSupportsEffort, resolveModel, parseModelRef, normalizeProviderModels, resolveContextWindow, resolveMaxOutputTokens } from './models.js';
|
|
26
26
|
import { normalizeKnownProviderForRuntime } from './llm/known-providers.js';
|
|
27
27
|
|
|
28
28
|
/** Default configuration values. */
|
|
@@ -394,11 +394,17 @@ export function loadConfig(overrides = {}) {
|
|
|
394
394
|
if (!config.availableModels.some(am => am.id === m.id)) {
|
|
395
395
|
const entry = {
|
|
396
396
|
id: m.id,
|
|
397
|
+
ref: p.name ? `${p.name}/${m.id}` : m.id,
|
|
397
398
|
provider: p.name,
|
|
398
399
|
label: m.id,
|
|
399
400
|
};
|
|
400
401
|
if (m.contextWindow !== undefined) entry.contextWindow = m.contextWindow;
|
|
401
402
|
if (m.maxOutput !== undefined) entry.maxOutput = m.maxOutput;
|
|
403
|
+
const effortOptions = getModelEffortOptions(m.id);
|
|
404
|
+
if (effortOptions.length > 0) {
|
|
405
|
+
entry.supportsEffort = modelSupportsEffort(m.id);
|
|
406
|
+
entry.effortOptions = effortOptions;
|
|
407
|
+
}
|
|
402
408
|
config.availableModels.push(entry);
|
|
403
409
|
}
|
|
404
410
|
}
|
package/yeaft/engine.js
CHANGED
|
@@ -1372,7 +1372,8 @@ export class Engine {
|
|
|
1372
1372
|
// valid prompt prefix.
|
|
1373
1373
|
const parsed = parseEffortPrefix(prompt);
|
|
1374
1374
|
const effectivePrompt = parsed.cleanedPrompt;
|
|
1375
|
-
const
|
|
1375
|
+
const configuredEffort = normalizeEffort(this.#config?.modelEffort);
|
|
1376
|
+
const effectiveUserEffort = normalizeEffort(userEffort) || parsed.effort || configuredEffort || null;
|
|
1376
1377
|
const effectiveCollabToolPolicy = collabToolPolicy === COLLAB_TOOL_POLICY.SINGLE_VP || collabToolPolicy === COLLAB_TOOL_POLICY.MULTI_VP
|
|
1377
1378
|
? collabToolPolicy
|
|
1378
1379
|
: null;
|
|
@@ -1957,6 +1958,7 @@ export class Engine {
|
|
|
1957
1958
|
tools: toolDefs.length > 0 ? toolDefs : undefined,
|
|
1958
1959
|
maxTokens: this.#config.maxOutputTokens || 16384,
|
|
1959
1960
|
effort: resolvedEffort,
|
|
1961
|
+
effortSource: userEffort ? 'user' : 'auto',
|
|
1960
1962
|
signal,
|
|
1961
1963
|
onRawExchange: captureRawExchange,
|
|
1962
1964
|
})) {
|
package/yeaft/llm/anthropic.js
CHANGED
|
@@ -156,10 +156,10 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
/**
|
|
159
|
-
* @param {{ model: string, system: string, messages: import('./adapter.js').UnifiedMessage[], tools?: import('./adapter.js').UnifiedToolDef[], maxTokens?: number, effort?: 'low'|'medium'|'high'|'max', signal?: AbortSignal }} params
|
|
159
|
+
* @param {{ model: string, system: string, messages: import('./adapter.js').UnifiedMessage[], tools?: import('./adapter.js').UnifiedToolDef[], maxTokens?: number, effort?: 'low'|'medium'|'high'|'max', effortSource?: 'user'|'auto', signal?: AbortSignal }} params
|
|
160
160
|
* @returns {AsyncGenerator<import('./adapter.js').StreamEvent>}
|
|
161
161
|
*/
|
|
162
|
-
async *stream({ model, system, messages, tools, maxTokens = 16384, effort, signal, onRawExchange }) {
|
|
162
|
+
async *stream({ model, system, messages, tools, maxTokens = 16384, effort, effortSource, signal, onRawExchange }) {
|
|
163
163
|
if (signal?.aborted) throw new LLMAbortError();
|
|
164
164
|
|
|
165
165
|
const body = {
|
|
@@ -175,7 +175,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
175
175
|
// 'anthropic' thinking protocol. Unknown models or non-thinking models
|
|
176
176
|
// silently drop the parameter — red line: never error on unsupported.
|
|
177
177
|
const normEffort = normalizeEffort(effort);
|
|
178
|
-
if (thinkingV1Enabled() && normEffort) {
|
|
178
|
+
if ((thinkingV1Enabled() || effortSource === 'user') && normEffort) {
|
|
179
179
|
const cap = getThinkingCapability(model);
|
|
180
180
|
if (cap.supportsThinking && cap.thinkingProtocol === 'anthropic') {
|
|
181
181
|
const budget = thinkingBudgetForEffort(model, normEffort);
|
|
@@ -427,7 +427,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
427
427
|
* models silently drop the param. max_tokens auto-widens to budget+1024
|
|
428
428
|
* when needed.
|
|
429
429
|
*/
|
|
430
|
-
async call({ model, system, messages, maxTokens = 4096, effort, signal }) {
|
|
430
|
+
async call({ model, system, messages, maxTokens = 4096, effort, effortSource, signal }) {
|
|
431
431
|
if (signal?.aborted) throw new LLMAbortError();
|
|
432
432
|
|
|
433
433
|
const body = {
|
|
@@ -439,7 +439,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
439
439
|
|
|
440
440
|
// task-327c: mirror stream()'s thinking injection for side queries.
|
|
441
441
|
const normEffort = normalizeEffort(effort);
|
|
442
|
-
if (thinkingV1Enabled() && normEffort) {
|
|
442
|
+
if ((thinkingV1Enabled() || effortSource === 'user') && normEffort) {
|
|
443
443
|
const cap = getThinkingCapability(model);
|
|
444
444
|
if (cap.supportsThinking && cap.thinkingProtocol === 'anthropic') {
|
|
445
445
|
const budget = thinkingBudgetForEffort(model, normEffort);
|
|
@@ -230,7 +230,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
230
230
|
// ─── Streaming ──────────────────────────────────────────
|
|
231
231
|
|
|
232
232
|
/**
|
|
233
|
-
* @param {{ model: string, system: string, messages: import('./adapter.js').UnifiedMessage[], tools?: import('./adapter.js').UnifiedToolDef[], maxTokens?: number, effort?: 'low'|'medium'|'high'|'max', extraBody?: object, signal?: AbortSignal, onRawExchange?: ({rawRequest, rawResponse}) => void }} params
|
|
233
|
+
* @param {{ model: string, system: string, messages: import('./adapter.js').UnifiedMessage[], tools?: import('./adapter.js').UnifiedToolDef[], maxTokens?: number, effort?: 'low'|'medium'|'high'|'max', effortSource?: 'user'|'auto', extraBody?: object, signal?: AbortSignal, onRawExchange?: ({rawRequest, rawResponse}) => void }} params
|
|
234
234
|
*
|
|
235
235
|
* NOTE on `extraBody`: any keys you spread here are merged verbatim into
|
|
236
236
|
* the wire body and — because the verbatim debug feature is intentionally
|
|
@@ -239,7 +239,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
239
239
|
* `api-key` headers are auto-redacted (see `redactRawRequest` in
|
|
240
240
|
* `adapter.js`); request-body fields are caller-controlled.
|
|
241
241
|
*/
|
|
242
|
-
async *stream({ model, system, messages, tools, maxTokens = 16384, effort, extraBody, signal, onRawExchange }) {
|
|
242
|
+
async *stream({ model, system, messages, tools, maxTokens = 16384, effort, effortSource, extraBody, signal, onRawExchange }) {
|
|
243
243
|
if (signal?.aborted) throw new LLMAbortError();
|
|
244
244
|
|
|
245
245
|
const body = {
|
|
@@ -257,7 +257,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
257
257
|
// registry entry must declare thinkingProtocol === 'openai-reasoning'.
|
|
258
258
|
// Unknown / unsupported models silently drop the field.
|
|
259
259
|
const normEffort = normalizeEffort(effort);
|
|
260
|
-
if (thinkingV1Enabled() && normEffort) {
|
|
260
|
+
if ((thinkingV1Enabled() || effortSource === 'user') && normEffort) {
|
|
261
261
|
const cap = getThinkingCapability(model);
|
|
262
262
|
if (cap.supportsThinking && cap.thinkingProtocol === 'openai-reasoning') {
|
|
263
263
|
const wireEffort = effortForResponses(normEffort);
|
|
@@ -478,7 +478,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
478
478
|
* expose them, mirror the stream() instrumentation. Parity with
|
|
479
479
|
* anthropic.js's `call()`.
|
|
480
480
|
*/
|
|
481
|
-
async call({ model, system, messages, maxTokens = 4096, effort, extraBody, signal }) {
|
|
481
|
+
async call({ model, system, messages, maxTokens = 4096, effort, effortSource, extraBody, signal }) {
|
|
482
482
|
if (signal?.aborted) throw new LLMAbortError();
|
|
483
483
|
|
|
484
484
|
const body = {
|
|
@@ -490,7 +490,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
490
490
|
|
|
491
491
|
// Mirror stream()'s thinking injection for non-streaming side queries.
|
|
492
492
|
const normEffort = normalizeEffort(effort);
|
|
493
|
-
if (thinkingV1Enabled() && normEffort) {
|
|
493
|
+
if ((thinkingV1Enabled() || effortSource === 'user') && normEffort) {
|
|
494
494
|
const cap = getThinkingCapability(model);
|
|
495
495
|
if (cap.supportsThinking && cap.thinkingProtocol === 'openai-reasoning') {
|
|
496
496
|
const wireEffort = effortForResponses(normEffort);
|
package/yeaft/llm/router.js
CHANGED
|
@@ -99,20 +99,22 @@ function thinkingV1Enabled() {
|
|
|
99
99
|
export function filterEffortForModel(params) {
|
|
100
100
|
if (!params || !('effort' in params)) return params;
|
|
101
101
|
if (!thinkingV1Enabled()) {
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
if (params.effortSource !== 'user') {
|
|
103
|
+
const { effort: _drop, effortSource: _source, ...rest } = params;
|
|
104
|
+
return rest;
|
|
105
|
+
}
|
|
104
106
|
}
|
|
105
107
|
const norm = normalizeEffort(params.effort);
|
|
106
108
|
if (!norm) {
|
|
107
|
-
const { effort: _drop, ...rest } = params;
|
|
109
|
+
const { effort: _drop, effortSource: _source, ...rest } = params;
|
|
108
110
|
return rest;
|
|
109
111
|
}
|
|
110
112
|
const cap = getThinkingCapability(parseModelRef(params.model).modelId);
|
|
111
113
|
if (!cap.supportsThinking || cap.thinkingProtocol === 'none') {
|
|
112
|
-
const { effort: _drop, ...rest } = params;
|
|
114
|
+
const { effort: _drop, effortSource: _source, ...rest } = params;
|
|
113
115
|
return rest;
|
|
114
116
|
}
|
|
115
|
-
return { ...params, effort: norm };
|
|
117
|
+
return { ...params, effort: norm, effortSource: params.effortSource };
|
|
116
118
|
}
|
|
117
119
|
|
|
118
120
|
/**
|
package/yeaft/models.js
CHANGED
|
@@ -411,6 +411,27 @@ export function mapEffortToOpenAIReasoning(effort) {
|
|
|
411
411
|
}
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
+
export const MODEL_EFFORT_OPTIONS = ['low', 'medium', 'high'];
|
|
415
|
+
|
|
416
|
+
function inferThinkingCapability(model) {
|
|
417
|
+
const id = parseModelRef(model).modelId.toLowerCase();
|
|
418
|
+
if (!id) return null;
|
|
419
|
+
|
|
420
|
+
if (/^(gpt-5|o1|o3|o4|chatgpt-|codex-)/.test(id)) {
|
|
421
|
+
return { supportsThinking: true, thinkingProtocol: 'openai-reasoning', defaultEffort: null, maxBudgetTokens: null };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Anthropic extended thinking is available on Claude 3.7+ and Claude 4.x.
|
|
425
|
+
// Be conservative: older Claude 3/3.5/Haiku entries stay unsupported unless
|
|
426
|
+
// explicitly listed in the registry.
|
|
427
|
+
if (/^claude-/.test(id) && (/(^|-)3-7($|-|\.)/.test(id) || /(^|-)4($|-|\.)/.test(id))) {
|
|
428
|
+
const maxBudgetTokens = id.includes('opus') ? 64000 : 32000;
|
|
429
|
+
return { supportsThinking: true, thinkingProtocol: 'anthropic', defaultEffort: null, maxBudgetTokens };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
|
|
414
435
|
/**
|
|
415
436
|
* Resolve the Anthropic thinking budget_tokens value for a given (model, effort).
|
|
416
437
|
*
|
|
@@ -443,7 +464,9 @@ export function thinkingBudgetForEffort(model, effort) {
|
|
|
443
464
|
*/
|
|
444
465
|
export function getThinkingCapability(model) {
|
|
445
466
|
const info = MODEL_REGISTRY.get(model);
|
|
446
|
-
|
|
467
|
+
const hasExplicitThinking = info && Object.prototype.hasOwnProperty.call(info, 'supportsThinking');
|
|
468
|
+
const inferred = hasExplicitThinking ? null : inferThinkingCapability(model);
|
|
469
|
+
if ((!info || !info.supportsThinking) && !inferred) {
|
|
447
470
|
return {
|
|
448
471
|
supportsThinking: false,
|
|
449
472
|
thinkingProtocol: 'none',
|
|
@@ -452,13 +475,23 @@ export function getThinkingCapability(model) {
|
|
|
452
475
|
};
|
|
453
476
|
}
|
|
454
477
|
return {
|
|
455
|
-
supportsThinking:
|
|
456
|
-
thinkingProtocol: info
|
|
457
|
-
defaultEffort: info
|
|
458
|
-
maxBudgetTokens: info
|
|
478
|
+
supportsThinking: Boolean(info?.supportsThinking ?? inferred?.supportsThinking),
|
|
479
|
+
thinkingProtocol: info?.thinkingProtocol || inferred?.thinkingProtocol || 'none',
|
|
480
|
+
defaultEffort: info?.defaultEffort ?? inferred?.defaultEffort ?? null,
|
|
481
|
+
maxBudgetTokens: info?.maxBudgetTokens ?? inferred?.maxBudgetTokens ?? null,
|
|
459
482
|
};
|
|
460
483
|
}
|
|
461
484
|
|
|
485
|
+
export function getModelEffortOptions(model) {
|
|
486
|
+
const cap = getThinkingCapability(model);
|
|
487
|
+
if (!cap.supportsThinking || cap.thinkingProtocol === 'none') return [];
|
|
488
|
+
return MODEL_EFFORT_OPTIONS.slice();
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export function modelSupportsEffort(model) {
|
|
492
|
+
return getModelEffortOptions(model).length > 0;
|
|
493
|
+
}
|
|
494
|
+
|
|
462
495
|
/**
|
|
463
496
|
* Valid-effort guard. Unknown values → null (caller should treat as "no effort").
|
|
464
497
|
*
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* v1 schema (intentionally tiny — extend via additive keys only):
|
|
8
8
|
* {
|
|
9
|
-
* "model": "my-proxy/claude-sonnet-4-20250514"
|
|
9
|
+
* "model": "my-proxy/claude-sonnet-4-20250514", // optional
|
|
10
|
+
* "modelEffort": "high" // optional
|
|
10
11
|
* }
|
|
11
12
|
*
|
|
12
13
|
* Missing file or `{}` → no session-level override. Missing field → fall back to user-level
|
|
@@ -26,7 +27,8 @@ import { sessionsRoot, resolveSessionYeaftDir } from './session-crud.js';
|
|
|
26
27
|
const CONFIG_FILE = 'config.json';
|
|
27
28
|
|
|
28
29
|
/** Whitelist of persisted session model-override fields. Reject everything else. */
|
|
29
|
-
const ALLOWED_KEYS = new Set(['model']);
|
|
30
|
+
const ALLOWED_KEYS = new Set(['model', 'modelEffort']);
|
|
31
|
+
const ALLOWED_EFFORTS = new Set(['low', 'medium', 'high']);
|
|
30
32
|
|
|
31
33
|
export class SessionConfigError extends Error {
|
|
32
34
|
constructor(code, message) {
|
|
@@ -97,6 +99,11 @@ export function validateSessionConfig(cfg) {
|
|
|
97
99
|
throw new SessionConfigError('invalid_model', 'model must be a non-empty string');
|
|
98
100
|
}
|
|
99
101
|
}
|
|
102
|
+
if ('modelEffort' in cfg && cfg.modelEffort !== null && cfg.modelEffort !== undefined && cfg.modelEffort !== '') {
|
|
103
|
+
if (typeof cfg.modelEffort !== 'string' || !ALLOWED_EFFORTS.has(cfg.modelEffort.trim())) {
|
|
104
|
+
throw new SessionConfigError('invalid_model_effort', 'modelEffort must be low, medium, or high');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
100
107
|
}
|
|
101
108
|
|
|
102
109
|
/**
|
|
@@ -163,5 +170,10 @@ export function resolveSessionConfig(userConfig, sessionConfig) {
|
|
|
163
170
|
base.model = model;
|
|
164
171
|
base.primaryModel = model;
|
|
165
172
|
}
|
|
173
|
+
if (overrides.modelEffort && typeof overrides.modelEffort === 'string' && ALLOWED_EFFORTS.has(overrides.modelEffort)) {
|
|
174
|
+
base.modelEffort = overrides.modelEffort;
|
|
175
|
+
} else {
|
|
176
|
+
delete base.modelEffort;
|
|
177
|
+
}
|
|
166
178
|
return base;
|
|
167
179
|
}
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -2796,6 +2796,7 @@ async function ensureSessionLoaded() {
|
|
|
2796
2796
|
type: 'session_ready',
|
|
2797
2797
|
conversationId: yeaftConversationId,
|
|
2798
2798
|
model: session.config.model,
|
|
2799
|
+
modelEffort: session.config.modelEffort || null,
|
|
2799
2800
|
availableModels: session.config.availableModels || [],
|
|
2800
2801
|
skills: session.status.skills,
|
|
2801
2802
|
mcpServers: session.status.mcpServers,
|
|
@@ -3920,23 +3921,34 @@ export function handleYeaftModeSwitch(_msg) {
|
|
|
3920
3921
|
}
|
|
3921
3922
|
|
|
3922
3923
|
|
|
3924
|
+
|
|
3925
|
+
export function modelRefMatchesAvailable(model, requested) {
|
|
3926
|
+
if (!model || !requested) return false;
|
|
3927
|
+
return model.id === requested
|
|
3928
|
+
|| model.ref === requested
|
|
3929
|
+
|| (model.provider && model.id && `${model.provider}/${model.id}` === requested);
|
|
3930
|
+
}
|
|
3931
|
+
|
|
3923
3932
|
/** Handle model switch from the web UI. */
|
|
3924
3933
|
export function handleYeaftModelSwitch(msg) {
|
|
3925
3934
|
if (!session || !msg.model) return;
|
|
3926
3935
|
refreshLiveSessionConfig();
|
|
3927
3936
|
|
|
3928
3937
|
const available = session.config.availableModels || [];
|
|
3929
|
-
const found = available.some(m => m
|
|
3938
|
+
const found = available.some(m => modelRefMatchesAvailable(m, msg.model));
|
|
3930
3939
|
if (!found) {
|
|
3931
3940
|
console.warn(`[Yeaft] model switch rejected — "${msg.model}" not in availableModels`);
|
|
3932
3941
|
return;
|
|
3933
3942
|
}
|
|
3934
3943
|
|
|
3935
3944
|
session.config.model = msg.model;
|
|
3945
|
+
session.config.primaryModel = msg.model;
|
|
3946
|
+
session.config.modelEffort = msg.modelEffort || null;
|
|
3936
3947
|
|
|
3937
3948
|
sendSessionEvent({
|
|
3938
3949
|
type: 'model_switched',
|
|
3939
3950
|
model: msg.model,
|
|
3951
|
+
modelEffort: session.config.modelEffort || null,
|
|
3940
3952
|
});
|
|
3941
3953
|
}
|
|
3942
3954
|
|
|
@@ -3996,6 +4008,7 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
3996
4008
|
type: 'session_ready',
|
|
3997
4009
|
conversationId: yeaftConversationId,
|
|
3998
4010
|
model: session.config.model,
|
|
4011
|
+
modelEffort: session.config.modelEffort || null,
|
|
3999
4012
|
availableModels: session.config.availableModels || [],
|
|
4000
4013
|
skills: session.status.skills,
|
|
4001
4014
|
mcpServers: session.status.mcpServers,
|