Merge pull request 'feat(cortex): re-expose flat max_model_len / max_input_tokens / max_output_tokens on /v1/models (#78)' (#107) from feat/78-max-model-len into main
Some checks failed
build-prerelease / Lint (fmt + clippy) (push) Blocked by required conditions
build-prerelease / Build neuron-blackwell (push) Blocked by required conditions
build-prerelease / Resolve version stamps + change detection (push) Successful in 13s
build-prerelease / Test (push) Has been cancelled
build-prerelease / Build cortex binary (push) Has been cancelled
build-prerelease / Build helexa-bench binary (push) Has been cancelled
build-prerelease / Build helexa-upstream binary (push) Has been cancelled
build-prerelease / Build neuron-ampere (push) Has been cancelled
build-prerelease / Build neuron-ada (push) Has been cancelled
build-prerelease / Package cortex RPM (push) Has been cancelled
build-prerelease / Package helexa-bench RPM (push) Has been cancelled
build-prerelease / Package helexa-upstream RPM (push) Has been cancelled
build-prerelease / Package helexa-neuron-ada RPM (push) Has been cancelled
build-prerelease / Package helexa-neuron-ampere RPM (push) Has been cancelled
build-prerelease / Package helexa-neuron-blackwell RPM (push) Has been cancelled
build-prerelease / Publish to rpm.lair.cafe (unstable) (push) Has been cancelled
Some checks failed
build-prerelease / Lint (fmt + clippy) (push) Blocked by required conditions
build-prerelease / Build neuron-blackwell (push) Blocked by required conditions
build-prerelease / Resolve version stamps + change detection (push) Successful in 13s
build-prerelease / Test (push) Has been cancelled
build-prerelease / Build cortex binary (push) Has been cancelled
build-prerelease / Build helexa-bench binary (push) Has been cancelled
build-prerelease / Build helexa-upstream binary (push) Has been cancelled
build-prerelease / Build neuron-ampere (push) Has been cancelled
build-prerelease / Build neuron-ada (push) Has been cancelled
build-prerelease / Package cortex RPM (push) Has been cancelled
build-prerelease / Package helexa-bench RPM (push) Has been cancelled
build-prerelease / Package helexa-upstream RPM (push) Has been cancelled
build-prerelease / Package helexa-neuron-ada RPM (push) Has been cancelled
build-prerelease / Package helexa-neuron-ampere RPM (push) Has been cancelled
build-prerelease / Package helexa-neuron-blackwell RPM (push) Has been cancelled
build-prerelease / Publish to rpm.lair.cafe (unstable) (push) Has been cancelled
This commit was merged in pull request #107.
This commit is contained in:
@@ -147,6 +147,39 @@ pub struct CortexModelEntry {
|
|||||||
/// `true` when any neuron reports this model supports reasoning tokens.
|
/// `true` when any neuron reports this model supports reasoning tokens.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub reasoning: bool,
|
pub reasoning: bool,
|
||||||
|
// ── Flat ecosystem context-window fields (issue #78) ──────
|
||||||
|
// Duplicates of `limit` under the flat, vLLM-convention key names
|
||||||
|
// (`max_model_len` et al.) that OpenAI-ecosystem clients (Hermes
|
||||||
|
// Agent, vLLM tooling) probe for — they cannot see `limit.context`.
|
||||||
|
// Additive: `limit` stays the opencode-oriented source of truth.
|
||||||
|
// Derived, never set directly — call [`sync_flat_limit`] after the
|
||||||
|
// final `limit` value is known. Omitted (not `0`) when the window
|
||||||
|
// is unknown; absent-vs-zero is load-bearing, as with `cost`.
|
||||||
|
//
|
||||||
|
// [`sync_flat_limit`]: CortexModelEntry::sync_flat_limit
|
||||||
|
/// Served max-seq-len in tokens — mirrors `limit.context`.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub max_model_len: Option<usize>,
|
||||||
|
/// Usable input budget — mirrors `limit.input` when present.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub max_input_tokens: Option<usize>,
|
||||||
|
/// Maximum generation tokens — mirrors `limit.output`.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub max_output_tokens: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CortexModelEntry {
|
||||||
|
/// Re-derive the flat ecosystem fields (#78) from `limit`.
|
||||||
|
///
|
||||||
|
/// Must run after the final `limit` is known (post merge/tightening),
|
||||||
|
/// immediately before serialization. Fully overwrites: a `None` limit
|
||||||
|
/// clears the flat fields, so stale values can't survive a merge that
|
||||||
|
/// dropped the limit.
|
||||||
|
pub fn sync_flat_limit(&mut self) {
|
||||||
|
self.max_model_len = self.limit.as_ref().map(|l| l.context);
|
||||||
|
self.max_input_tokens = self.limit.as_ref().and_then(|l| l.input);
|
||||||
|
self.max_output_tokens = self.limit.as_ref().map(|l| l.output);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|||||||
@@ -581,6 +581,11 @@ async fn list_models(State(fleet): State<Arc<CortexState>>) -> Json<Value> {
|
|||||||
// Runtime-detected — will be OR-ed in Pass 2 from neuron data.
|
// Runtime-detected — will be OR-ed in Pass 2 from neuron data.
|
||||||
tool_call: false,
|
tool_call: false,
|
||||||
reasoning: false,
|
reasoning: false,
|
||||||
|
// Flat #78 fields are derived from `limit` in the final
|
||||||
|
// sync pass, once merging is done.
|
||||||
|
max_model_len: None,
|
||||||
|
max_input_tokens: None,
|
||||||
|
max_output_tokens: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -638,6 +643,9 @@ async fn list_models(State(fleet): State<Arc<CortexState>>) -> Json<Value> {
|
|||||||
cost: None,
|
cost: None,
|
||||||
tool_call: entry.tool_call,
|
tool_call: entry.tool_call,
|
||||||
reasoning: entry.reasoning,
|
reasoning: entry.reasoning,
|
||||||
|
max_model_len: None,
|
||||||
|
max_input_tokens: None,
|
||||||
|
max_output_tokens: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -694,6 +702,9 @@ async fn list_models(State(fleet): State<Arc<CortexState>>) -> Json<Value> {
|
|||||||
cost: None,
|
cost: None,
|
||||||
tool_call: false,
|
tool_call: false,
|
||||||
reasoning: false,
|
reasoning: false,
|
||||||
|
max_model_len: None,
|
||||||
|
max_input_tokens: None,
|
||||||
|
max_output_tokens: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -728,11 +739,24 @@ async fn list_models(State(fleet): State<Arc<CortexState>>) -> Json<Value> {
|
|||||||
cost: target_entry.cost.clone(),
|
cost: target_entry.cost.clone(),
|
||||||
tool_call: target_entry.tool_call,
|
tool_call: target_entry.tool_call,
|
||||||
reasoning: target_entry.reasoning,
|
reasoning: target_entry.reasoning,
|
||||||
|
max_model_len: None,
|
||||||
|
max_input_tokens: None,
|
||||||
|
max_output_tokens: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let data: Vec<Value> = entries.values().map(|e| json!(e)).collect();
|
// Final pass: derive the flat ecosystem context-window fields (#78)
|
||||||
|
// from each entry's now-settled `limit`, so vLLM-convention clients
|
||||||
|
// (Hermes Agent et al.) can read the window without knowing helexa's
|
||||||
|
// `limit` schema.
|
||||||
|
let data: Vec<Value> = entries
|
||||||
|
.values_mut()
|
||||||
|
.map(|e| {
|
||||||
|
e.sync_flat_limit();
|
||||||
|
json!(e)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
Json(json!({
|
Json(json!({
|
||||||
"object": "list",
|
"object": "list",
|
||||||
"data": data,
|
"data": data,
|
||||||
|
|||||||
@@ -8,8 +8,11 @@
|
|||||||
//! - `cost` from the catalogue profile (operator-set pricing).
|
//! - `cost` from the catalogue profile (operator-set pricing).
|
||||||
//! - `tool_call` / `reasoning` from the neuron's runtime detection (OR-ed in)
|
//! - `tool_call` / `reasoning` from the neuron's runtime detection (OR-ed in)
|
||||||
//!
|
//!
|
||||||
//! Also a regression guard for the removal of `max_model_len` — the misnamed,
|
//! Also asserts the flat, vLLM-convention duplicates (`max_model_len`,
|
||||||
//! unconsumed vLLM-ism that this contract replaces.
|
//! `max_input_tokens`, `max_output_tokens`) mirror `limit` (#78): the
|
||||||
|
//! earlier removal of `max_model_len` as "unconsumed" was wrong — Hermes
|
||||||
|
//! Agent (and the wider OpenAI client ecosystem) probes those flat keys
|
||||||
|
//! and cannot see `limit.context`.
|
||||||
|
|
||||||
use cortex_core::config::{
|
use cortex_core::config::{
|
||||||
EvictionSettings, EvictionStrategy, GatewayConfig, GatewaySettings, NeuronEndpoint,
|
EvictionSettings, EvictionStrategy, GatewayConfig, GatewaySettings, NeuronEndpoint,
|
||||||
@@ -85,6 +88,21 @@ capabilities = ["text"]
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
// A model with no derivable limit: the flat #78 fields must be
|
||||||
|
// OMITTED (absent-vs-zero is load-bearing), never 0 or a guess.
|
||||||
|
node.models.insert(
|
||||||
|
"no-limit-model".into(),
|
||||||
|
ModelEntry {
|
||||||
|
id: "no-limit-model".into(),
|
||||||
|
status: ModelStatus::Loaded,
|
||||||
|
last_accessed: None,
|
||||||
|
vram_estimate_mb: None,
|
||||||
|
capabilities: vec!["text".into()],
|
||||||
|
tool_call: false,
|
||||||
|
reasoning: false,
|
||||||
|
limit: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let app = cortex_gateway::build_app(Arc::clone(&fleet));
|
let app = cortex_gateway::build_app(Arc::clone(&fleet));
|
||||||
@@ -123,11 +141,26 @@ capabilities = ["text"]
|
|||||||
assert_eq!(entry["tool_call"], true);
|
assert_eq!(entry["tool_call"], true);
|
||||||
assert_eq!(entry["reasoning"], true);
|
assert_eq!(entry["reasoning"], true);
|
||||||
|
|
||||||
// Regression guard: the removed, unconsumed vLLM-ism must not reappear.
|
// Flat ecosystem duplicates (#78) mirror the advertised `limit` so
|
||||||
assert!(
|
// vLLM-convention probes (Hermes Agent) auto-detect the window.
|
||||||
entry.get("max_model_len").is_none(),
|
assert_eq!(entry["max_model_len"], 49152);
|
||||||
"max_model_len was removed; /v1/models must not advertise it"
|
assert_eq!(entry["max_input_tokens"], 40960);
|
||||||
);
|
assert_eq!(entry["max_output_tokens"], 8192);
|
||||||
|
|
||||||
|
// No limit → flat fields omitted entirely, never 0 or a guess.
|
||||||
|
let unknown = body["data"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.find(|m| m["id"] == "no-limit-model")
|
||||||
|
.expect("no-limit-model present in /v1/models");
|
||||||
|
assert!(unknown.get("limit").is_none());
|
||||||
|
for key in ["max_model_len", "max_input_tokens", "max_output_tokens"] {
|
||||||
|
assert!(
|
||||||
|
unknown.get(key).is_none(),
|
||||||
|
"{key} must be omitted when the window is unknown"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let _ = std::fs::remove_file(&cat_path);
|
let _ = std::fs::remove_file(&cat_path);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,12 @@ pub fn aggregate_models(topology: &HashMap<String, CortexTopology>) -> Vec<Corte
|
|||||||
|
|
||||||
let mut out: Vec<CortexModelEntry> = merged.into_values().collect();
|
let mut out: Vec<CortexModelEntry> = merged.into_values().collect();
|
||||||
out.sort_by(|a, b| a.id.cmp(&b.id));
|
out.sort_by(|a, b| a.id.cmp(&b.id));
|
||||||
|
// Re-derive the flat ecosystem fields (#78) from the merged (tightest)
|
||||||
|
// limit — the values deserialized from each cortex are per-operator and
|
||||||
|
// may not match the federation-wide merge.
|
||||||
|
for e in &mut out {
|
||||||
|
e.sync_flat_limit();
|
||||||
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +80,10 @@ fn router_entry(cortex: &str, e: &CortexModelEntry) -> CortexModelEntry {
|
|||||||
cost: e.cost.clone(),
|
cost: e.cost.clone(),
|
||||||
tool_call: e.tool_call,
|
tool_call: e.tool_call,
|
||||||
reasoning: e.reasoning,
|
reasoning: e.reasoning,
|
||||||
|
// Derived from `limit` by the final sync pass in aggregate_models.
|
||||||
|
max_model_len: None,
|
||||||
|
max_input_tokens: None,
|
||||||
|
max_output_tokens: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +161,9 @@ mod tests {
|
|||||||
cost: None,
|
cost: None,
|
||||||
tool_call: false,
|
tool_call: false,
|
||||||
reasoning: false,
|
reasoning: false,
|
||||||
|
max_model_len: None,
|
||||||
|
max_input_tokens: None,
|
||||||
|
max_output_tokens: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,5 +252,9 @@ mod tests {
|
|||||||
assert_eq!(out.len(), 1);
|
assert_eq!(out.len(), 1);
|
||||||
assert_eq!(out[0].limit.as_ref().unwrap().context, 16_384);
|
assert_eq!(out[0].limit.as_ref().unwrap().context, 16_384);
|
||||||
assert_eq!(out[0].cost.as_ref().unwrap().input, 0.20);
|
assert_eq!(out[0].cost.as_ref().unwrap().input, 0.20);
|
||||||
|
// Flat #78 fields re-derived from the merged (tightest) limit.
|
||||||
|
assert_eq!(out[0].max_model_len, Some(16_384));
|
||||||
|
assert_eq!(out[0].max_input_tokens, None);
|
||||||
|
assert_eq!(out[0].max_output_tokens, Some(4096));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ fn model_entry(loaded: bool, feasible: bool) -> CortexModelEntry {
|
|||||||
cost: None,
|
cost: None,
|
||||||
tool_call: false,
|
tool_call: false,
|
||||||
reasoning: false,
|
reasoning: false,
|
||||||
|
max_model_len: None,
|
||||||
|
max_input_tokens: None,
|
||||||
|
max_output_tokens: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user