Skip to content

feat(provider): MTP posture and acceptance counters on the local /metrics endpoint - #619

Merged
Gajesh2007 merged 3 commits into
masterfrom
fix/provider-mtp-metrics
Aug 13, 2026
Merged

Gajesh2007 merged 3 commits into
masterfrom
fix/provider-mtp-metrics

Conversation

@Gajesh2007

@Gajesh2007 Gajesh2007 commented Aug 13, 2026 •

Copy link
Copy Markdown
Member

Problem

Bug 2 from Layr-Labs/mlx-swift-lm#106's Discovered bugs: MTP acceptance is unobservable outside Datadog Logs. The counters exist in-engine (CBv2MTPMetrics, polled via EngineV2Bridge.mtpStatusSnapshot()), but the only surface was the engine_v2_slot_posture telemetry event → Datadog Logs. The provider's local HTTP server exposes /metrics with no MTP counters at all, and the os_log store is inaccessible in sandboxes — so a headless operator could not observe whether MTP is producing drafts, what the acceptance ratio is, or that MTP silently never activated (inline_artifact_invalid, the sibling bug fixed in #618; inert_kv_unsupported; …).

Change

/metrics now appends provider-owned per-model lines behind the upstream MLXLMServer body, in the same Prometheus text style (# TYPE + bare lines):

mtp_enabled{model="…"} 0|1
mtp_active{model="…"} 0|1
mtp_rounds_total{model="…"} N
mtp_tokens_proposed_total{model="…"} N
mtp_tokens_accepted_total{model="…"} N
mtp_inactive_reason{model="…",reason="inline_artifact_invalid"} 1   # omitted when productively running

Counters are cumulative per slot lifetime (standard counter contract, matching the telemetry event); acceptance rate = accepted_total / proposed_total. mtp_inactive_reason carries the existing MTPFallbackReason enum — the same status inline_artifact_invalid rides in — so a silently-disabled drafter is operationally visible.

Mechanics: the upstream /metrics route lives in MLXLMServer (ServerMetrics.prometheusText()), whose router is already built when the provider wraps it — so a new LocalMetricsResponder serves GET /metrics itself (same public MLXOpenAIService.prometheusMetrics() entry point + the MTP lines), exactly the interception pattern LocalChatUploadResponder established. Wired through both local endpoints via a mtpSlots closure: the StandaloneServer slot cache (--local) and the unified ProviderLoop.modelSlots (--local-endpoint) — the same bridge snapshot the capacity tick already feeds into DaemonSlotPostureBuilder, so /metrics, daemon-state, and telemetry cannot describe a slot differently.

Deliberately not touched: the coordinator telemetry wire types (Go/Swift/TS three-language sync — out of scope); the daemon-state SlotPosture already carries mtpEnabled/mtpActive/mtpInactiveReason, so darkbloom status needed no change.

Behavior

flowchart LR
  subgraph Before
    A1["CBv2MTPMetrics (in-engine)"] --> B1["engine_v2_slot_posture event"] --> C1["Datadog Logs ONLY<br/>headless/sandboxed operator: blind<br/>silently-disabled MTP invisible"]
    D1["GET /metrics"] --> E1["upstream mlx_server_* counters only"]
  end
  subgraph After
    A2["CBv2MTPMetrics (in-engine)"] --> B2["engine_v2_slot_posture event -> Datadog (unchanged)"]
    A2 --> D2["GET /metrics"] --> E2["mlx_server_* body +<br/>mtp_enabled / mtp_active / mtp_rounds_total /<br/>mtp_tokens_proposed_total / mtp_tokens_accepted_total /<br/>mtp_inactive_reason{reason=…}"]
  end
Loading

Code

flowchart LR
  subgraph Before
    R1["auth -> CORS -> chat-upload -> upstream router"] --> M1["/metrics = ServerMetrics.prometheusText() verbatim"]
  end
  subgraph After
    R2["auth -> CORS -> LocalMetricsResponder -> chat-upload -> upstream router"] --> M2["/metrics = prometheusMetrics() + MTPPrometheusRenderer.render(mtpSlots())"]
    S2[StandaloneServer.mtpSlotMetricsSamples] --> M2
    P2[ProviderLoop.mtpSlotMetricsSamplesForLocal] --> M2
  end
Loading

Pin independence (#616)

Pure provider-swift change, based on master (which already carries the #616 pin bump to mlx-swift-lm d06da67). It uses no field introduced by mlx-swift-lm#106 — CBv2MTPMetrics.rounds/draftedTokens/acceptedTokens/active and MLXOpenAIService.prometheusMetrics() all predate it — so it also compiles against the pre-#106 pin (c04afb4); no availability gating needed.

Tests

Targeted filters green (swift test, debug, with source-matched metallib per make provider-test):

  • mtpPrometheusRendererEmitsPostureAndCounters — stubbed active snapshot (7 rounds, 21 proposed, 14 accepted) and a stubbed inline_artifact_invalid slot: all six families render, healthy slots omit mtp_inactive_reason, label values are Prometheus-escaped, empty slot list renders nothing (upstream body stays byte-identical).
  • localMetricsEndpointAppendsMTPLinesToUpstreamBody — full responder stack via makeLocalInferenceApplication with a stubbed mtpSlots closure: GET /metrics returns 200 text/plain, the upstream mlx_server_* body intact, and the stubbed counters (3/9/6) appear.
  • standaloneServerMetricsWithoutSlotsServesUpstreamBodyOnly — cold standalone server: upstream body only, no mtp_ lines.
  • Plus the 16-test standalone-server/CORS/auth/upload filter unchanged.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

…rics endpoint

Bug 2 from mlx-swift-lm#106 'Discovered bugs': MTP acceptance existed only
in-engine (CBv2MTPMetrics via EngineV2Bridge.mtpStatusSnapshot) and in the
engine_v2_slot_posture telemetry event — i.e. Datadog Logs. A headless
operator (no os_log store in sandboxes) could not observe acceptance, or
that MTP silently never activated (inline_artifact_invalid).

- LocalMetricsResponder serves GET /metrics: the upstream MLXLMServer
  ServerMetrics body (same public prometheusMetrics() entry point) plus
  provider-owned per-model lines in the same Prometheus text style:
  mtp_enabled, mtp_active, mtp_rounds_total, mtp_tokens_proposed_total,
  mtp_tokens_accepted_total, and mtp_inactive_reason{reason=...} carrying
  the MTPFallbackReason enum (inline_artifact_invalid, inert_kv_unsupported,
  ...) whenever MTP is not productively running.
- Wired through both local endpoints via a mtpSlots closure: StandaloneServer
  slot cache and the unified ProviderLoop modelSlots — the same bridge
  snapshot the capacity tick feeds DaemonSlotPostureBuilder.
- Coordinator telemetry wire types untouched (3-language sync out of scope);
  daemon-state SlotPosture already carries the mtp posture trio.
- Pure provider-swift change; no submodule field added by #106 is required
  (counters predate it), so it is pin-independent of the #616 bump.
@vercel

vercel Bot commented Aug 13, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
d-inference Ready Ready Preview Aug 13, 2026 11:41pm
d-inference-console-ui-dev Ready Ready Preview Aug 13, 2026 11:41pm
d-inference-landing Ready Ready Preview Aug 13, 2026 11:41pm

Request Review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8678aa4af8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread provider-swift/Sources/ProviderCore/Server/LocalMetricsResponder.swift Outdated
The MTP block was appended to MLXLMServer's prometheusMetrics() body by
bare string concatenation, silently relying on the upstream body's
trailing newline — an implementation detail of ServerMetrics
.prometheusText()'s multiline literal. A submodule bump dropping that
newline would fuse the final upstream sample and the first MTP header
into one invalid line (mlx_server_uptime_seconds 12# TYPE mtp_enabled
gauge), making Prometheus reject the whole scrape.

MTPPrometheusRenderer.joinedBody now makes the seam explicit: exactly
one newline between a non-empty upstream body and the MTP block, empty
upstream yields the MTP block alone, empty MTP block leaves the
upstream body byte-identical, and the joined body keeps the single
trailing newline the text format expects.

Tests: the /metrics route tests now assert line-level exposition
validity (every non-empty line is a # TYPE/# HELP header or a complete
name{labels} value sample) instead of substring checks alone, plus a
regression case for an unterminated upstream body + resident slot.
Verified the regression test fails against the pre-fix bare concat.

This branch had an error being deployed

1 failed and 3 active deployments
Preview – d-inference — 86ba2343 Deployed Aug 13, 2026 by vercel[bot]
Preview – d-inference-console-ui-dev — 86ba2343 Deployed Aug 13, 2026 by vercel[bot]
Preview – d-inference-landing — 86ba2343 Deployed Aug 13, 2026 by vercel[bot]
benchmarks — 86ba2343 Deployed Aug 13, 2026 by Gajesh2007 via E2E Benchmarks #69
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant