@yeaft/webchat-agent 0.1.464 → 0.1.466

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.464",
3
+ "version": "0.1.466",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -11,6 +11,7 @@
11
11
  import { existsSync, readFileSync, writeFileSync } from 'fs';
12
12
  import { join } from 'path';
13
13
  import { DEFAULT_YEAFT_DIR } from './init.js';
14
+ import { normalizeProviderModels, serializeModelForPersistence } from './models.js';
14
15
 
15
16
  /**
16
17
  * Read the LLM-relevant portion of config.json.
@@ -87,7 +88,17 @@ export function updateLlmConfig(update, dir) {
87
88
  return { error: `Provider "${p.name}" must have at least one model` };
88
89
  }
89
90
  }
90
- existing.providers = update.providers;
91
+ // Normalize + re-serialize each provider's models so that:
92
+ // - id-only entries are persisted as plain strings (back-compat)
93
+ // - entries with ctx / maxOutput are persisted as objects
94
+ // - empty / 0 / NaN values get stripped
95
+ existing.providers = update.providers.map(p => {
96
+ const normalized = normalizeProviderModels(p);
97
+ return {
98
+ ...p,
99
+ models: normalized.map(serializeModelForPersistence),
100
+ };
101
+ });
91
102
  }
92
103
 
93
104
  // Update model selections
package/unify/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 } from './models.js';
25
+ import { resolveModel, parseModelRef, normalizeProviderModels } from './models.js';
26
26
 
27
27
  /** Default configuration values. */
28
28
  const DEFAULTS = {
@@ -260,19 +260,24 @@ export function loadConfig(overrides = {}) {
260
260
  adapter: null,
261
261
  };
262
262
 
263
- // Aggregate all available models from providers
263
+ // Aggregate all available models from providers.
264
+ // Normalizes each provider's `models` into `{ id, contextWindow?, maxOutput? }`
265
+ // so consumers never have to deal with raw string / object ambiguity.
264
266
  config.availableModels = [];
265
267
  if (providers) {
266
268
  for (const p of providers) {
267
- if (!Array.isArray(p.models)) continue;
268
- for (const m of p.models) {
269
+ const normalized = normalizeProviderModels(p);
270
+ for (const m of normalized) {
269
271
  // Avoid duplicates (first provider wins)
270
- if (!config.availableModels.some(am => am.id === m)) {
271
- config.availableModels.push({
272
- id: m,
272
+ if (!config.availableModels.some(am => am.id === m.id)) {
273
+ const entry = {
274
+ id: m.id,
273
275
  provider: p.name,
274
- label: m,
275
- });
276
+ label: m.id,
277
+ };
278
+ if (m.contextWindow !== undefined) entry.contextWindow = m.contextWindow;
279
+ if (m.maxOutput !== undefined) entry.maxOutput = m.maxOutput;
280
+ config.availableModels.push(entry);
276
281
  }
277
282
  }
278
283
  }
package/unify/models.js CHANGED
@@ -59,31 +59,25 @@ export const MODEL_REGISTRY = new Map([
59
59
  maxOutputTokens: 16384,
60
60
  displayName: 'GPT-5',
61
61
  }],
62
+ // gpt-5-mini/-nano/-pro: keep id + family/protocol metadata so they appear
63
+ // as known models, but do NOT hardcode context/maxOutput — the real limits
64
+ // should come from provider config (user-supplied) instead of guesses.
62
65
  ['gpt-5-mini', {
63
66
  provider: 'openai',
64
67
  adapter: 'chat-completions',
65
68
  baseUrl: 'https://api.openai.com/v1',
66
- // TODO: verify exact limits against OpenAI docs on first real call
67
- contextWindow: 400000,
68
- maxOutputTokens: 128000,
69
69
  displayName: 'GPT-5 Mini',
70
70
  }],
71
71
  ['gpt-5-nano', {
72
72
  provider: 'openai',
73
73
  adapter: 'chat-completions',
74
74
  baseUrl: 'https://api.openai.com/v1',
75
- // TODO: verify exact limits against OpenAI docs on first real call
76
- contextWindow: 400000,
77
- maxOutputTokens: 128000,
78
75
  displayName: 'GPT-5 Nano',
79
76
  }],
80
77
  ['gpt-5-pro', {
81
78
  provider: 'openai',
82
79
  adapter: 'chat-completions',
83
80
  baseUrl: 'https://api.openai.com/v1',
84
- // TODO: verify exact limits against OpenAI docs on first real call
85
- contextWindow: 400000,
86
- maxOutputTokens: 128000,
87
81
  displayName: 'GPT-5 Pro',
88
82
  }],
89
83
  ['gpt-5.4', {
@@ -236,3 +230,108 @@ export function parseModelRef(ref) {
236
230
  modelId: ref.slice(slashIdx + 1),
237
231
  };
238
232
  }
233
+
234
+ // ─── task-284: config-driven context / maxOutput ────────────────
235
+
236
+ /**
237
+ * Coerce a possibly-stringy numeric value to a positive integer, or
238
+ * `undefined` if it's empty / zero / NaN / negative / non-finite.
239
+ *
240
+ * @param {*} v
241
+ * @returns {number | undefined}
242
+ */
243
+ function coercePositiveInt(v) {
244
+ if (v === null || v === undefined || v === '') return undefined;
245
+ const n = typeof v === 'number' ? v : Number(v);
246
+ if (!Number.isFinite(n) || n <= 0) return undefined;
247
+ return Math.floor(n);
248
+ }
249
+
250
+ /**
251
+ * Normalize a provider's `models` field into an in-memory array of
252
+ * `{ id, contextWindow?, maxOutput? }` objects.
253
+ *
254
+ * Accepts legacy `string[]` and new object form `{id, contextWindow, maxOutput}`.
255
+ * Empty / 0 / NaN / negative context/max values are treated as unset (dropped).
256
+ *
257
+ * @param {{ models?: Array<string | object> } | null | undefined} provider
258
+ * @returns {Array<{ id: string, contextWindow?: number, maxOutput?: number }>}
259
+ */
260
+ export function normalizeProviderModels(provider) {
261
+ if (!provider || !Array.isArray(provider.models)) return [];
262
+ const out = [];
263
+ for (const entry of provider.models) {
264
+ if (typeof entry === 'string') {
265
+ const id = entry.trim();
266
+ if (id) out.push({ id });
267
+ continue;
268
+ }
269
+ if (entry && typeof entry === 'object' && typeof entry.id === 'string' && entry.id.trim()) {
270
+ const norm = { id: entry.id.trim() };
271
+ const ctx = coercePositiveInt(entry.contextWindow);
272
+ const max = coercePositiveInt(entry.maxOutput);
273
+ if (ctx !== undefined) norm.contextWindow = ctx;
274
+ if (max !== undefined) norm.maxOutput = max;
275
+ out.push(norm);
276
+ }
277
+ // silently skip anything else (null / missing id / numbers)
278
+ }
279
+ return out;
280
+ }
281
+
282
+ /**
283
+ * Serialize a normalized model entry back for persistence.
284
+ * - id-only → plain string (back-compat with existing configs)
285
+ * - with ctx or max → object with only the fields that are set
286
+ *
287
+ * @param {{ id: string, contextWindow?: number, maxOutput?: number }} entry
288
+ * @returns {string | object}
289
+ */
290
+ export function serializeModelForPersistence(entry) {
291
+ if (!entry || typeof entry !== 'object') return entry;
292
+ const ctx = coercePositiveInt(entry.contextWindow);
293
+ const max = coercePositiveInt(entry.maxOutput);
294
+ if (ctx === undefined && max === undefined) return entry.id;
295
+ const obj = { id: entry.id };
296
+ if (ctx !== undefined) obj.contextWindow = ctx;
297
+ if (max !== undefined) obj.maxOutput = max;
298
+ return obj;
299
+ }
300
+
301
+ /**
302
+ * Resolve model info with per-provider override.
303
+ *
304
+ * Lookup order:
305
+ * 1. provider-config fields (contextWindow / maxOutput) — highest priority
306
+ * 2. MODEL_REGISTRY entry (contextWindow / maxOutputTokens)
307
+ * 3. undefined if neither has any info
308
+ *
309
+ * Returns a normalized shape: `{ id, contextWindow?, maxOutput?, provider?, adapter?, baseUrl?, displayName? }`.
310
+ *
311
+ * @param {string} model
312
+ * @param {{ id?: string, contextWindow?: number, maxOutput?: number } | null} [providerConfig]
313
+ * @returns {object | undefined}
314
+ */
315
+ export function getModelInfo(model, providerConfig) {
316
+ if (!model) return undefined;
317
+ const reg = MODEL_REGISTRY.get(model);
318
+ const overrideCtx = coercePositiveInt(providerConfig?.contextWindow);
319
+ const overrideMax = coercePositiveInt(providerConfig?.maxOutput);
320
+
321
+ const ctx = overrideCtx ?? reg?.contextWindow;
322
+ const max = overrideMax ?? reg?.maxOutputTokens;
323
+
324
+ // If we have literally no information, return undefined
325
+ if (!reg && ctx === undefined && max === undefined) return undefined;
326
+
327
+ const info = { id: model };
328
+ if (reg) {
329
+ if (reg.provider) info.provider = reg.provider;
330
+ if (reg.adapter) info.adapter = reg.adapter;
331
+ if (reg.baseUrl) info.baseUrl = reg.baseUrl;
332
+ if (reg.displayName) info.displayName = reg.displayName;
333
+ }
334
+ if (ctx !== undefined) info.contextWindow = ctx;
335
+ if (max !== undefined) info.maxOutput = max;
336
+ return info;
337
+ }