mobygate 0.8.2 → 0.8.3

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/CHANGELOG.md CHANGED
@@ -4,6 +4,81 @@ All notable changes to mobygate are documented here. Format loosely follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version numbers are
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.8.3] — 2026-04-28
8
+
9
+ OpenClaw .26 compatibility, sonnet 1M context, and observability for
10
+ silent model swaps. Found by upgrading mobygate to v0.8.1 on Windows
11
+ during heavy multi-agent testing.
12
+
13
+ ### Fixed
14
+
15
+ - **OpenClaw connector wrote deprecated `models.main` / `models.default`
16
+ keys**, which OpenClaw `2026.4.26+` outright rejects with `Unrecognized
17
+ keys: "main", "default"`. The gateway refused to start until
18
+ `openclaw doctor --fix` cleaned them up.
19
+
20
+ v0.8.3 connector now writes the modern path:
21
+ - `agents.defaults.model.primary` (the active default)
22
+ - `agents.defaults.models` (registered model id map)
23
+
24
+ Plus on every `plan()` it removes the deprecated top-level keys if a
25
+ previous mobygate version (v0.8.0–v0.8.2) wrote them. Re-running
26
+ `mobygate connect openclaw` after upgrading will repair any broken
27
+ config in place.
28
+
29
+ Why this didn't bite Mac users: hand-rolled configs that never went
30
+ through `mobygate connect`'s apply path were never written by the
31
+ bad code. It surfaced on Windows when `mobygate init` auto-wired
32
+ detected clients during a fresh upgrade.
33
+
34
+ - **Sonnet 4.6 silently capped at 200k context.** v0.8.2 routed
35
+ `claude-sonnet-4-6` through directly but didn't append the `[1m]`
36
+ suffix the way opus 4.7 does, so the SDK ran sonnet at the default
37
+ 200k window. Confirmed via `modelUsage.contextWindow=200000` in the
38
+ diagnostic logging.
39
+
40
+ Fix: `claude-sonnet-4-6` now maps to `claude-sonnet-4-6[1m]`. Verified
41
+ next-turn `modelUsage` shows `contextWindow: 1000000`. Matches the
42
+ capability we already advertise in `/v1/models`.
43
+
44
+ Side note: `claude-sonnet-4-6-200k` added as an explicit alias for
45
+ callers that want the cheaper 200k variant.
46
+
47
+ ### Added
48
+
49
+ - **`[model-billed]` diagnostic log line.** Every successful (non-tool-
50
+ use-aborted) request now logs the SDK's `modelUsage` map showing
51
+ what model Anthropic actually billed against. Output looks like:
52
+
53
+ ```
54
+ [model-billed] requested=claude-sonnet-4-6[1m]
55
+ modelUsage={"claude-sonnet-4-6[1m]":{"inputTokens":3,"costUSD":0.029,
56
+ "contextWindow":1000000,...}}
57
+ ```
58
+
59
+ This was originally added as a temporary diagnostic to chase whether
60
+ Anthropic was silently swapping sonnet → opus. The data confirmed
61
+ no swap is happening — but the line is too useful to remove. Surfaces
62
+ cost-per-turn, actual context window, and any future silent model
63
+ changes the SDK might introduce. Low log volume (one line per
64
+ result message, ~50% of requests since tool_use turns abort
65
+ before result).
66
+
67
+ Bonus finding from this log: the SDK transparently uses **two models
68
+ per opus turn** — a haiku for fast intermediate work and opus for
69
+ the final response. Adds ~$0.024 per opus turn beyond what the
70
+ capture summary shows. Worth knowing for cost analysis.
71
+
72
+ ### Notes
73
+
74
+ The "claude.ai/settings/usage Sonnet only stuck at 0%" mystery turned
75
+ out to be a Claude Max plan accounting design: the "Sonnet only" bar
76
+ is overflow that only ticks once "All models" is exhausted. On Max
77
+ plans, sonnet usage rolls into the "All models" bar (which was
78
+ climbing as expected). Not a mobygate bug — investigation surfaced
79
+ the v0.8.2 [1m] context omission, which IS a mobygate bug, so net
80
+ positive on the chase.
81
+
7
82
  ## [0.8.2] — 2026-04-28
8
83
 
9
84
  Multi-agent fixes. Found the day after v0.8.1 shipped, while testing
package/lib/anthropic.js CHANGED
@@ -36,17 +36,23 @@ import { v4 as uuidv4 } from 'uuid';
36
36
  * against shape variations (the Claude Agent SDK sometimes nests these
37
37
  * under `.usage`, sometimes places them flat on the message). Returns
38
38
  * a complete usage shape with cache_read / cache_creation fields zeroed
39
- * out if absent. Used by the 4 mobygate handlers to populate response
40
- * captures and dashboard cache-hit metrics.
39
+ * out if absent, plus `modelUsage` (the SDK's per-model usage breakdown,
40
+ * keyed by the actual model name Anthropic billed against — useful for
41
+ * spotting silent model fallbacks where the requested model differs
42
+ * from what Anthropic actually ran).
43
+ *
44
+ * Used by the 4 mobygate handlers to populate response captures and
45
+ * dashboard cache-hit metrics.
41
46
  */
42
47
  export function extractSdkUsage(message) {
43
- if (!message) return { input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 };
48
+ if (!message) return { input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0, cache_creation_input_tokens: 0, modelUsage: null };
44
49
  const u = message.usage || message;
45
50
  return {
46
51
  input_tokens: u.input_tokens || 0,
47
52
  output_tokens: u.output_tokens || 0,
48
53
  cache_read_input_tokens: u.cache_read_input_tokens || 0,
49
54
  cache_creation_input_tokens: u.cache_creation_input_tokens || 0,
55
+ modelUsage: message.modelUsage || null,
50
56
  };
51
57
  }
52
58
 
@@ -169,8 +169,12 @@ export const openclawConnector = {
169
169
  configPath: det.configPath,
170
170
  mobyProviderExists: !!providers[PROVIDER_NAME_OPENAI],
171
171
  mobyNativeProviderExists: !!providers[PROVIDER_NAME_ANTHROPIC],
172
- currentMain: det.parsed?.models?.main || null,
173
- currentDefault: det.parsed?.models?.default || null,
172
+ currentPrimary: det.parsed?.agents?.defaults?.model?.primary || null,
173
+ // Old keys: still surfaced so the user/dashboard can spot if a
174
+ // pre-v0.8.3 connector run left these around (they're invalid in
175
+ // OpenClaw .26+). A non-null value here is a "needs reconnect" hint.
176
+ deprecatedMain: det.parsed?.models?.main || null,
177
+ deprecatedDefault: det.parsed?.models?.default || null,
174
178
  shadowProviders, // pre-v0.8.0 entries pointing at our base URL
175
179
  };
176
180
  },
@@ -212,14 +216,52 @@ export const openclawConnector = {
212
216
  : null;
213
217
  if (preferredProvider) {
214
218
  const target = `${preferredProvider}/claude-opus-4-7`;
215
- after.models.main = target;
216
- after.models.default = target;
219
+
220
+ // Modern OpenClaw schema (>=2026.4.x): defaults live under
221
+ // `agents.defaults.model.primary` and `agents.defaults.models`.
222
+ // The old `models.main` / `models.default` top-level keys were
223
+ // valid in earlier OpenClaw versions but `.26+` rejects them
224
+ // ("Unrecognized keys: main, default"). v0.8.0/0.8.1 connector
225
+ // wrote the old shape and broke OpenClaw `.26` startup. Fixed
226
+ // in v0.8.3 by switching to the modern path + cleaning up any
227
+ // old keys it previously wrote.
228
+ if (!after.agents) after.agents = {};
229
+ if (!after.agents.defaults) after.agents.defaults = {};
230
+ if (!after.agents.defaults.model) after.agents.defaults.model = {};
231
+ if (!after.agents.defaults.models) after.agents.defaults.models = {};
232
+
233
+ after.agents.defaults.model.primary = target;
234
+ // Register every model we surface in the provider so OpenClaw
235
+ // sees them in agents-list. Keep existing entries to preserve
236
+ // user-registered models (ollama, anthropic-direct, etc).
237
+ for (const m of MODELS_NATIVE_SURFACE) {
238
+ const id = `${preferredProvider}/${m.id}`;
239
+ if (!after.agents.defaults.models[id]) {
240
+ after.agents.defaults.models[id] = {};
241
+ }
242
+ }
243
+
244
+ // Cleanup: remove the deprecated top-level keys if a previous
245
+ // connector run (v0.8.0/0.8.1) wrote them. OpenClaw `.26`
246
+ // rejects the config outright if these are present.
247
+ if (after.models?.main !== undefined) delete after.models.main;
248
+ if (after.models?.default !== undefined) delete after.models.default;
217
249
  }
218
250
  }
219
251
 
220
252
  const summary = diffSummary(
221
- { providers: before.models?.providers, main: before.models?.main, default: before.models?.default },
222
- { providers: after.models.providers, main: after.models.main, default: after.models.default },
253
+ {
254
+ providers: before.models?.providers,
255
+ primary: before.agents?.defaults?.model?.primary,
256
+ deprecatedMain: before.models?.main,
257
+ deprecatedDefault: before.models?.default,
258
+ },
259
+ {
260
+ providers: after.models.providers,
261
+ primary: after.agents?.defaults?.model?.primary,
262
+ deprecatedMain: after.models?.main, // should be undefined post-fix
263
+ deprecatedDefault: after.models?.default, // should be undefined post-fix
264
+ },
223
265
  );
224
266
 
225
267
  return {
@@ -263,14 +305,32 @@ export const openclawConnector = {
263
305
  if (providers[name]) { delete providers[name]; changed = true; }
264
306
  }
265
307
  }
266
- // If main/default was pointing at us, blank them — let the user
267
- // re-pick rather than guess at a replacement.
268
- if (isMobyDefaultPointer(after.models?.main)) {
269
- after.models.main = null;
308
+ // Modern path: if the agents.defaults.model.primary points at us,
309
+ // blank it out let OpenClaw fall back to whatever default the
310
+ // user has configured otherwise.
311
+ const primary = after.agents?.defaults?.model?.primary;
312
+ if (isMobyDefaultPointer(primary)) {
313
+ after.agents.defaults.model.primary = null;
314
+ changed = true;
315
+ }
316
+ // Remove our model registrations from agents.defaults.models.
317
+ if (after.agents?.defaults?.models) {
318
+ for (const key of Object.keys(after.agents.defaults.models)) {
319
+ if (isMobyDefaultPointer(key)) {
320
+ delete after.agents.defaults.models[key];
321
+ changed = true;
322
+ }
323
+ }
324
+ }
325
+ // Legacy cleanup: remove deprecated top-level main/default if present
326
+ // (left over from v0.8.0/0.8.1 connector). OpenClaw .26 rejects them
327
+ // outright, so removing on disconnect is the right move.
328
+ if (after.models?.main !== undefined) {
329
+ delete after.models.main;
270
330
  changed = true;
271
331
  }
272
- if (isMobyDefaultPointer(after.models?.default)) {
273
- after.models.default = null;
332
+ if (after.models?.default !== undefined) {
333
+ delete after.models.default;
274
334
  changed = true;
275
335
  }
276
336
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobygate",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "OpenAI-compatible local proxy for Claude Max. The Möbius-strip gateway: OpenAI shape in, Claude Max out.",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -186,13 +186,14 @@ const MODEL_MAP = {
186
186
  'claude-opus-4-7[1m]': 'claude-opus-4-7[1m]',
187
187
  'claude-opus-4-7-1m': 'claude-opus-4-7[1m]',
188
188
  'claude-opus-4-7-200k': 'claude-opus-4-7',
189
- 'claude-sonnet-4': 'claude-sonnet-4-6', // current latest sonnet
190
- 'claude-sonnet-4-5': 'claude-sonnet-4-5-20250929', // explicit request for older 4-5
191
- 'claude-sonnet-4-6': 'claude-sonnet-4-6', // SDK now supports natively; was retired-mapped before v0.8.2
189
+ 'claude-sonnet-4': 'claude-sonnet-4-6[1m]', // current latest sonnet, 1M context
190
+ 'claude-sonnet-4-5': 'claude-sonnet-4-5-20250929', // explicit request for older 4-5
191
+ 'claude-sonnet-4-6': 'claude-sonnet-4-6[1m]', // SDK supports natively; [1m] unlocks 1M context (same pattern as opus 4.7)
192
+ 'claude-sonnet-4-6-200k': 'claude-sonnet-4-6', // explicit 200k variant
192
193
  'claude-haiku-4': 'claude-haiku-4-5-20251001',
193
194
  'claude-haiku-4-5': 'claude-haiku-4-5-20251001',
194
195
  'opus': 'claude-opus-4-7[1m]',
195
- 'sonnet': 'claude-sonnet-4-6', // current latest sonnet
196
+ 'sonnet': 'claude-sonnet-4-6[1m]', // current latest sonnet, 1M context
196
197
  'haiku': 'claude-haiku-4-5-20251001',
197
198
  };
198
199
 
@@ -590,6 +591,7 @@ async function handleStreaming(req, res, body, requestId, sessionKey) {
590
591
  outputTokens = usage.output_tokens;
591
592
  cacheReadTokens = usage.cache_read_input_tokens;
592
593
  cacheCreateTokens = usage.cache_creation_input_tokens;
594
+ console.log(` [model-billed] requested=${resolvedModel} modelUsage=${JSON.stringify(usage.modelUsage || '(none)')}`);
593
595
  break;
594
596
  }
595
597
  }
@@ -795,6 +797,7 @@ async function handleNonStreaming(res, body, requestId, sessionKey) {
795
797
  outputTokens = usage.output_tokens;
796
798
  cacheReadTokens = usage.cache_read_input_tokens;
797
799
  cacheCreateTokens = usage.cache_creation_input_tokens;
800
+ console.log(` [model-billed] requested=${resolvedModel} modelUsage=${JSON.stringify(usage.modelUsage || '(none)')}`);
798
801
  if (message.subtype) stopReason = message.subtype;
799
802
  break;
800
803
  }
@@ -1009,6 +1012,7 @@ async function handleAnthropicNonStreaming(res, body, requestId, sessionKey) {
1009
1012
  outputTokens = usage.output_tokens;
1010
1013
  cacheReadTokens = usage.cache_read_input_tokens;
1011
1014
  cacheCreateTokens = usage.cache_creation_input_tokens;
1015
+ console.log(` [model-billed] requested=${resolvedModel} modelUsage=${JSON.stringify(usage.modelUsage || '(none)')}`);
1012
1016
  stopReason = mapStopReason(message);
1013
1017
  break;
1014
1018
  }
@@ -1254,6 +1258,7 @@ async function handleAnthropicStreaming(req, res, body, requestId, sessionKey) {
1254
1258
  outputTokens = usage.output_tokens;
1255
1259
  cacheReadTokens = usage.cache_read_input_tokens;
1256
1260
  cacheCreateTokens = usage.cache_creation_input_tokens;
1261
+ console.log(` [model-billed] requested=${resolvedModel} modelUsage=${JSON.stringify(usage.modelUsage || '(none)')}`);
1257
1262
  if (!toolUseEmitted) stopReason = mapStopReason(message);
1258
1263
  break;
1259
1264
  }