Skip to content

feat(inference): add llmisvc_model_provider_resolver - #699

Open
jland-redhat wants to merge 2 commits into
praxis-proxy:mainfrom
jland-redhat:llmisvc_model_provider_resolver
Open

feat(inference): add llmisvc_model_provider_resolver#699
jland-redhat wants to merge 2 commits into
praxis-proxy:mainfrom
jland-redhat:llmisvc_model_provider_resolver

Conversation

@jland-redhat

@jland-redhat jland-redhat commented Aug 10, 2026

Copy link
Copy Markdown

Summary

  • Add llmisvc_model_provider_resolver, porting only the LLMISvc / KServe BBR body-rewrite path from IPP’s model-provider-resolver.
  • Prefer a configurable model header (default X-Model, aligned with model_to_header), fall back to body "model", and when the value is a publisher ID (publishers/.../models/<name>) rewrite the body "model" to <name> only.
  • Leave the routing header untouched so KServe can still route on the publisher ID; stash the original ID in llmisvc_model_provider_resolver.publisher_id for metering.
  • Includes unit tests, example config, integration coverage, and generated filter docs.

Does not port ExternalModel / ExternalProvider resolution, weighted provider selection, Host rewrite, api-format detection, or credential handling.

Sister PR (merge after this)

Without the ExtProc follow-up, body rewrites that change length will fail in Envoy BUFFERED + header SEND mode even though this filter’s rewrite is correct.

Test plan

  • Unit tests for rewrite / header preference / body fallback / non-publisher passthrough
  • Example config + integration tests
  • Validated on local cluster with publisher-ID model request; upstream returned a completion:
{
  "id": "chatcmpl-efb19481-6952-5be9-9572-ebfb8aa9070b",
  "model": "demo/sim-stream",
  "object": "chat.completion",
  "choices": [
    {
      "finish_reason": "stop",
      "message": {
        "role": "assistant",
        "content": "I am fine, how are you today? ..."
      }
    }
  ]
}

@jland-redhat
jland-redhat requested review from a team and leseb August 10, 2026 20:38
@praxis-bot-app

Copy link
Copy Markdown

Missing Signed-off-by: 5113bb3. All commits require sign-off (via git commit --signoff).

@praxis-bot-app

Copy link
Copy Markdown

AI tool authorship detected:

  • d11ef9e: Co-authored-by: Cursor <cursoragent@cursor.com>

Sorry, this project does not accept commits authored by tools as valid.
Commits need to be authored by and signed-off by the human(s) responsible for the PR, with their name and contact.

Signed-off-by: jland <jland@redhat.com>
@Jaland
Jaland force-pushed the llmisvc_model_provider_resolver branch from d11ef9e to b0d0e9a Compare August 10, 2026 20:46
@jland-redhat jland-redhat changed the title Adding nnew llmisvc_model_provider_resolver feat(inference): add llmisvc_model_provider_resolver Aug 10, 2026
@Jaland
Jaland force-pushed the llmisvc_model_provider_resolver branch from b53a455 to d03e60a Compare August 10, 2026 21:14
@praxis-bot-app

Copy link
Copy Markdown

Unsigned commits: d03e60a. Please sign your commits.

return Some(from_header);
}

obj.get("model")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why this if model_to_header ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

model_to_header puts the model in the header.

But the model serving needs the header to be a "canonical id" publishers//models/<MODEL_NAME>. So the way BBR works in 3.5 is that we have the user put this value in the body "models" value and we move it into the header and replace it with the real <MODEL_NAME>

This resolver is that second part.

External Models do something similar but replacement will happen based on the ExternalModel CRD.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I mean, this seems to "if it's not in the header as provided by model_to_header", it will fall back to getting the value from where model_to_header would/should have gotten the value.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Hmm maybe we can sync on this so I can better understand but I would think that is ok right?

It is faster to get this from the header and we fallback to the model if it is not there seems reasonable. But I can just remove the fallback if we think that makes more sense.

return Ok(FilterAction::Continue);
}

obj.insert("model".to_owned(), serde_json::Value::String(short_name.to_owned()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there ever a chance that "model" won't be in the request body on a valid request?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Not if it is following the OpenAI spec I don't believe, and it has to follow that spec if it is a vLLM model which should be the only thing that this filter would catch.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ok. I'm hinting at we could pretty easy do a StringBuffer splice to inject the new model value to avoid the full DOM deserialize and serialize of the complete body. (handling missing model fields is a little bit more tricky, but a replace in a buffer is easy)

Backtick CamelCase identifiers for doc_markdown, shrink
tests under too_many_lines, and set metrics_route when
building against praxis main.

Signed-off-by: jland <jland@redhat.com>
@Jaland
Jaland force-pushed the llmisvc_model_provider_resolver branch from d03e60a to 8f9c832 Compare August 11, 2026 00:45

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

praxis-bot review

Clean filter implementation with good test coverage, proper config validation, and correct use of replace_json_body. One edge case around body mutation when the "model" field is absent.

Findings: 1 medium

return Ok(FilterAction::Continue);
}

obj.insert("model".to_owned(), serde_json::Value::String(short_name.to_owned()));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Medium] When resolve_model_name obtains the model from the header (not the body), the body might not contain a "model" key at all. In that case obj.insert(...) silently adds a "model" field the caller never sent, which could surprise backends that do not expect one (e.g. non-completions endpoints that happen to share a pipeline).

Guard the insert so it only rewrites an existing field:

if !obj.contains_key("model") {
    return Ok(FilterAction::Continue);
}

obj.insert("model".to_owned(), serde_json::Value::String(short_name.to_owned()));

Add a unit test: header carries a publisher ID, body is {"messages":[]} (no "model"), assert the body is unchanged after the filter runs.

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

praxis-bot review (round 2)

Fix commit is clean: backtick formatting, CI metrics_route field, and test loop refactor all look correct. One nit below. The prior Medium (body mutation when "model" absent) remains open.

Findings: 1 nit

assert_eq!(
llmisvc_short_model_name("publishers/ns/models/a/b"),
Some("a/b"),
"SplitN keeps remainder after first /models/"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Nit] Assertion message says SplitN but the implementation (line 279) uses split_once. They are semantically similar, but the message should match the actual method for accuracy.

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

praxis-bot review (round 3)

Clean implementation with good separation of concerns. Config validation (deny_unknown_fields, empty-header rejection, validate_max_body_bytes), the header-vs-body resolution chain, and the split_once-based publisher-ID parsing are all correct. Integration tests exercise the core rewrite, routing-header preservation, and non-publisher passthrough end-to-end. The register.rs test refactor to a loop is a nice cleanup.

The prior Medium from round 1 (body mutation when "model" absent -- obj.insert(...) adds a field the caller never sent) remains the only actionable concern and is not yet addressed.

Findings: 0 new (prior Medium still open)

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.

3 participants