Skip to main content

inferlab_protocol/
wire.rs

1//! The versioned wire types of the framework integration protocol
2//! ([[RFC-0006:C-INTEGRATIONS]]): the one-shot stdin/stdout JSON contract for
3//! the plan/render serve operations, plus the client request/result surfaces
4//! the release-owned Eval and Bench measurement runtimes exchange with their
5//! clients. [`AdapterProtocol`] is the schema root from which the committed
6//! JSON schema and the Python SDK models are generated.
7
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10use std::collections::BTreeMap;
11use std::path::PathBuf;
12
13/// The shared protocol version used by framework integrations and release-owned
14/// measurement clients. The only accepted value is `5` (serialized as the
15/// string `"5"`); a mismatch is rejected before lowering.
16#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
17pub enum ProtocolVersion {
18    /// Protocol version 5.
19    #[serde(rename = "5")]
20    V5,
21}
22
23/// The one JSON request an integration reads from stdin, tagged by the
24/// requested operation.
25#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
26#[serde(tag = "operation", rename_all = "snake_case", deny_unknown_fields)]
27pub enum AdapterRequest {
28    /// Plan a serve topology: declare roles, per-replica requirements, links,
29    /// and endpoint requirements from the requested shape.
30    PlanServe {
31        protocol_version: ProtocolVersion,
32        input: PlanServeInput,
33    },
34    /// Render final process invocations for a planned topology, given the
35    /// control plane's concrete process allocations.
36    RenderServe {
37        protocol_version: ProtocolVersion,
38        input: RenderServeInput,
39    },
40}
41
42impl AdapterRequest {
43    /// The protocol version carried by this request, regardless of operation.
44    #[must_use]
45    pub const fn protocol_version(&self) -> ProtocolVersion {
46        match self {
47            Self::PlanServe {
48                protocol_version, ..
49            }
50            | Self::RenderServe {
51                protocol_version, ..
52            } => *protocol_version,
53        }
54    }
55}
56
57/// The requested serve shape a `PlanServe` operation lowers into a topology.
58#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
59#[serde(deny_unknown_fields)]
60pub struct PlanServeInput {
61    pub model: ServeModelInput,
62    pub topology: ServeTopology,
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub routing_backend: Option<String>,
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub kv_transfer: Option<KvTransferMechanism>,
67    pub roles: Vec<ServeRoleInput>,
68    #[serde(default)]
69    pub profiling: bool,
70}
71
72/// The planned topology plus the control plane's concrete allocations that a
73/// `RenderServe` operation turns into final process invocations.
74#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
75#[serde(deny_unknown_fields)]
76pub struct RenderServeInput {
77    pub model: ServeModelInput,
78    pub topology: ServeTopology,
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub routing_backend: Option<String>,
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub kv_transfer: Option<KvTransferMechanism>,
83    pub roles: Vec<ServeRoleResult>,
84    pub routing: RoutingResult,
85    pub links: Vec<ServeRoleLink>,
86    pub allocations: Vec<ServeProcessAllocation>,
87    #[serde(default)]
88    pub render_inputs: Vec<SuppliedRenderInput>,
89    #[serde(default)]
90    pub profiling: bool,
91}
92
93/// The serving deployment topology.
94#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
95#[serde(rename_all = "snake_case")]
96pub enum ServeTopology {
97    /// One aggregated serving role.
98    Single,
99    /// Disaggregated prefill and decode roles.
100    PrefillDecode,
101}
102
103/// The logical role a replica plays in a serve topology.
104#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
105#[serde(rename_all = "snake_case")]
106pub enum ServeRoleKind {
107    /// Aggregated serving role of a single topology.
108    Serve,
109    /// Prefill role of a disaggregated topology.
110    Prefill,
111    /// Decode role of a disaggregated topology.
112    Decode,
113    /// Request-routing role that does not execute model inference.
114    Router,
115}
116
117/// The KV-transfer mechanism connecting prefill and decode.
118#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
119#[serde(rename_all = "snake_case")]
120pub enum KvTransferMechanism {
121    /// Mooncake KV-cache transfer.
122    Mooncake,
123    /// NIXL KV-cache transfer.
124    Nixl,
125}
126
127/// A requested serving role: its identity, kind, replica cardinality, and
128/// declared (not-yet-completed) parallelism and settings.
129#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
130#[serde(deny_unknown_fields)]
131pub struct ServeRoleInput {
132    pub id: String,
133    pub kind: ServeRoleKind,
134    pub replica_count: u32,
135    pub parallelism: Parallelism,
136    pub settings: BTreeMap<String, SettingValue>,
137}
138
139/// A role as the integration resolved it: preserved identity and cardinality
140/// with the complete effective settings and parallelism.
141#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
142#[serde(deny_unknown_fields)]
143pub struct ServeRoleResult {
144    pub id: String,
145    pub kind: ServeRoleKind,
146    pub declared_replica_count: u32,
147    pub effective_replica_count: u32,
148    pub effective_settings: BTreeMap<String, SettingValue>,
149    pub effective_parallelism: Parallelism,
150}
151
152/// Framework-neutral component-aware parallelism ([[RFC-0003:C-SERVE-PARALLELISM]]).
153/// Every component is optional so an operator can override one without
154/// repeating the rest; omitted components are filled by the integration into a
155/// complete effective shape.
156#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
157#[serde(default, deny_unknown_fields)]
158pub struct Parallelism {
159    /// Outer deployment parallelism shared by attention and experts.
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub outer: Option<ParallelismOuter>,
162    /// Attention-block parallelism.
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub attention: Option<ParallelismAttention>,
165    /// MoE expert parallelism.
166    #[serde(skip_serializing_if = "Option::is_none")]
167    pub experts: Option<ParallelismExperts>,
168}
169
170impl Parallelism {
171    /// Overlay the components present in `other` onto `self`, leaving
172    /// components `other` omits untouched (the per-component precedence merge).
173    pub fn merge_from(&mut self, other: &Self) {
174        if let Some(other) = &other.outer {
175            self.outer.get_or_insert_default().merge_from(other);
176        }
177        if let Some(other) = &other.attention {
178            self.attention.get_or_insert_default().merge_from(other);
179        }
180        if let Some(other) = &other.experts {
181            self.experts.get_or_insert_default().merge_from(other);
182        }
183    }
184}
185
186/// Outer deployment parallelism: tensor and pipeline degrees.
187#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
188#[serde(default, deny_unknown_fields)]
189pub struct ParallelismOuter {
190    #[schemars(range(min = 1))]
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub tensor_parallel_size: Option<u32>,
193    #[schemars(range(min = 1))]
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub pipeline_parallel_size: Option<u32>,
196}
197
198impl ParallelismOuter {
199    fn merge_from(&mut self, other: &Self) {
200        if other.tensor_parallel_size.is_some() {
201            self.tensor_parallel_size = other.tensor_parallel_size;
202        }
203        if other.pipeline_parallel_size.is_some() {
204            self.pipeline_parallel_size = other.pipeline_parallel_size;
205        }
206    }
207}
208
209/// Attention-block parallelism: tensor, data, and context degrees.
210#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
211#[serde(default, deny_unknown_fields)]
212pub struct ParallelismAttention {
213    #[schemars(range(min = 1))]
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub tensor_parallel_size: Option<u32>,
216    #[schemars(range(min = 1))]
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub data_parallel_size: Option<u32>,
219    #[schemars(range(min = 1))]
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub context_parallel_size: Option<u32>,
222}
223
224impl ParallelismAttention {
225    fn merge_from(&mut self, other: &Self) {
226        if other.tensor_parallel_size.is_some() {
227            self.tensor_parallel_size = other.tensor_parallel_size;
228        }
229        if other.data_parallel_size.is_some() {
230            self.data_parallel_size = other.data_parallel_size;
231        }
232        if other.context_parallel_size.is_some() {
233            self.context_parallel_size = other.context_parallel_size;
234        }
235    }
236}
237
238/// MoE expert parallelism: tensor, data, expert, and dense-tensor degrees.
239#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
240#[serde(default, deny_unknown_fields)]
241pub struct ParallelismExperts {
242    #[schemars(range(min = 1))]
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub tensor_parallel_size: Option<u32>,
245    #[schemars(range(min = 1))]
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub data_parallel_size: Option<u32>,
248    #[schemars(range(min = 1))]
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub expert_parallel_size: Option<u32>,
251    #[schemars(range(min = 1))]
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub dense_tensor_parallel_size: Option<u32>,
254}
255
256impl ParallelismExperts {
257    fn merge_from(&mut self, other: &Self) {
258        if other.tensor_parallel_size.is_some() {
259            self.tensor_parallel_size = other.tensor_parallel_size;
260        }
261        if other.data_parallel_size.is_some() {
262            self.data_parallel_size = other.data_parallel_size;
263        }
264        if other.expert_parallel_size.is_some() {
265            self.expert_parallel_size = other.expert_parallel_size;
266        }
267        if other.dense_tensor_parallel_size.is_some() {
268            self.dense_tensor_parallel_size = other.dense_tensor_parallel_size;
269        }
270    }
271}
272
273/// The logical model supplied during serving planning and rendering.
274#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
275#[serde(deny_unknown_fields)]
276pub struct ServeModelInput {
277    pub id: String,
278    pub served_name: String,
279}
280
281/// The model identity used by measurement clients. Unlike integration
282/// planning, a benchmark client may need a controller-visible tokenizer
283/// locator.
284#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
285#[serde(deny_unknown_fields)]
286pub struct MeasurementModelInput {
287    pub locator: String,
288    pub served_name: String,
289}
290
291/// A concrete host/port endpoint the control plane allocated for a process.
292#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
293#[serde(deny_unknown_fields)]
294pub struct EndpointAssignment {
295    pub host: String,
296    pub port: u16,
297}
298
299/// The public workload endpoint an Eval or Bench client connects to.
300#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
301#[serde(deny_unknown_fields)]
302pub struct ClientEndpointInput {
303    pub protocol: EndpointProtocol,
304    pub host: String,
305    pub port: u16,
306    pub api_path: String,
307}
308
309/// The measurement an Eval client runs against the workload endpoint.
310#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
311#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
312pub enum EvalDefinitionInput {
313    /// A single-prompt liveness check.
314    #[serde(rename = "openai_smoke")]
315    OpenAiSmoke {
316        prompt: String,
317        max_tokens: u32,
318        timeout_seconds: u64,
319    },
320    /// An lm-eval task run with a pass threshold on the chosen metric.
321    LmEval {
322        task: String,
323        dataset: Option<String>,
324        split: Option<String>,
325        limit: Option<u32>,
326        few_shot: Option<u32>,
327        seed: Option<u64>,
328        max_tokens: Option<u32>,
329        concurrency: Option<u32>,
330        metric: String,
331        threshold: f64,
332        timeout_seconds: u64,
333    },
334}
335
336/// The workload shape a Bench client drives, shared across its load cases.
337#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
338#[serde(deny_unknown_fields)]
339pub struct BenchDefinitionInput {
340    pub input_tokens: u32,
341    pub output_tokens: u32,
342    pub seed: u64,
343    pub temperature: f64,
344    pub timeout_seconds: u64,
345    #[serde(default)]
346    pub reset_prefix_cache: bool,
347}
348
349/// A framework-specific server setting value carried as structured JSON data
350/// (never a pre-rendered shell fragment) across the integration boundary.
351#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
352#[serde(untagged)]
353pub enum SettingValue {
354    /// A JSON boolean.
355    Bool(bool),
356    /// A JSON integer.
357    Integer(i64),
358    /// A JSON floating-point number.
359    Float(f64),
360    /// A JSON string.
361    String(String),
362    /// A JSON array of setting values.
363    Array(Vec<SettingValue>),
364    /// A JSON object of named setting values.
365    Object(BTreeMap<String, SettingValue>),
366}
367
368/// The one JSON response an integration writes to stdout, tagged by outcome.
369#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
370#[serde(tag = "status", rename_all = "snake_case", deny_unknown_fields)]
371pub enum AdapterResponse {
372    /// The operation succeeded and carries its result.
373    Ok {
374        protocol_version: ProtocolVersion,
375        result: Box<AdapterResult>,
376    },
377    /// The operation was rejected with a structured error.
378    Error {
379        protocol_version: ProtocolVersion,
380        error: AdapterError,
381    },
382}
383
384impl AdapterResponse {
385    /// The protocol version carried by this response, regardless of outcome.
386    #[must_use]
387    pub const fn protocol_version(&self) -> ProtocolVersion {
388        match self {
389            Self::Ok {
390                protocol_version, ..
391            }
392            | Self::Error {
393                protocol_version, ..
394            } => *protocol_version,
395        }
396    }
397}
398
399/// The successful result of an [`AdapterRequest`], tagged by the operation it
400/// answers.
401#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
402#[serde(tag = "operation", rename_all = "snake_case", deny_unknown_fields)]
403pub enum AdapterResult {
404    /// The planned topology from a `PlanServe` request.
405    PlanServe { output: Box<PlanServeResult> },
406    /// The rendered process invocations from a `RenderServe` request.
407    RenderServe { output: Box<RenderServeResult> },
408}
409
410/// The lowered topology returned by a `PlanServe`: the effective server-level
411/// shape, per-role resolution, whole-replica requirements, role links, and the
412/// public and per-role endpoint requirements the control plane then allocates.
413#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
414#[serde(deny_unknown_fields)]
415pub struct PlanServeResult {
416    pub integration: IntegrationIdentity,
417    pub roles: Vec<ServeRoleResult>,
418    pub replicas: Vec<ServeReplicaRequirement>,
419    pub links: Vec<ServeRoleLink>,
420    pub routing: RoutingResult,
421    pub endpoint: EndpointRequirement,
422    #[serde(default)]
423    pub render_inputs: Vec<RenderInputDeclaration>,
424}
425
426/// One workspace-authored UTF-8 source file an integration declares during
427/// planning for the control plane to supply during final rendering.
428#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
429#[serde(deny_unknown_fields)]
430pub struct RenderInputDeclaration {
431    pub source_path: String,
432}
433
434/// The original declared path plus the exact UTF-8 contents and digest the
435/// control plane supplies to an integration during final rendering.
436#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
437#[serde(deny_unknown_fields)]
438pub struct SuppliedRenderInput {
439    pub source_path: String,
440    pub text: String,
441    pub sha256: String,
442}
443
444/// A whole-replica resource and readiness requirement the integration declares
445/// without choosing placement, ranks, or concrete endpoints.
446#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
447#[serde(deny_unknown_fields)]
448pub struct ServeReplicaRequirement {
449    pub id: String,
450    pub role_id: String,
451    pub replica_index: u32,
452    pub device_count: u32,
453    pub ports: Vec<String>,
454    pub primary_ports: Vec<String>,
455    pub primary_readiness: ReadinessProbe,
456    pub worker_readiness: ReadinessProbe,
457    #[serde(default, skip_serializing_if = "Option::is_none")]
458    pub capture_target: Option<CaptureTargetRequirement>,
459}
460
461/// One concrete process the control plane placed, supplied to `RenderServe`:
462/// its identity, rank, machine, devices, model locator, and allocated
463/// endpoints and named ports.
464#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
465#[serde(deny_unknown_fields)]
466pub struct ServeProcessAllocation {
467    pub process: String,
468    pub role: String,
469    pub replica: u32,
470    pub rank: u32,
471    pub rank_count: u32,
472    pub machine: String,
473    pub devices: Vec<u32>,
474    #[serde(default, skip_serializing_if = "Option::is_none")]
475    pub model_locator: Option<String>,
476    #[serde(default, skip_serializing_if = "Option::is_none")]
477    pub endpoint: Option<EndpointAssignment>,
478    pub ports: BTreeMap<String, EndpointAssignment>,
479    pub cache: String,
480    pub launch: AllocationLaunch,
481    #[serde(default)]
482    pub dependencies: Vec<String>,
483}
484
485/// The machine-local launch channel selected by the control plane.
486#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
487#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
488pub enum AllocationLaunch {
489    Local,
490    Ssh { target: String },
491}
492
493/// A directed link between serve roles the integration declares as part of the
494/// topology.
495#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
496#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
497pub enum ServeRoleLink {
498    /// The source role routes requests to the target roles.
499    RequestRouting {
500        source: String,
501        targets: Vec<String>,
502    },
503    /// KV cache is transferred from source to target over `mechanism`.
504    KvTransfer {
505        source: String,
506        target: String,
507        mechanism: KvTransferMechanism,
508    },
509    /// The source discovers the target through a bootstrap port.
510    Bootstrap {
511        source: String,
512        target: String,
513        port: String,
514    },
515    /// The source and target exchange out-of-band data over a side-channel port.
516    SideChannel {
517        source: String,
518        target: String,
519        port: String,
520    },
521}
522
523/// The closed routing owner selected during integration planning.
524#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
525#[serde(tag = "owner", rename_all = "snake_case", deny_unknown_fields)]
526pub enum RoutingResult {
527    Direct {
528        role: String,
529        replica: u32,
530    },
531    InferlabBuiltin {
532        implementation: BuiltinRouterKind,
533        policy: String,
534        prefill_role: String,
535        decode_role: String,
536        #[serde(default)]
537        ports: Vec<String>,
538        readiness: ReadinessProbe,
539    },
540    IntegrationNative {
541        role: String,
542        replica: u32,
543        policy: String,
544    },
545}
546
547/// An Inferlab-owned routing implementation with stable target semantics.
548#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
549#[serde(rename_all = "snake_case")]
550pub enum BuiltinRouterKind {
551    VllmMooncake,
552    VllmNixl,
553    Sglang,
554    Trtllm,
555}
556
557/// Marks a replica as a profiling capture target and carries its window
558/// control ([[RFC-0004:C-WORKLOAD-PROFILING]]).
559#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
560#[serde(deny_unknown_fields)]
561pub struct CaptureTargetRequirement {
562    pub control: CaptureControlRequirement,
563}
564
565/// The HTTP paths a capture target exposes to open and close a capture window.
566#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
567#[serde(deny_unknown_fields)]
568pub struct CaptureControlRequirement {
569    pub start_path: String,
570    pub stop_path: String,
571}
572
573/// The final process invocations returned by a `RenderServe`, one per supplied
574/// allocation.
575#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
576#[serde(deny_unknown_fields)]
577pub struct RenderServeResult {
578    pub integration: IntegrationIdentity,
579    pub processes: Vec<RenderedServeProcess>,
580}
581
582/// A rendered process bound to the allocation `id` it was produced for.
583#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
584#[serde(deny_unknown_fields)]
585pub struct RenderedServeProcess {
586    pub process: String,
587    pub role: String,
588    pub replica: u32,
589    pub rank: u32,
590    pub rank_count: u32,
591    pub launch_files: Vec<LaunchFileDeclaration>,
592    pub command: ProcessSpec,
593}
594
595/// One immutable text input a rendered process requires before it can launch.
596#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
597#[serde(deny_unknown_fields)]
598pub struct LaunchFileDeclaration {
599    pub relative_path: String,
600    pub text: String,
601    pub sha256: String,
602}
603
604/// An HTTP action (method and path) invoked against the workload endpoint.
605#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
606#[serde(deny_unknown_fields)]
607pub struct HttpActionSpec {
608    pub method: HttpMethod,
609    pub path: String,
610}
611
612/// The HTTP method of an [`HttpActionSpec`].
613#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
614#[serde(rename_all = "snake_case")]
615pub enum HttpMethod {
616    /// HTTP POST.
617    Post,
618}
619
620/// The integration's identity recorded on its results: adapter id, adapter
621/// version, and the framework it lowers to.
622#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
623#[serde(deny_unknown_fields)]
624pub struct IntegrationIdentity {
625    pub adapter_id: String,
626    pub adapter_version: String,
627    pub framework: String,
628    pub framework_version: String,
629}
630
631/// A launchable process: its argument vector and environment.
632#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
633#[serde(deny_unknown_fields)]
634pub struct ProcessSpec {
635    pub argv: Vec<String>,
636    pub env: BTreeMap<String, String>,
637}
638
639/// How the control plane decides a process is ready.
640#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
641#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
642pub enum ReadinessProbe {
643    /// Ready when an HTTP GET of `path` succeeds.
644    Http { path: String },
645    /// Ready when the public endpoint succeeds and its HTTP target registry
646    /// contains every control-plane-derived serving target.
647    HttpTargetRegistry(Box<HttpTargetRegistryReadiness>),
648    /// Ready as soon as the process is alive.
649    ProcessAlive,
650}
651
652/// The integration-owned HTTP registry contract for target-aware readiness.
653#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
654#[serde(deny_unknown_fields)]
655pub struct HttpTargetRegistryReadiness {
656    pub target_scheme: TargetEndpointScheme,
657    pub readiness_path: String,
658    pub registry_path: String,
659    pub targets_field: String,
660    pub target_url_field: String,
661    pub target_role_field: String,
662    pub target_healthy_field: String,
663    pub target_bootstrap_port_field: String,
664    pub prefill_role_value: String,
665    pub decode_role_value: String,
666    pub prefill_bootstrap_port: String,
667}
668
669/// The application protocol used to identify serving targets in a registry.
670#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
671#[serde(rename_all = "snake_case")]
672pub enum TargetEndpointScheme {
673    /// HTTP serving endpoint.
674    Http,
675    /// gRPC serving endpoint.
676    Grpc,
677}
678
679/// The workload endpoint's protocol and API path, plus an optional
680/// prefix-cache-reset action a Bench case can invoke between runs.
681#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
682#[serde(deny_unknown_fields)]
683pub struct EndpointRequirement {
684    pub protocol: EndpointProtocol,
685    pub api_path: String,
686    #[serde(default, skip_serializing_if = "Option::is_none")]
687    pub prefix_cache_reset: Option<HttpActionSpec>,
688}
689
690/// The application protocol a workload endpoint speaks.
691#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
692#[serde(rename_all = "snake_case")]
693pub enum EndpointProtocol {
694    /// HTTP.
695    Http,
696}
697
698/// A structured rejection an integration returns in an [`AdapterResponse::Error`].
699#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
700#[serde(deny_unknown_fields)]
701pub struct AdapterError {
702    pub code: AdapterErrorCode,
703    pub message: String,
704}
705
706/// Machine-readable failure category an adapter reports in an [`AdapterError`].
707#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
708#[serde(rename_all = "snake_case")]
709pub enum AdapterErrorCode {
710    /// The request was malformed or missing required fields.
711    InvalidRequest,
712    /// The request's protocol version is not accepted.
713    UnsupportedProtocolVersion,
714    /// A framework setting was unknown or invalid.
715    InvalidSettings,
716    /// An unexpected internal failure occurred in the integration.
717    Internal,
718    /// The requested operation is not supported by this integration.
719    UnsupportedOperation,
720}
721
722/// The request the Eval measurement runtime passes to its client: the endpoint
723/// to hit, the model, the eval definition, and where to write artifacts.
724#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
725#[serde(deny_unknown_fields)]
726pub struct EvalClientRequest {
727    pub protocol_version: ProtocolVersion,
728    pub endpoint: ClientEndpointInput,
729    pub model: MeasurementModelInput,
730    pub definition: EvalDefinitionInput,
731    pub artifact_dir: PathBuf,
732}
733
734/// The request the Bench measurement runtime passes to its client: the
735/// endpoint, model, bench definition, the load case to run, and the artifact
736/// directory.
737#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
738#[serde(deny_unknown_fields)]
739pub struct BenchClientRequest {
740    pub protocol_version: ProtocolVersion,
741    pub endpoint: ClientEndpointInput,
742    pub model: MeasurementModelInput,
743    pub definition: BenchDefinitionInput,
744    pub case: BenchCaseInput,
745    pub artifact_dir: PathBuf,
746}
747
748/// A single Bench case: its load shape and the number of requests to send.
749#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
750#[serde(deny_unknown_fields)]
751pub struct BenchCaseInput {
752    pub load_shape: BenchLoadInput,
753    pub request_count: u32,
754}
755
756/// How a Bench case paces its requests.
757#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
758#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
759pub enum BenchLoadInput {
760    /// A fixed number of in-flight requests.
761    ConcurrencyLimited { concurrency: u32 },
762    /// A target arrival rate, optionally shaped by a burstiness factor.
763    RequestRateLimited {
764        request_rate: f64,
765        burstiness: Option<f64>,
766    },
767    /// All requests issued as fast as possible.
768    UnboundedRequestRate,
769}
770
771/// The terminal outcome a measurement client reports.
772#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
773#[serde(rename_all = "snake_case")]
774pub enum ClientStatus {
775    /// The client completed its measurement successfully.
776    Succeeded,
777    /// The client did not complete successfully.
778    Failed,
779}
780
781/// The result an Eval client writes for the measurement runtime to consume.
782#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
783#[serde(deny_unknown_fields)]
784pub struct EvalClientResult {
785    /// Result envelope version; clients write `1`. The measurement runtime
786    /// rejects an eval result whose version is not `1`.
787    pub schema_version: u32,
788    pub status: ClientStatus,
789    pub metrics: BTreeMap<String, f64>,
790    pub native_command: Vec<String>,
791    pub raw_artifacts: Vec<RawArtifact>,
792    pub error: Option<String>,
793}
794
795/// The result a Bench client writes for the measurement runtime to consume.
796#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
797#[serde(deny_unknown_fields)]
798pub struct BenchClientResult {
799    /// Result envelope version; clients write `1`. The measurement runtime
800    /// rejects a bench result whose version is not `1`.
801    pub schema_version: u32,
802    pub status: ClientStatus,
803    pub completed_requests: u64,
804    pub failed_requests: u64,
805    pub normalization_schema: String,
806    pub metrics: BTreeMap<String, f64>,
807    pub native_command: Vec<String>,
808    pub native_exit_code: Option<i32>,
809    pub raw_artifacts: Vec<RawArtifact>,
810    pub error: Option<String>,
811}
812
813/// A raw output file a client produced, retained as workload evidence.
814#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
815#[serde(deny_unknown_fields)]
816pub struct RawArtifact {
817    pub name: String,
818    pub kind: String,
819    pub path: PathBuf,
820}
821
822/// The schema root aggregating every wire type. It exists to generate one
823/// committed JSON schema (and the Python SDK models); its optional client
824/// fields are never all populated in a single message.
825#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
826#[serde(deny_unknown_fields)]
827pub struct AdapterProtocol {
828    pub request: AdapterRequest,
829    pub response: AdapterResponse,
830    #[serde(default, skip_serializing_if = "Option::is_none")]
831    pub eval_client_request: Option<EvalClientRequest>,
832    #[serde(default, skip_serializing_if = "Option::is_none")]
833    pub eval_client_result: Option<EvalClientResult>,
834    #[serde(default, skip_serializing_if = "Option::is_none")]
835    pub bench_client_request: Option<BenchClientRequest>,
836    #[serde(default, skip_serializing_if = "Option::is_none")]
837    pub bench_client_result: Option<BenchClientResult>,
838}