@yeaft/webchat-agent 0.1.998 → 0.1.1000
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 +55 -3
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
|
@@ -1494,15 +1494,54 @@ export function handleYeaftVpRead(msg) {
|
|
|
1494
1494
|
/**
|
|
1495
1495
|
* Session CRUD wired to WS events.
|
|
1496
1496
|
*/
|
|
1497
|
+
function decorateSessionsWithRuntimeState(sessions) {
|
|
1498
|
+
const rows = Array.isArray(sessions) ? sessions : [];
|
|
1499
|
+
if (rows.length === 0) return rows;
|
|
1500
|
+
let statuses = [];
|
|
1501
|
+
try {
|
|
1502
|
+
statuses = getVpStatusBroker().snapshot();
|
|
1503
|
+
} catch {
|
|
1504
|
+
statuses = [];
|
|
1505
|
+
}
|
|
1506
|
+
const bySession = new Map();
|
|
1507
|
+
for (const status of statuses) {
|
|
1508
|
+
const sessionId = status?.sessionId || status?.groupId || null;
|
|
1509
|
+
if (!sessionId) continue;
|
|
1510
|
+
const state = status.state || 'idle';
|
|
1511
|
+
const running = !['idle', 'offline', 'completed', 'failed', 'aborted'].includes(state);
|
|
1512
|
+
const updatedAt = status.updatedAt || status.since || Date.now();
|
|
1513
|
+
const prev = bySession.get(sessionId) || { running: false, runningVpCount: 0, latestActivityAt: 0 };
|
|
1514
|
+
if (running) prev.runningVpCount += 1;
|
|
1515
|
+
prev.running = prev.running || running;
|
|
1516
|
+
prev.latestActivityAt = Math.max(prev.latestActivityAt || 0, updatedAt || 0);
|
|
1517
|
+
bySession.set(sessionId, prev);
|
|
1518
|
+
}
|
|
1519
|
+
return rows.map(session => {
|
|
1520
|
+
if (!session || !session.id) return session;
|
|
1521
|
+
const runtime = bySession.get(session.id);
|
|
1522
|
+
if (!runtime) return { ...session, running: false, active: false };
|
|
1523
|
+
return {
|
|
1524
|
+
...session,
|
|
1525
|
+
running: !!runtime.running,
|
|
1526
|
+
active: !!runtime.running,
|
|
1527
|
+
runningVpCount: runtime.runningVpCount || 0,
|
|
1528
|
+
latestActivityAt: runtime.latestActivityAt || null,
|
|
1529
|
+
};
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1497
1533
|
function sendSessionCrudResult(payload) {
|
|
1498
|
-
|
|
1534
|
+
const next = payload && payload.ok && Array.isArray(payload.sessions)
|
|
1535
|
+
? { ...payload, sessions: decorateSessionsWithRuntimeState(payload.sessions) }
|
|
1536
|
+
: payload;
|
|
1537
|
+
sendSessionEvent({ type: 'session_crud_result', ...next });
|
|
1499
1538
|
}
|
|
1500
1539
|
|
|
1501
1540
|
function sendSessionSnapshotBroadcast() {
|
|
1502
1541
|
try {
|
|
1503
1542
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
1504
1543
|
if (!yeaftDir) return;
|
|
1505
|
-
const sessions = snapshotSessions(yeaftDir);
|
|
1544
|
+
const sessions = decorateSessionsWithRuntimeState(snapshotSessions(yeaftDir));
|
|
1506
1545
|
sendSessionEvent({ type: 'session_list_updated', sessions });
|
|
1507
1546
|
} catch (err) {
|
|
1508
1547
|
console.warn('[Yeaft] sendSessionSnapshotBroadcast failed:', err?.message || err);
|
|
@@ -2796,6 +2835,7 @@ async function ensureSessionLoaded() {
|
|
|
2796
2835
|
type: 'session_ready',
|
|
2797
2836
|
conversationId: yeaftConversationId,
|
|
2798
2837
|
model: session.config.model,
|
|
2838
|
+
modelEffort: session.config.modelEffort || null,
|
|
2799
2839
|
availableModels: session.config.availableModels || [],
|
|
2800
2840
|
skills: session.status.skills,
|
|
2801
2841
|
mcpServers: session.status.mcpServers,
|
|
@@ -3920,23 +3960,34 @@ export function handleYeaftModeSwitch(_msg) {
|
|
|
3920
3960
|
}
|
|
3921
3961
|
|
|
3922
3962
|
|
|
3963
|
+
|
|
3964
|
+
export function modelRefMatchesAvailable(model, requested) {
|
|
3965
|
+
if (!model || !requested) return false;
|
|
3966
|
+
return model.id === requested
|
|
3967
|
+
|| model.ref === requested
|
|
3968
|
+
|| (model.provider && model.id && `${model.provider}/${model.id}` === requested);
|
|
3969
|
+
}
|
|
3970
|
+
|
|
3923
3971
|
/** Handle model switch from the web UI. */
|
|
3924
3972
|
export function handleYeaftModelSwitch(msg) {
|
|
3925
3973
|
if (!session || !msg.model) return;
|
|
3926
3974
|
refreshLiveSessionConfig();
|
|
3927
3975
|
|
|
3928
3976
|
const available = session.config.availableModels || [];
|
|
3929
|
-
const found = available.some(m => m
|
|
3977
|
+
const found = available.some(m => modelRefMatchesAvailable(m, msg.model));
|
|
3930
3978
|
if (!found) {
|
|
3931
3979
|
console.warn(`[Yeaft] model switch rejected — "${msg.model}" not in availableModels`);
|
|
3932
3980
|
return;
|
|
3933
3981
|
}
|
|
3934
3982
|
|
|
3935
3983
|
session.config.model = msg.model;
|
|
3984
|
+
session.config.primaryModel = msg.model;
|
|
3985
|
+
session.config.modelEffort = msg.modelEffort || null;
|
|
3936
3986
|
|
|
3937
3987
|
sendSessionEvent({
|
|
3938
3988
|
type: 'model_switched',
|
|
3939
3989
|
model: msg.model,
|
|
3990
|
+
modelEffort: session.config.modelEffort || null,
|
|
3940
3991
|
});
|
|
3941
3992
|
}
|
|
3942
3993
|
|
|
@@ -3996,6 +4047,7 @@ export async function handleYeaftLoadHistory(msg) {
|
|
|
3996
4047
|
type: 'session_ready',
|
|
3997
4048
|
conversationId: yeaftConversationId,
|
|
3998
4049
|
model: session.config.model,
|
|
4050
|
+
modelEffort: session.config.modelEffort || null,
|
|
3999
4051
|
availableModels: session.config.availableModels || [],
|
|
4000
4052
|
skills: session.status.skills,
|
|
4001
4053
|
mcpServers: session.status.mcpServers,
|