@bitkyc08/opencodex 2.40.0 → 2.42.0

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.
Files changed (97) hide show
  1. package/README.md +4 -0
  2. package/gui/dist/assets/index-BU1tE0sr.js +112 -0
  3. package/gui/dist/assets/index-DL9-iS6J.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/meta.svg +1 -0
  6. package/package.json +4 -3
  7. package/src/adapters/cursor/catalog.ts +71 -29
  8. package/src/adapters/cursor/claude-id.ts +76 -0
  9. package/src/adapters/cursor/discovery.ts +16 -3
  10. package/src/adapters/cursor/effort-map.ts +27 -12
  11. package/src/adapters/cursor/protobuf-request.ts +41 -21
  12. package/src/adapters/google.ts +39 -2
  13. package/src/adapters/identity.ts +8 -2
  14. package/src/adapters/openai-responses.ts +57 -4
  15. package/src/bridge.ts +25 -3
  16. package/src/cli/account-auth.ts +28 -3
  17. package/src/cli/account-extended.ts +7 -1
  18. package/src/cli/capabilities.ts +2 -2
  19. package/src/cli/claude.ts +11 -2
  20. package/src/cli/connect.ts +7 -1
  21. package/src/cli/observe.ts +3 -1
  22. package/src/cli/registry.ts +1 -1
  23. package/src/cli/status.ts +19 -4
  24. package/src/client/connect.ts +5 -1
  25. package/src/client/hub-client.ts +29 -5
  26. package/src/clients/config-export.ts +12 -2
  27. package/src/codex/auth-api.ts +102 -9
  28. package/src/codex/catalog/aggregation.ts +8 -0
  29. package/src/codex/catalog/effort.ts +15 -2
  30. package/src/codex/catalog/metadata.ts +119 -9
  31. package/src/codex/catalog/native-models.ts +71 -0
  32. package/src/codex/catalog/parsing.ts +5 -3
  33. package/src/codex/catalog/provider-fetch.ts +166 -28
  34. package/src/codex/catalog.ts +1 -1
  35. package/src/codex/convergence-types.ts +1 -0
  36. package/src/codex/data/upstream-models.json +169 -0
  37. package/src/codex/desired-state.ts +18 -11
  38. package/src/codex/inject.ts +96 -6
  39. package/src/codex/injected-marker.ts +30 -4
  40. package/src/codex/journal.ts +14 -0
  41. package/src/combos/failover.ts +185 -6
  42. package/src/combos/index.ts +6 -0
  43. package/src/combos/resolve.ts +43 -6
  44. package/src/config.ts +5 -1
  45. package/src/generated/compatibility-version.json +115 -83
  46. package/src/generated/model-metadata.ts +1 -1
  47. package/src/grok/sync.ts +10 -2
  48. package/src/integrations/cursor-effort-table.ts +143 -0
  49. package/src/integrations/state.ts +1 -1
  50. package/src/integrations/writer.ts +2 -2
  51. package/src/lib/app-owned-memory-stores.ts +27 -8
  52. package/src/lib/bounded-body.ts +16 -1
  53. package/src/oauth/account-quota-rank.ts +40 -1
  54. package/src/oauth/chatgpt-device.ts +187 -0
  55. package/src/oauth/chatgpt.ts +31 -4
  56. package/src/oauth/generic-account-failover.ts +2 -2
  57. package/src/oauth/index.ts +24 -3
  58. package/src/oauth/log.ts +3 -0
  59. package/src/oauth/meta-muse.ts +235 -0
  60. package/src/providers/antigravity-models.ts +71 -13
  61. package/src/providers/command-code-efforts.ts +15 -0
  62. package/src/providers/free-directory.ts +4 -1
  63. package/src/providers/muse-subscription-usage.ts +95 -0
  64. package/src/providers/quota.ts +96 -0
  65. package/src/providers/registry.ts +116 -8
  66. package/src/responses/code-mode-helper-compat.ts +4 -1
  67. package/src/responses/state.ts +5 -4
  68. package/src/server/auth-cors.ts +241 -56
  69. package/src/server/chat-completions.ts +11 -2
  70. package/src/server/chat-native.ts +30 -4
  71. package/src/server/claude-messages.ts +17 -3
  72. package/src/server/effort-row.ts +131 -0
  73. package/src/server/index.ts +82 -45
  74. package/src/server/live.ts +18 -4
  75. package/src/server/management/api-key-rotation.ts +2 -1
  76. package/src/server/management/api-key-usage.ts +97 -43
  77. package/src/server/management/context.ts +3 -0
  78. package/src/server/management/cursor-integration-routes.ts +36 -7
  79. package/src/server/management/logs-usage-routes.ts +64 -87
  80. package/src/server/management/oauth-account-routes.ts +10 -3
  81. package/src/server/management/provider-routes.ts +218 -1
  82. package/src/server/management/route-registry.ts +1 -0
  83. package/src/server/management/usage-aggregate-cache.ts +464 -0
  84. package/src/server/management/usage-summary-cache.ts +4 -0
  85. package/src/server/models-capabilities.ts +60 -5
  86. package/src/server/responses/core.ts +95 -7
  87. package/src/server/responses/empty-completion-guard.ts +4 -0
  88. package/src/types/config.ts +10 -1
  89. package/src/types/request.ts +8 -0
  90. package/src/types/tools.ts +12 -9
  91. package/src/usage/expected-prices.ts +43 -7
  92. package/src/usage/ledger-scanner.ts +448 -0
  93. package/src/usage/log.ts +1 -1
  94. package/src/usage/summary.ts +915 -655
  95. package/src/web-search/index.ts +1 -1
  96. package/gui/dist/assets/index-BHe2rl_C.js +0 -112
  97. package/gui/dist/assets/index-CJSb3HPe.css +0 -1
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Meta's subscription-usage SSE frame.
3
+ *
4
+ * Meta publishes no quota endpoint — 17 plausible REST paths were probed and every one
5
+ * 404s, and no `x-ratelimit-*` header appears on any of three measured request shapes
6
+ * (devlog/_plan/260903_muse_spark_plan_oauth/003 §E). The only machine-readable usage
7
+ * Meta emits arrives mid-stream, as one extra event alongside the ordinary
8
+ * `response.*` sequence on a streaming `POST /v1/responses`.
9
+ *
10
+ * That inverts the usual seam: this module is fed by the request path, not by a probe,
11
+ * and nothing can refresh its output on demand — obtaining a newer value would mean
12
+ * spending a real inference turn.
13
+ *
14
+ * Measured payload (2026-09-03):
15
+ *
16
+ * ```json
17
+ * { "type": "response.subscription_usage",
18
+ * "subscription": {
19
+ * "tier": "27681393394859588",
20
+ * "window": { "used_percent": 0, "resets_at": 1788431188, "window_duration_mins": 300 },
21
+ * "weekly": { "used_percent": 0, "resets_at": 1788739200 } } }
22
+ * ```
23
+ */
24
+ import { asRecord, normalizePercent, normalizeResetAt, toFiniteNumber } from "./quota-wire";
25
+ import type { ProviderQuota, ProviderQuotaWindow } from "./quota-types";
26
+
27
+ /** The SSE frame type Meta emits on streaming turns. */
28
+ export const MUSE_SUBSCRIPTION_USAGE_TYPE = "response.subscription_usage";
29
+
30
+ /** Meta's five-hour window, identified by its declared duration rather than assumed. */
31
+ const FIVE_HOUR_WINDOW_MINS = 300;
32
+
33
+ /** True when a parsed SSE payload is the subscription-usage frame. */
34
+ export function isMuseSubscriptionUsagePayload(payload: unknown): boolean {
35
+ return asRecord(payload)?.type === MUSE_SUBSCRIPTION_USAGE_TYPE;
36
+ }
37
+
38
+ /**
39
+ * Translate the frame into a `ProviderQuota`.
40
+ *
41
+ * Returns null — never throws — for anything unrecognizable. This runs inside SSE
42
+ * inspection on a live request, where the only acceptable failure is silence: a parse
43
+ * error must not cost the user their turn.
44
+ *
45
+ * `subscription.tier` is deliberately dropped. It is an opaque numeric id, not the plan
46
+ * label the Muse CLI prints, so surfacing it would show a meaningless number.
47
+ */
48
+ export function parseMuseSubscriptionUsage(payload: unknown): ProviderQuota | null {
49
+ const subscription = asRecord(asRecord(payload)?.subscription);
50
+ if (!subscription) return null;
51
+
52
+ const quota: ProviderQuota = { updatedAt: Date.now() };
53
+ let sawWindow = false;
54
+
55
+ const window = asRecord(subscription.window);
56
+ if (window) {
57
+ const percent = normalizePercent(window.used_percent);
58
+ const resetAt = normalizeResetAt(window.resets_at);
59
+ const durationMins = toFiniteNumber(window.window_duration_mins);
60
+ if (percent !== undefined) {
61
+ if (durationMins === FIVE_HOUR_WINDOW_MINS) {
62
+ quota.fiveHourPercent = percent;
63
+ if (resetAt !== undefined) quota.fiveHourResetAt = resetAt;
64
+ sawWindow = true;
65
+ } else {
66
+ // A window of some other length is NOT forced into the five-hour slot: filing a
67
+ // ten-hour window there would understate usage by the ratio of the two windows,
68
+ // and would do so with full confidence. Carry it with its real duration instead.
69
+ const custom: ProviderQuotaWindow = {
70
+ label: durationMins === undefined ? "subscription" : `${durationMins}m`,
71
+ percent,
72
+ ...(resetAt !== undefined ? { resetAt } : {}),
73
+ };
74
+ quota.customWindows = [...(quota.customWindows ?? []), custom];
75
+ sawWindow = true;
76
+ }
77
+ }
78
+ }
79
+
80
+ const weekly = asRecord(subscription.weekly);
81
+ if (weekly) {
82
+ const percent = normalizePercent(weekly.used_percent);
83
+ if (percent !== undefined) {
84
+ quota.weeklyPercent = percent;
85
+ const resetAt = normalizeResetAt(weekly.resets_at);
86
+ if (resetAt !== undefined) quota.weeklyResetAt = resetAt;
87
+ sawWindow = true;
88
+ }
89
+ }
90
+
91
+ // Either window may be absent independently, but a payload carrying neither says
92
+ // nothing — returning a bare `updatedAt` would publish an empty row that the GUI
93
+ // would render as a quota with no bars.
94
+ return sawWindow ? quota : null;
95
+ }
@@ -1409,6 +1409,27 @@ async function fetchKiroQuota(provider: string): Promise<ProviderQuotaReport | n
1409
1409
  return report(provider, "kiro:usage-limits", snapshot.quota);
1410
1410
  }
1411
1411
 
1412
+ /**
1413
+ * Provider-level row for a passive provider: the ACTIVE account's last observed
1414
+ * subscription windows, the same shape `fetchAnthropicQuota` and `fetchKiroQuota`
1415
+ * return.
1416
+ *
1417
+ * Cache-only. A dashboard load or `ocx account refresh` must never spend an inference
1418
+ * turn, so `forceRefresh` does not exist on this path — there is nothing to refresh.
1419
+ * `report.updatedAt` is the observation time, which is what both GUI surfaces render
1420
+ * as the relative age of the row.
1421
+ */
1422
+ async function fetchPassiveProviderQuota(provider: string): Promise<ProviderQuotaReport | null> {
1423
+ const activeId = getAccountSet(provider)?.activeAccountId;
1424
+ if (!activeId) return null;
1425
+ // Idempotent; without it a proxy restart shows nothing until the next streaming turn
1426
+ // even though the last observation is on disk.
1427
+ hydrateAccountQuotaCache();
1428
+ const entry = accountQuotaCache.get(accountCacheKey(provider, activeId));
1429
+ if (!entry?.quota) return null;
1430
+ return report(provider, `${provider}:subscription-observation`, entry.quota);
1431
+ }
1432
+
1412
1433
  // ---------------------------------------------------------------------------
1413
1434
  // Per-account quota (multiauth)
1414
1435
  // ---------------------------------------------------------------------------
@@ -1505,6 +1526,78 @@ export function setCachedProviderAccountQuotaForTests(
1505
1526
  accountQuotaCache.set(key, { ts: Date.now(), quota });
1506
1527
  }
1507
1528
 
1529
+ /**
1530
+ * Providers whose per-account quota is OBSERVED in-band, never probed.
1531
+ *
1532
+ * Deliberately separate from `supportsPerAccountQuota` rather than folded into it. That
1533
+ * predicate gates `fetchAccountQuota`, whose fallback branch sends any
1534
+ * non-Kiro/non-Antigravity bearer to Anthropic's usage endpoint — so adding `meta-muse`
1535
+ * there without a dedicated branch would ship a Meta credential to Anthropic. And even
1536
+ * with a branch it would be the wrong predicate: it means "this provider can be probed",
1537
+ * and Meta publishes no quota endpoint to probe.
1538
+ */
1539
+ export function hasPassiveAccountQuota(provider: string): boolean {
1540
+ return provider === "meta-muse";
1541
+ }
1542
+
1543
+ /**
1544
+ * Record a quota observed in-band on a streaming turn.
1545
+ *
1546
+ * The CALLER captures `writerGeneration` when it resolves the serving credential, not
1547
+ * this function at write time. A streaming turn is a long await, and a generation
1548
+ * captured immediately before the write cannot see a config or account change that
1549
+ * happened EARLIER in the same turn — which is exactly the case the fence exists for.
1550
+ */
1551
+ export function recordPassiveAccountQuota(
1552
+ provider: string,
1553
+ accountId: string,
1554
+ quota: ProviderQuota,
1555
+ writerGeneration: number,
1556
+ ): void {
1557
+ if (!hasPassiveAccountQuota(provider) || !accountId) return;
1558
+ const key = accountCacheKey(provider, accountId);
1559
+ if (!mayCommitAccountQuotaKey(key, writerGeneration)) return;
1560
+ // Hydrate BEFORE writing, not only on the read path. `persistAccountQuotaCache`
1561
+ // serializes the whole in-memory map, so a passive write that lands before anything
1562
+ // has read the cache would persist this one row and erase every other provider's
1563
+ // saved row -- and `diskHydrated` would then stop any later reader from recovering
1564
+ // them. A probe writer cannot hit this because its own read hydrates first; an
1565
+ // observation arrives unprompted, so it must hydrate itself.
1566
+ hydrateAccountQuotaCache();
1567
+ accountQuotaCache.set(key, { ts: Date.now(), quota });
1568
+ // Persisted so a restart keeps the last observation: with no probe to re-establish it,
1569
+ // a forgotten row stays forgotten until the user happens to run another streaming turn.
1570
+ persistAccountQuotaCache();
1571
+ // sweepExpiredOnWrite is deliberately NOT called. Existing probe writers call it
1572
+ // because they run on a poll; this runs on the request path, where a state sweep does
1573
+ // not belong. Passive rows are still reclaimed by generation reconciliation
1574
+ // (reconcileProviderAccountQuotaRows) and by the disk reader's age bound.
1575
+ }
1576
+
1577
+ /**
1578
+ * Cache-only per-account rows for a passive provider. Never probes, never refreshes.
1579
+ *
1580
+ * An account with no observation is OMITTED rather than returned with `quota: null` and
1581
+ * `unavailable`: that pair means "a probe was attempted and failed", and no probe was
1582
+ * ever attempted here. A user who has not yet run a streaming turn simply has no
1583
+ * measurement, which is not an error state.
1584
+ */
1585
+ export function readPassiveProviderAccountQuotas(provider: string): ProviderAccountQuota[] {
1586
+ if (!hasPassiveAccountQuota(provider)) return [];
1587
+ // Idempotent, and otherwise only reached from probe paths a passive provider never
1588
+ // enters — without it a restart shows nothing until the next streaming turn, even
1589
+ // though the row is sitting on disk.
1590
+ hydrateAccountQuotaCache();
1591
+ const set = getAccountSet(provider);
1592
+ if (!set) return [];
1593
+ const rows: ProviderAccountQuota[] = [];
1594
+ for (const account of set.accounts) {
1595
+ const entry = accountQuotaCache.get(accountCacheKey(provider, account.id));
1596
+ if (entry?.quota) rows.push({ accountId: account.id, quota: entry.quota });
1597
+ }
1598
+ return rows;
1599
+ }
1600
+
1508
1601
  export function sweepExpiredProviderAccountQuotaRows(now = Date.now()): number {
1509
1602
  let removed = 0;
1510
1603
  for (const [key, entry] of accountQuotaCache) {
@@ -2299,6 +2392,9 @@ async function maybeFetchProviderQuota(
2299
2392
  if (provider.authMode === "oauth" && name === "cursor") return fetchCursorQuota(name);
2300
2393
  if (provider.authMode === "oauth" && name === "google-antigravity") return fetchAntigravityQuota(name, provider);
2301
2394
  if (provider.authMode === "oauth" && name === "kiro") return fetchKiroQuota(name);
2395
+ // Passive providers (meta-muse): Meta publishes no quota endpoint, so there is no
2396
+ // probe to run — the row is the active account's last in-band observation.
2397
+ if (provider.authMode === "oauth" && hasPassiveAccountQuota(name)) return fetchPassiveProviderQuota(name);
2302
2398
  // Kimi Code `/usages` accepts OAuth or coding-plan API keys, but only on the canonical
2303
2399
  // host and only for real key auth — forward/local modes carry no credential of ours.
2304
2400
  if (provider.authMode === "oauth" && name === "kimi") return fetchKimiQuota(name, provider);
@@ -435,6 +435,32 @@ const OPENAI_API_GPT56_VIRTUAL_MODELS: Record<string, { wireModelId: string; rea
435
435
  "gpt-5.6-luna-pro": { wireModelId: "gpt-5.6-luna", reasoningMode: "pro" },
436
436
  };
437
437
  const OPENAI_API_GPT56_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
438
+ /*
439
+ * Meta Model API (https://api.meta.ai/v1) — published ladder, deliberately NOT the
440
+ * house set. dev.meta.ai/docs/reasoning lists "none", "minimal", "low", "medium",
441
+ * "high", "xhigh" and then excludes "none" for this family: "not supported by Muse
442
+ * Spark and returns HTTP 400". "max" and "ultra" are absent from the vendor's list
443
+ * entirely, so appending one by family resemblance would invent a wire value.
444
+ *
445
+ * Corroborated on a second surface: an unauthenticated OpenCode Zen probe of
446
+ * muse-spark-1.3-contributor-free (2026-09-03) accepted minimal..xhigh, rejected
447
+ * max/ultra with `unknown variant`, and rejected none with "does not support none
448
+ * with this model".
449
+ */
450
+ const META_MUSE_REASONING_EFFORTS = ["minimal", "low", "medium", "high", "xhigh"];
451
+ /*
452
+ * Identity wire map. `requestToCodexEffort` (src/reasoning-effort.ts) rewrites
453
+ * `minimal` to `low` unless a model-scoped wire map says otherwise, so without this
454
+ * the picker would advertise an effort the wire never sends — and a registry-array
455
+ * assertion would pass while the request body was wrong. Identity because Meta's
456
+ * values ARE the Codex names.
457
+ */
458
+ const META_MUSE_REASONING_EFFORT_MAP: Record<string, string> = Object.fromEntries(
459
+ META_MUSE_REASONING_EFFORTS.map(effort => [effort, effort]),
460
+ );
461
+ /** Both Muse Spark 1.3 tiers publish a 1,048,576-token window (dev.meta.ai/docs/models). */
462
+ const META_MUSE_CONTEXT_WINDOW = 1_048_576;
463
+ const META_MUSE_MODELS = ["muse-spark-1.3", "muse-spark-1.3-contributor"];
438
464
  /**
439
465
  * Daybreak program aliases. These `-latest` ids are the stable contract: OpenAI repoints
440
466
  * them at newer snapshots over time (red -> gpt-5.6-cyber, blue -> gpt-5.6-sol as of
@@ -552,6 +578,8 @@ const COMMAND_CODE_IMAGE_MODELS = [
552
578
  "gpt-5.6-sol",
553
579
  "MiniMaxAI/MiniMax-M3",
554
580
  "moonshotai/Kimi-K3",
581
+ "meta/muse-spark-1.3",
582
+ "meta/muse-spark-1.3-contributor",
555
583
  "meta/muse-spark-1.2",
556
584
  "meta/muse-spark-1.2-contributor",
557
585
  ] as const;
@@ -1446,6 +1474,74 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1446
1474
  },
1447
1475
  virtualModels: OPENAI_API_GPT56_VIRTUAL_MODELS,
1448
1476
  },
1477
+ /* [Decision Log]
1478
+ - 목적과 의도: Reach Meta's Muse Spark models directly on Meta's own Model API, instead of only through the Command Code and OpenCode Zen resellers already in this registry.
1479
+ - 기존 구현 및 제약 조건: Meta publishes both POST /v1/responses and POST /v1/chat/completions at https://api.meta.ai/v1, and no API key was issued for this change — every value here comes from the published spec (devlog/_plan/260903_muse_spark_plan_oauth/001).
1480
+ - 검토한 주요 대안: register as openai-chat; use provider id "meta"; enable live discovery; wire the Muse Code subscription credential as OAuth.
1481
+ - 선택한 방식: an openai-responses key provider under the id "meta-model", with a static two-model roster and no OAuth.
1482
+ - 다른 대안 대신 이 방식을 선택한 이유: Meta calls Responses "the recommended default for new work ... OpenAI-compatible and exposes the full feature set", carrying reasoning replay and native input_image that Chat would forfeit. The id is "meta-model" because "meta" would capture the LIVE Command Code selector meta/muse-spark-1.3 at router.ts's provider-prefix branch, and would derive META_API_KEY — the Muse Code CLI's variable, not this API's MODEL_API_KEY.
1483
+ - 장점, 단점 및 영향: users reach Muse Spark without a reseller; discovery stays off until an authenticated /v1/models payload is actually observed, so an unseen roster (Meta also serves image and voice families here) cannot leak into the picker.
1484
+ */
1485
+ {
1486
+ id: "meta-model",
1487
+ label: "Meta Model API",
1488
+ adapter: "openai-responses",
1489
+ baseUrl: "https://api.meta.ai/v1",
1490
+ authKind: "key",
1491
+ dashboardUrl: "https://dev.meta.ai/docs/authentication",
1492
+ defaultModel: "muse-spark-1.3",
1493
+ models: META_MUSE_MODELS,
1494
+ // Static roster: no authenticated /v1/models payload was ever observed (the only
1495
+ // contact was an unauthenticated GET returning 401 invalid_api_key), and Meta serves
1496
+ // non-agent families on this same base URL. Turning discovery on would publish an
1497
+ // unseen roster into the picker.
1498
+ liveModels: false,
1499
+ // A user may already own a custom provider named "meta-model" pointing elsewhere;
1500
+ // without this, registry transport canonicalization would retarget it and send their
1501
+ // saved key to Meta.
1502
+ preserveCustomDestination: true,
1503
+ modelContextWindows: Object.fromEntries(META_MUSE_MODELS.map(id => [id, META_MUSE_CONTEXT_WINDOW])),
1504
+ // text+image only. Meta also documents video, audio (degraded on 1.3), and PDF, but
1505
+ // the catalog modality enum is text/image and over-advertising poisons the exported
1506
+ // client config (see tests/catalog-input-modality-enum.test.ts).
1507
+ modelInputModalities: Object.fromEntries(META_MUSE_MODELS.map(id => [id, ["text", "image"] as ["text", "image"]])),
1508
+ modelReasoningEfforts: Object.fromEntries(META_MUSE_MODELS.map(id => [id, META_MUSE_REASONING_EFFORTS])),
1509
+ modelReasoningEffortMap: Object.fromEntries(META_MUSE_MODELS.map(id => [id, META_MUSE_REASONING_EFFORT_MAP])),
1510
+ // No defaultMaxOutputTokens: Meta publishes none. The only number in its docs
1511
+ // (131072) appears inside a third-party config sample, and the protocol pages call
1512
+ // the real limit "model-dependent".
1513
+ // Meta names its variable MODEL_API_KEY, but the env var opencodex reads is derived
1514
+ // from the provider id (META_MODEL_API_KEY). Saying only Meta's name would send a
1515
+ // user to export a variable this proxy never reads.
1516
+ note: "Pay-as-you-go Meta Model API. Get a key at https://dev.meta.ai (Meta calls it MODEL_API_KEY; export it here as META_MODEL_API_KEY) — a Meta developer account needs a payment method before it can serve requests, and every call is metered per token. A Muse Code subscription does NOT work here: Meta scopes that credential to the Muse Code CLI and bills any other key pay-as-you-go (dev.meta.ai/docs/muse-code/subscriptions). The Contributor tier (muse-spark-1.3-contributor) is cheap because Meta trains on your prompts — about 92% off input, 95% off output, 99% off cached input; do not send confidential material through it. Muse Spark is also reachable through resellers: command-code carries both tiers, opencode-go serves only muse-spark-1.3-contributor.",
1517
+ },
1518
+ /* [Decision Log]
1519
+ - 목적과 의도: Let an operator who already signed the Muse Code CLI in reach Muse Spark with that credential, instead of provisioning a second key.
1520
+ - 기존 구현 및 제약 조건: The CLI stores a pointer at ~/.config/muse/auth.json and the secret in the macOS Keychain (ai.meta.dev.credentials/meta). Measured: the OAuth access_token 401s on /v1/models while the sibling api_key returns 200, so the usable artifact is a static key, not a refreshable token.
1521
+ - 검토한 주요 대안: spawn `muse login` and poll; reimplement Meta's device grant; treat it as a second key preset; ship nothing.
1522
+ - 선택한 방식: an import-only, macOS-only OAuth provider that reads the existing credential, validates it once, and never spawns or reimplements anything.
1523
+ - 다른 대안 대신 이 방식을 선택한 이유: `muse login` has no non-interactive mode, so a spawned child could outlive cancellation, and polling for the pointer file is satisfied instantly by the one already on disk — reimporting the OLD account on a force-login. Reimplementing the grant would mean guessing a client id the vendor does not publish.
1524
+ - 장점, 단점 및 영향: no new credential to provision, and the id is distinct from meta-model so neither pool contaminates the other. Meta scopes this credential to its own CLI, so the provider carries a HIGH_RISK ToS warning, a CLI-side warning before any read, and a note that says plainly what is unsupported.
1525
+ */
1526
+ {
1527
+ id: "meta-muse",
1528
+ label: "Meta Muse Code (CLI credential)",
1529
+ adapter: "openai-responses",
1530
+ baseUrl: "https://api.meta.ai/v1",
1531
+ authKind: "oauth",
1532
+ oauthId: "meta-muse",
1533
+ dashboardUrl: "https://dev.meta.ai",
1534
+ defaultModel: "muse-spark-1.3",
1535
+ models: META_MUSE_MODELS,
1536
+ // Same reason as meta-model: the authenticated roster carries muse-image-1.0 and
1537
+ // muse-voice-transcribe-1.0, which this Responses-agent provider cannot drive.
1538
+ liveModels: false,
1539
+ modelContextWindows: Object.fromEntries(META_MUSE_MODELS.map(id => [id, META_MUSE_CONTEXT_WINDOW])),
1540
+ modelInputModalities: Object.fromEntries(META_MUSE_MODELS.map(id => [id, ["text", "image"] as ["text", "image"]])),
1541
+ modelReasoningEfforts: Object.fromEntries(META_MUSE_MODELS.map(id => [id, META_MUSE_REASONING_EFFORTS])),
1542
+ modelReasoningEffortMap: Object.fromEntries(META_MUSE_MODELS.map(id => [id, META_MUSE_REASONING_EFFORT_MAP])),
1543
+ note: "Reuses the API key the Muse Code CLI stores after `muse login` (macOS only; requires the CLI installed and signed in). Meta scopes that credential to the Muse Code CLI, so this is an UNSUPPORTED use: Meta does not authorize subscription coverage outside its own CLI, how these calls settle is not observable from the API, and you should treat every call as billable against your account. The imported key is copied into OpenCodex's auth store. OpenCodex reads Meta's subscription windows from streaming responses and shows the last observed value with its age; there is no endpoint to query them on demand, so refreshing one requires another streaming turn, and translated (non-passthrough) turns report none. Rate limits apply per team, not per key. For a supported path use the meta-model provider with your own key (export it as META_MODEL_API_KEY).",
1544
+ },
1449
1545
  {
1450
1546
  id: "umans",
1451
1547
  label: "Umans AI Coding Plan",
@@ -1487,25 +1583,33 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1487
1583
  - 다른 대안 대신 이 방식을 선택한 이유: OpenCode Go documents sibling models on Chat or Anthropic endpoints, and an exact registry default preserves both those routes and explicit opt-out precedence.
1488
1584
  - 장점, 단점 및 영향: Each listed model reaches `/responses` from every inbound surface without changing siblings; a future upstream endpoint change requires an evidence-backed registry update.
1489
1585
  */
1490
- modelWireDefaults: { "gpt-5.6-luna": "openai-responses", "muse-spark-1.2-contributor": "openai-responses" },
1586
+ modelWireDefaults: {
1587
+ "gpt-5.6-luna": "openai-responses",
1588
+ "muse-spark-1.3-contributor": "openai-responses",
1589
+ "muse-spark-1.2-contributor": "openai-responses",
1590
+ },
1491
1591
  modelContextWindows: {
1492
1592
  "kimi-k3": KIMI_K3_STANDARD_CONTEXT_WINDOW,
1493
1593
  // The DeepSeek vision preview id is metadata-only here: the Go roster is
1494
1594
  // discovered live, so it applies the moment the gateway serves the id.
1495
1595
  [DEEPSEEK_VISION_PREVIEW_MODEL]: 1_048_576,
1496
- // Muse Spark 1.2 Contributor serves a 1,048,576-token (1M) context window over
1596
+ // Muse Spark Contributor serves a 1,048,576-token (1M) context window over
1497
1597
  // /responses on Zen Go, matching its 1.1 sibling (Meta developer docs, verified 2026-08-28).
1498
1598
  // Without this declaration the catalog falls back to 128k, capping real usable context.
1599
+ // 1.3 ships the same window as 1.2 and is served from the same Zen Go roster.
1600
+ "muse-spark-1.3-contributor": 1_048_576,
1499
1601
  "muse-spark-1.2-contributor": 1_048_576,
1500
1602
  },
1501
1603
  modelInputModalities: {
1502
1604
  "kimi-k3": ["text", "image"],
1503
1605
  // Experimental DeepSeek vision preview — expected to merge into deepseek-v4-flash later.
1504
1606
  [DEEPSEEK_VISION_PREVIEW_MODEL]: ["text", "image"],
1505
- // Muse Spark 1.2 Contributor is natively multimodal on Zen Go: it accepts input_image
1607
+ // Muse Spark Contributor is natively multimodal on Zen Go: it accepts input_image
1506
1608
  // parts over /responses (probed 2026-08-26). Without this declaration the catalog
1507
1609
  // advertises it text-only and the Codex app blocks image attachments client-side with
1508
1610
  // "This model does not support image inputs" before the request ever reaches the proxy.
1611
+ // 1.3 is the same-shaped successor and Command Code documents it as multimodal.
1612
+ "muse-spark-1.3-contributor": ["text", "image"],
1509
1613
  "muse-spark-1.2-contributor": ["text", "image"],
1510
1614
  },
1511
1615
  modelReasoningEfforts: {
@@ -1736,13 +1840,17 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1736
1840
  // devlog/_plan/260710_provider_hardening/001_research_frontier.md.
1737
1841
  {
1738
1842
  id: "google", label: "Google Gemini", adapter: "google", baseUrl: "https://generativelanguage.googleapis.com", authKind: "key", featured: true,
1739
- dashboardUrl: "https://aistudio.google.com/apikey", defaultModel: "gemini-3.5-flash", models: ["gemini-3.6-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview", "gemini-3.7-flash"],
1740
- modelContextWindows: { "gemini-3.6-flash": 1_048_576, "gemini-3.5-flash": 1_000_000, "gemini-3.5-flash-lite": 1_048_576, "gemini-3.7-flash": 1_048_576 },
1741
- modelInputModalities: { "gemini-3.6-flash": ["text", "image"], "gemini-3.5-flash-lite": ["text", "image"], "gemini-3.7-flash": ["text", "image"] },
1843
+ dashboardUrl: "https://aistudio.google.com/apikey", defaultModel: "gemini-3.5-flash", models: ["gemini-3.8-flash", "gemini-3.6-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview", "gemini-3.7-flash"],
1844
+ modelContextWindows: { "gemini-3.8-flash": 1_048_576, "gemini-3.6-flash": 1_048_576, "gemini-3.5-flash": 1_000_000, "gemini-3.5-flash-lite": 1_048_576, "gemini-3.7-flash": 1_048_576 },
1845
+ modelInputModalities: { "gemini-3.8-flash": ["text", "image"], "gemini-3.6-flash": ["text", "image"], "gemini-3.5-flash-lite": ["text", "image"], "gemini-3.7-flash": ["text", "image"] },
1742
1846
  modelReasoningEfforts: {
1847
+ // 3.7 and 3.8 omit `minimal`: Google documents it as a validation error on both model
1848
+ // pages, so advertising it hands the user a rung the API rejects. 3.5/3.6 keep theirs —
1849
+ // their pages still list it, and this unit has no evidence to change them.
1850
+ "gemini-3.8-flash": ["low", "medium", "high"],
1851
+ "gemini-3.7-flash": ["low", "medium", "high"],
1743
1852
  "gemini-3.6-flash": ["minimal", "low", "medium", "high"],
1744
1853
  "gemini-3.5-flash": ["minimal", "low", "medium", "high"],
1745
- "gemini-3.7-flash": ["minimal", "low", "medium", "high"],
1746
1854
  "gemini-3.1-pro-preview": ["low", "medium", "high"],
1747
1855
  },
1748
1856
  jawcodeBundle: "google", extraMetadataAliases: ["gemini"],
@@ -1750,7 +1858,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1750
1858
  // 2026-07-10: defaultModel is frozen pending Vertex-specific Tier-2 evidence; Gemini API
1751
1859
  // evidence from ai.google.dev does not establish Vertex publisher availability.
1752
1860
  { id: "google-vertex", label: "Google Vertex AI", adapter: "google", baseUrl: "https://aiplatform.googleapis.com", authKind: "key", dashboardUrl: "https://console.cloud.google.com/vertex-ai", defaultModel: "gemini-3-pro", googleMode: "vertex", jawcodeBundle: "google", extraMetadataAliases: ["gemini-vertex"] },
1753
- { id: "google-antigravity", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", allowBaseUrlOverride: true, dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: true, defaultModel: "gemini-3.7-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"] },
1861
+ { id: "google-antigravity", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", allowBaseUrlOverride: true, dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: true, defaultModel: "gemini-3.8-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"] },
1754
1862
  { id: "azure-openai", label: "Azure OpenAI", adapter: "azure-openai", baseUrl: "https://{resource}.openai.azure.com/openai", authKind: "key", featured: true, dashboardUrl: "https://portal.azure.com" },
1755
1863
  { id: "ollama", label: "Ollama (local)", adapter: "openai-chat", baseUrl: "http://localhost:11434/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" },
1756
1864
  { id: "vllm", label: "vLLM (local)", adapter: "openai-chat", baseUrl: "http://localhost:8000/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" },
@@ -21,7 +21,7 @@ function unwrapPatchInput(value: string): string {
21
21
  * Convert a nested Code Mode helper call into unified-exec JavaScript.
22
22
  *
23
23
  * Parsed values are serialized as data, never interpolated as source, so command and patch text
24
- * cannot escape the generated call. Invalid structured shell payloads are also passed as data so
24
+ * cannot escape the generated call. Invalid structured helper payloads are also passed as data so
25
25
  * nested-tool validation can reject them without evaluating provider text as JavaScript.
26
26
  */
27
27
  export function compileCodeModeHelperInput(argumentsText: unknown, toolName: string): string {
@@ -46,5 +46,8 @@ export function compileCodeModeHelperInput(argumentsText: unknown, toolName: str
46
46
  args.cmd = args.command;
47
47
  delete args.command;
48
48
  }
49
+ if (toolName === "write_stdin") {
50
+ return `const result = await tools.write_stdin(${JSON.stringify(args)});\ntext(result);`;
51
+ }
49
52
  return `const result = await tools.exec_command(${JSON.stringify(args)});\ntext(result);`;
50
53
  }
@@ -1469,14 +1469,14 @@ function ensureLoaded(): void {
1469
1469
 
1470
1470
  type SnapshotWriteOutcome = "stable" | "unstable" | "failed";
1471
1471
 
1472
- async function writeBoundedSnapshot(path: string): Promise<SnapshotWriteOutcome> {
1472
+ async function writeBoundedSnapshot(path: string, attemptLimit: number): Promise<SnapshotWriteOutcome> {
1473
1473
  // Serialize writers so concurrent flush + debounce cannot race on temps / ACL (#612).
1474
1474
  const previous = persistGate;
1475
1475
  let release!: () => void;
1476
1476
  persistGate = new Promise<void>(resolve => { release = resolve; });
1477
1477
  await previous;
1478
1478
  try {
1479
- for (let attempt = 0; attempt < MAX_SNAPSHOT_REWRITE_ATTEMPTS; attempt += 1) {
1479
+ for (let attempt = 0; attempt < attemptLimit; attempt += 1) {
1480
1480
  const revision = stateRevision;
1481
1481
  const entries: Array<[string, unknown]> = [];
1482
1482
  let total = 0;
@@ -1579,12 +1579,13 @@ async function persistNow(path: string, awaitFollowUp = false): Promise<void> {
1579
1579
  persistTimer = null;
1580
1580
  }
1581
1581
  pendingPersistPath = null;
1582
- let outcome = await writeBoundedSnapshot(path);
1582
+ const attemptLimit = awaitFollowUp ? MAX_SNAPSHOT_REWRITE_ATTEMPTS : 1;
1583
+ let outcome = await writeBoundedSnapshot(path, attemptLimit);
1583
1584
  if (outcome === "unstable" && awaitFollowUp) {
1584
1585
  if (persistTimer) clearTimeout(persistTimer);
1585
1586
  persistTimer = null;
1586
1587
  pendingPersistPath = null;
1587
- outcome = await writeBoundedSnapshot(path);
1588
+ outcome = await writeBoundedSnapshot(path, attemptLimit);
1588
1589
  }
1589
1590
  if (outcome === "stable") drainPendingSpillUnlinks();
1590
1591
  else if (outcome === "unstable" && !awaitFollowUp) schedulePersistAt(path, true);