Skip to content

feat: support per-request model override in /v1/chat/completions - #103

Merged
ilblackdragon merged 15 commits into
nearai:mainfrom
raahimSal:feat-openai-compat-model-override-49
Feb 19, 2026
Merged

ilblackdragon merged 15 commits into
nearai:mainfrom
raahimSal:feat-openai-compat-model-override-49

Conversation

@raahimSal

Copy link
Copy Markdown
Contributor

Summary

  • Implement issue Support per-request model override in /v1/chat/completions #49 by making model selection request-scoped for the OpenAI-compatible API
  • Add model: Option to CompletionRequest and ToolCompletionRequest
  • Remove /v1/chat/completions hard mismatch rejection and pass request model through non-streaming and streaming paths
  • Use req.model (fallback to active model) in NEAR AI providers
  • Preserve model override through worker/orchestrator proxy request structs
  • Update FEATURE_PARITY.md notes for OpenAI-compatible API behavior

Why

PR #31 introduced the OpenAI-compatible API and explicitly tracked per-request model routing as follow-up in #49. This PR closes that gap without mutating shared provider state.

Test plan

  • cargo +1.92.0 test --test openai_compat_integration -- --nocapture
  • Repeated closed loop (3 passes): cargo +1.92.0 test --test openai_compat_integration -- --nocapture

All passes succeeded (11/11 each run).

Closes #49
Refs #31

- add optional model override to completion request types\n- forward request model through gateway, worker, and orchestrator proxy paths\n- use request model in NEAR AI providers with fallback to active model\n- replace model-mismatch integration test with override propagation checks\n- update FEATURE_PARITY.md note for OpenAI-compatible API behavior\n\nRefs nearai#49
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @raahimSal, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the flexibility of the OpenAI-compatible API by introducing support for per-request model overrides. Previously, the API enforced a strict match between the requested model and the active model, limiting dynamic model selection. This change allows users to specify a different model for each completion request, improving adaptability for various use cases without mutating shared provider state.

Highlights

  • Per-Request Model Override: Enabled the ability to specify a model for each request to the OpenAI-compatible API's /v1/chat/completions endpoint, removing the previous restriction to a single active model.
  • API Logic Adjustment: Removed the hard rejection of model mismatches in the OpenAI-compatible API handler and ensured the requested model is passed through both streaming and non-streaming completion paths.
  • Data Structure Enhancement: Introduced an optional 'model' field to the 'CompletionRequest' and 'ToolCompletionRequest' data structures, along with builder methods for setting it.
  • Provider Integration: Updated NEAR AI providers to utilize the per-request model if provided, otherwise falling back to the currently active model.
  • Internal Communication Propagation: Extended proxy request structs in the worker and orchestrator layers to correctly preserve and pass the model override across internal service calls.
  • Documentation and Testing: Updated the 'FEATURE_PARITY.md' documentation to reflect the new capability and added comprehensive integration tests to verify the correct handling of model overrides.
Changelog
  • FEATURE_PARITY.md
    • Updated the feature parity table to reflect support for per-request model override in the OpenAI-compatible API.
  • src/channels/web/openai_compat.rs
    • Removed the model validation logic that previously rejected requests with a model mismatch.
    • Captured the 'req.model' for use in subsequent processing.
    • Modified calls to 'ToolCompletionRequest::new' and 'CompletionRequest::new' to include the 'req.model'.
    • Updated 'handle_streaming' to use 'req.model' for 'model_name' and pass it to completion requests.
  • src/llm/nearai.rs
    • Modified 'complete' and 'tool_complete' methods to use the 'model' field from the request if present, otherwise defaulting to the provider's active model.
  • src/llm/nearai_chat.rs
    • Modified 'complete' and 'tool_complete' methods to use the 'model' field from the request if present, otherwise defaulting to the provider's active model.
  • src/llm/provider.rs
    • Added an 'Option' field named 'model' to both 'CompletionRequest' and 'ToolCompletionRequest' structs.
    • Introduced 'with_model' builder methods for 'CompletionRequest' and 'ToolCompletionRequest' to set the optional model.
  • src/orchestrator/api.rs
    • Updated 'llm_complete' and 'llm_complete_with_tools' functions to pass the 'model' field from the incoming proxy request to the internal 'CompletionRequest' and 'ToolCompletionRequest'.
  • src/worker/api.rs
    • Added an 'Option' field named 'model' to 'ProxyCompletionRequest' and 'ProxyToolCompletionRequest' structs.
    • Ensured the 'model' field is cloned and passed from the incoming request to the internal 'CompletionRequest' and 'ToolCompletionRequest' in 'complete' and 'tool_complete' methods.
  • tests/openai_compat_integration.rs
    • Imported 'Mutex' for thread-safe state management in mock provider.
    • Introduced 'MockLlmState' to track models received by the mock provider.
    • Modified 'MockLlmProvider' to store and retrieve 'MockLlmState'.
    • Updated 'start_test_server' to return 'Arc' and initialized 'MockLlmProvider' with this state.
    • Added assertions in 'test_chat_completions_basic', 'test_chat_completions_with_tools', and 'test_chat_completions_streaming' to verify the model received by the mock provider.
    • Renamed 'test_chat_completions_model_mismatch' to 'test_chat_completions_model_override' and updated its assertions to confirm successful model override (status 200 and correct model in response) instead of a 404 error.
    • Adjusted various test functions to accept and ignore the '_mock_state' if not directly used.
Activity
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request successfully implements per-request model overrides for the OpenAI-compatible API, which is a great feature enhancement. However, it introduces a potential Denial of Service (DoS) vulnerability in the streaming path due to repeated allocations of the user-supplied model name in SSE chunks, which should be addressed by validating or truncating the model name. The suggested refactorings for duplicated code have been deferred to a follow-up task, aligning with the PR's primary goal of feature enhancement. Additionally, there are suggestions for improving the flexibility of with_model functions.

Comment thread src/channels/web/openai_compat.rs Outdated
Comment thread src/llm/provider.rs
Comment thread src/llm/provider.rs

@zmanian zmanian 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.

Clean implementation of per-request model override. The approach is correct:

  1. Remove the hard model-mismatch rejection in the OpenAI-compat handler
  2. Thread an Option<String> model field through CompletionRequest / ToolCompletionRequest
  3. Propagate through proxy structs in worker/orchestrator layers
  4. Providers fall back to active model when the override is None

Test coverage is solid -- the renamed test_chat_completions_model_override now verifies the override flows through to the mock provider via MockLlmState. The Arc<MockLlmState> pattern for tracking received models is clean.

One minor suggestion for a future follow-up: consider logging when a model override is used (at debug level) for observability, since silent model switching could be confusing during debugging.

Closes #49. Good to merge.

@raahimSal

Copy link
Copy Markdown
Contributor Author

Follow-up (previous comment had shell formatting issues):

  • Addressed the high-priority streaming memory concern by validating model length at the OpenAI-compatible boundary (max 256 bytes).
  • Added regression coverage in test_chat_completions_model_too_long.
  • Also fixed gateway runtime wiring so /v1/chat/completions uses the active provider via with_llm_provider in main.

Latest head commit: 91cc007.

@zmanian could you take a quick re-review on the latest commit when you have a moment?

@raahimSal

Copy link
Copy Markdown
Contributor Author

@gemini-code-assist review

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request successfully implements per-request model overrides for the OpenAI-compatible /v1/chat/completions endpoint. The changes are logically sound, propagating the model option through various layers from the web handler down to the LLM providers. The test suite has been effectively updated to cover the new functionality, including model override success cases and input validation failure cases.

My review includes one suggestion to refactor some duplicated code in src/channels/web/openai_compat.rs to improve long-term maintainability. Overall, this is a well-executed feature addition.

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request successfully implements per-request model overrides for the OpenAI-compatible API, which is a great feature enhancement. The changes are well-structured, propagating the model override through various layers of the application, from the web handler to the LLM providers. The accompanying tests are thorough and validate the new functionality effectively. I have one minor suggestion to improve efficiency by avoiding an unnecessary string clone in the streaming path.

@raahimSal
raahimSal requested a review from zmanian February 17, 2026 05:08

@ilblackdragon ilblackdragon left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Code Review: Per-request model override in /v1/chat/completions

Overall this is a well-structured PR that cleanly threads the model override through the request pipeline without mutating shared state. The approach of adding Option<String> to the request types and falling back to the active model in the providers is the right design.

Issues

1. Duplicate with_llm_provider call in src/main.rs (low)

The base branch already sets the LLM provider on the gateway via the builder chain:

let mut gw = GatewayChannel::new(gw_config.clone()).with_llm_provider(llm.clone());

The PR adds a second call:

gw = gw.with_llm_provider(Arc::clone(&llm));

This is harmless (just overwrites the same value), but it is redundant and may confuse future readers who wonder whether the two calls serve different purposes. Remove the duplicate.

2. Model override silently ignored by other LLM providers (medium)

Only NearAiProvider and NearAiChatProvider were updated to read req.model. The other LlmProvider implementations -- OpenAiCompatibleChatProvider (src/llm/openai_compatible_chat.rs), RigAdapter (src/llm/rig_adapter.rs), and FailoverProvider (which delegates to inner providers) -- still unconditionally use self.active_model_name() and silently ignore the per-request model field.

If someone configures IronClaw with one of these providers and sends a model override, the override will be silently dropped with no warning. At minimum, consider:

  • Adding req.model.unwrap_or_else(|| self.active_model_name()) to OpenAiCompatibleChatProvider and RigAdapter as well (same pattern used in the NEAR AI providers), or
  • Logging a warning in the LlmProvider trait's default behavior when req.model is Some but the provider doesn't handle it.

3. validate_model_name could be stricter on character set (low/nit)

The validation checks for empty and length > 256, which addresses the DoS concern raised in the earlier review. However, it does not restrict the character set. Model names containing control characters, newlines, or other non-printable characters could end up in JSON responses, log output, or SSE streams. Consider adding a check like:

if !model.bytes().all(|b| b.is_ascii_graphic() || b == b' ') {
    return Err("model contains invalid characters".to_string());
}

This is a defense-in-depth measure since serde_json will escape control chars in output, but it prevents odd values from reaching provider APIs.

Positive observations

  • The Option<String> field with unwrap_or_else(|| self.active_model_name()) in providers is the correct zero-mutation approach. Internal callers that don't set model get the existing behavior automatically.
  • Worker/orchestrator proxy structs (ProxyCompletionRequest, ProxyToolCompletionRequest) correctly propagate the field, maintaining the override across the container boundary.
  • Test coverage is solid: the MockLlmState pattern cleanly verifies the model reaches the provider, and test_chat_completions_model_too_long validates the length guard.
  • The 256-byte limit on model names addresses the streaming DoS concern from the earlier review.

Summary

The core feature is correctly implemented. The main substantive concern is issue #2 (incomplete provider coverage), which could cause confusion when users expect model overrides to work with non-NEAR AI backends. Issue #1 is a quick cleanup. Issue #3 is a hardening suggestion.

@tribendu

Copy link
Copy Markdown

Batch 2 PR Review: nearai/ironclaw PRs 111, 110, 109, 103, 95, 74

Reviewer: AI Sub-Agent
Date: February 17, 2026
Review Method: Manual diff analysis with focus on code quality, bugs, security, performance, test coverage, and Rust best practices


PR #111: Fix backwards compatibility for nearai.session_token

Summary

This PR adds backwards compatibility for the nearai session management by implementing a fallback mechanism. When nearai.session_token is not found, the system now attempts to fall back to the legacy nearai.session setting. The change is minimal and focused on the session token retrieval logic in SessionManager::renew_session(). The implementation uses a graceful fallback pattern with proper error handling and logging.

Pros

  • Graceful degradation: Implements backwards compatibility without breaking existing functionality
  • Clear error handling: Preserves original error handling for both primary and fallback paths
  • Informative logging: Adds a warning message when falling back to legacy session storage
  • Minimal change scope: Only affects session token retrieval logic, reducing risk of regression
  • Proper pattern: Uses idiomatic Rust if let Some(...) for optional value handling

Concerns

  • Technical debt: This adds maintenance burden by keeping two parallel session storage schemas
  • No migration path: There's no clear deprecation timeline or migration strategy for the legacy schema
  • Silent fallback: The warning log may be missed in production, leading to continued use of deprecated schema
  • Error propagation: The error messages remain generic - they could be more specific about whether the primary or fallback path failed

Suggestions

  • Add a migration function to convert legacy sessions to the new format periodically
  • Consider adding telemetry/metrics to track usage of the legacy fallback path
  • Document the expected lifecycle and deprecation timeline for nearai.session
  • Add a configuration option to disable the fallback after migration is complete
  • Consider making the fallback opt-in rather than silent to encourage migration

PR #110: Add env docs for local LLM providers

Summary

This PR updates the .env.example file to document environment variables for local LLM providers including Ollama, LM Studio, vLLM, and other OpenAI-compatible endpoints. The changes are purely documentation-focused, providing clear examples of environment variable configurations for users who want to use local models instead of the default NEAR AI backend.

Pros

  • User-friendly: Makes it easy for new users to configure local LLM providers
  • Comprehensive: Covers multiple backend options (Ollama, OpenAI-compatible)
  • Clear defaults: Shows default values for URLs and ports
  • Well-organized: Groups related configurations logically
  • No code changes: Documentation-only PR, zero risk of breaking functionality

Concerns

  • No validation: Environment variables are documented but there's no validation that the backend value matches the selected provider
  • Incomplete examples: Example model names are specific but may not match what users have installed
  • Missing authentication notes: Doesn't document that LLM_API_KEY is optional for local servers

Suggestions

  • Add comments explaining when LLM_API_KEY is optional vs required
  • Include a link to documentation for each provider
  • Add a note about model availability requiring local installation
  • Consider adding environment variable validation logic in a future PR
  • Document how to switch between providers at runtime
  • Add example commands to test connections (e.g., curl http://localhost:11434/api/tags)

PR #109: Normalize memory search query and update marked.js

Summary

This PR addresses two security and stability issues in the web interface. First, it adds input normalization for memory search queries to prevent excessive query lengths and invalid input types. Second, it updates the marked.js dependency to a specific version with integrity hashing for supply chain security. The changes are defensive in nature, preventing potential DoS attacks and ensuring the integrity of third-party JavaScript dependencies.

Pros

  • Input validation: Adds MEMORY_SEARCH_QUERY_MAX_LENGTH constant (100 chars) to prevent excessively long queries
  • Type safety: Normalizes queries to string type, preventing crashes from non-string input
  • Supply chain security: Uses SRI (Subresource Integrity) hashing for marked.js CDN dependency
  • Consistent application: Applies normalization throughout search functions (searchMemory, snippetAround, highlightQuery)
  • Clear error handling: Early return when normalized query is empty

Concerns

  • Arbitrary limit: 100-character limit may be too restrictive for complex semantic search queries
  • Silent truncation: Queries longer than the limit are silently truncated without user feedback
  • CDN dependency: Still relies on external CDN for marked.js despite SRI - could use bundled version
  • No rate limiting: Query length limit doesn't prevent rapid-fire spam queries
  • Client-side only: Validation happens on client side only - server should also validate

Suggestions

  • Increase the limit or make it configurable (e.g., 200-500 characters)
  • Add user feedback when query is truncated (toast message or inline warning)
  • Implement rate limiting on the /api/memory/search endpoint
  • Add server-side query validation to defend against bypassed client checks
  • Consider bundling marked.js with the application to eliminate CDN dependency
  • Add unit tests for the normalization function covering edge cases (null, undefined, empty string, very long string)

PR #103: Per-request model override for OpenAI-compatible API

Summary

This is a significant feature PR that adds per-request model override capability across the entire LLM provider ecosystem. Previously, all requests used the active model, but now clients can specify a different model per request. The changes span multiple modules: request structs now include optional model fields, providers check for request-level models before falling back to defaults, OpenAI-compatible endpoint validates model names, and comprehensive integration tests verify functionality. The PR includes migration documentation and removes the previous strict model validation that returned 404 for non-active models.

Pros

  • Well-architected: Uses Option with builder pattern (with_model()) for clean API
  • Comprehensive testing: Updated integration tests to mock model tracking and verify model override works
  • Full stack coverage: Changes propagate from API entry points through worker layer to actual providers
  • Good error handling: Validates model name length (MAX_MODEL_NAME_BYTES: 256) before processing
  • Backwards compatible: Existing behavior preserved when model field is not provided
  • Code quality: Consistent pattern across CompletionRequest and ToolCompletionRequest
  • Documentation: Updates FEATURE_PARITY.md to reflect new capability

Concerns

  • No model validation: The endpoint now accepts ANY model name without checking if it exists or is configured
  • Validation bypasses providers: Validation happens at HTTP layer, but providers don't validate model availability
  • Security implications: Users can request models they shouldn't have access to; cost tracking may break
  • Incomplete fallback: If provider doesn't support the requested model, behavior is undefined
  • Missing permissions: No RBAC or policy checking for model override capability
  • Potential for confusion: Active model concept remains but can be overridden per request

Suggestions

  • Add a method to validate if a model exists/can be used before making the request
  • Implement optional model allowlist/denylist configuration per user or API key
  • Add telemetry to track model usage patterns across different requests
  • Document the precedence: request model > active model > provider default
  • Consider adding a force_model flag that fails fast if model is unavailable
  • Add tests for error cases (non-existent model, invalid characters, empty string)
  • Update cost tracking to handle per-request model pricing differences
  • Consider adding model availability check to LlmProvider trait's set_model() method

PR #95: Add Venice AI provider and embeddings

Summary

This PR adds comprehensive support for Venice AI as both an LLM provider and an embeddings provider. It introduces a new VeniceProvider module with full API integration including Venice-specific features like web search, web scraping, and dynamic model pricing fetched from the /models endpoint. The PR also adds VeniceEmbeddings for semantic search, updates configuration handling, and integrates Venice into the setup wizard. The implementation includes extensive unit tests, proper error handling, caching of model catalogs with TTL, and follows the existing provider patterns.

Pros

  • Fully featured: Implements complete Venice API including proprietary parameters (web search, scraping, system prompt)
  • Dynamic pricing: Fetches model catalog with pricing from Venice API, updates cost tracking automatically
  • Good caching: Uses 1-hour TTL for model catalog to reduce API calls
  • Comprehensive testing: Extensive unit tests for message conversion, serialization, cost calculation
  • Type safety:Uses properly typed structures and enums for Venice-specific functionality
  • Consistent patterns: Follows existing provider patterns (NearAiProvider, etc.)
  • Error handling: Proper HTTP error mapping (401→AuthFailed, 429→RateLimited)
  • Config validation: Validates Venice-specific config values (web_search must be "off"/"on"/"auto")

Concerns

  • Large file: venice.rs is 880 lines - consider splitting into smaller modules
  • Lock choice: Uses std::sync::RwLock with comment about async safety - risk if async code is added later
  • Secret handling: API key exposed as String in multiple places despite being wrapped in SecretString in Config
  • No retry logic: Network requests don't have exponential backoff for transient failures
  • Hardcoded timeout: 120-second timeout may be too long or too short for different use cases
  • Cache misses on errors: If catalog fetch fails, stale cache is kept indefinitely
  • Embeddings dimension hardcoding: Magic numbers for dimensions (1536, 3072) scattered in code

Suggestions

  • Split venice.rs into multiple modules: provider.rs, models.rs, api.rs
  • Consider using tokio::sync::RwLock for future-proofing with async code
  • Add retry logic with exponential backoff for API requests
  • Make timeout configurable via VeniceConfig
  • Add metrics for cache hit/miss ratio
  • Document the security model: when are secrets exposed?
  • Extract dimension mapping to a constant or helper function
  • Add integration test that calls actual Venice API with test credentials
  • Consider adding health check endpoint for provider connectivity
  • Add support for streaming responses (if Venice API supports it)

PR #74: Security fix: Enhanced HTTP response size validation

Summary

This PR strengthens the HTTP tool's defense against OOM attacks by implementing two-stage response size validation. First, it checks the Content-Length header before downloading any content to immediately reject oversized responses. Second, it streams the response body with a hard size cap during download, protecting against malicious or misconfigured servers that may send incorrect or missing Content-Length headers. The implementation uses streaming with futures::StreamExt and enforces a consistent 5 MB limit across both stages.

Pros

  • Defense in depth: Two-stage validation (header check + streaming cap) protects against multiple attack vectors
  • Early rejection: Aborts request before downloading potentially malicious content
  • Streaming approach: Memory-efficient, doesn't load entire response before checking size
  • Informative logging: Warns with URL and size details when rejecting responses
  • Consistent constant: Uses same 5 MB limit as WASM HTTP wrapper
  • Well-documented: Clear comments explain the security rationale and 5 MB justification
  • Test coverage: Includes test verifying the constant value is reasonable

Concerns

  • Header spoofing: Relies on server sending accurate Content-Length - malicious servers can send small value then send large body
  • No byte limit on individual chunks: Only checks cumulative size; individual chunks could still be large
  • Error message exposure: Returns detailed size information in errors which might leak information
  • No rate limiting: An attacker could still send many small requests to exhaust resources
  • Hardcoded limit: 5 MB may be too small for some legitimate use cases (e.g., downloading large JSON datasets)

Suggestions

  • Add per-chunk size limit (e.g., 1 MB max per chunk) in addition to cumulative limit
  • Consider making MAX_RESPONSE_SIZE configurable per tool or per request
  • Add telemetry/metrics for rejected responses to detect DDoS patterns
  • Implement request rate limiting in addition to size limiting
  • Add option to override limit for authorized users/admins
  • Consider adding a timeout for the streaming operation
  • Add test cases for Content-Length header spoofing attempt
  • Document how users can work around the limit if needed (e.g., using multiple smaller requests with pagination)

Overall Recommendations

High Priority

  1. PR feat: support per-request model override in /v1/chat/completions #103: Address the security implications of unrestricted model override before merging - add allowlist/denylist functionality
  2. PR fix: check Content-Length before downloading HTTP response body #74: Consider making the size limit configurable for flexibility while maintaining security

Medium Priority

  1. PR feat: add Venice AI as first-class LLM backend #95: Refactor the large venice.rs file and add retry logic for network resilience
  2. PR web: add integrity check for marked CDN and cap highlight regex input #109: Add server-side validation to complement client-side checks
  3. PR llm: fallback to legacy nearai.session key when loading DB session #111: Add migration strategy for legacy session tokens

Low Priority

  1. PR docs: add .env.example examples for Ollama and OpenAI-compatible #110: Add validation documentation and testing commands
  2. PR feat: add Venice AI as first-class LLM backend #95: Extract magic numbers and add integration tests
  3. PR web: add integrity check for marked CDN and cap highlight regex input #109: Consider removing CDN dependency for marked.js

General Observations

  • All PRs follow Rust best practices and maintain code quality
  • Test coverage is generally good, though some integration tests could be expanded
  • Error handling is consistent across the codebase
  • Documentation (comments and inline docs) is clear and helpful
  • Most PRs are backwards compatible, which is good for production deployments

Copilot AI review requested due to automatic review settings February 18, 2026 00:46

Copilot AI left a comment

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.

Pull request overview

Implements issue #49 by making model selection request-scoped for the OpenAI-compatible /v1/chat/completions endpoint, propagating an optional per-request model through internal request types and proxy layers without mutating shared provider state.

Changes:

  • Add model: Option<String> to CompletionRequest and ToolCompletionRequest, with builder helpers.
  • Pass request model through OpenAI-compatible handler (non-streaming + streaming), NEAR AI providers, and worker/orchestrator proxy request structs.
  • Update integration tests and feature parity documentation; add model name validation in the OpenAI-compatible handler.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/openai_compat_integration.rs Extends mock provider/tests to assert per-request model propagation and adds model validation test cases.
src/channels/web/openai_compat.rs Removes model mismatch rejection, validates model name, and forwards request model through streaming/non-streaming codepaths.
src/llm/provider.rs Introduces optional per-request model on internal request types plus with_model() helpers.
src/llm/nearai.rs Uses request model override (fallback to active model) when building NEAR AI requests (including retry path).
src/llm/nearai_chat.rs Uses request model override (fallback to active model) when building NEAR AI chat-completions requests.
src/llm/rig_adapter.rs Warns when an override is requested but ignored for providers that can’t switch models per request.
src/worker/api.rs Preserves model through worker→orchestrator proxy request structs.
src/orchestrator/api.rs Maps proxy request model into internal completion/tool-completion requests.
src/main.rs Injects the shared LLM provider into the web gateway channel.
FEATURE_PARITY.md Notes per-request model override support for the OpenAI-compatible API.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/channels/web/openai_compat.rs Outdated
Comment on lines +469 to +475
let model_name = requested_model;
let id = chat_completion_id();
let created = unix_timestamp();

if has_tools {
let tools = convert_tools(req.tools.as_deref().unwrap_or(&[]));
let mut tool_req = ToolCompletionRequest::new(messages, tools);
let mut tool_req = ToolCompletionRequest::new(messages, tools).with_model(req.model);

Copilot AI Feb 18, 2026

Copy link

Choose a reason for hiding this comment

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

The OpenAI response model is set from the requested model string (requested_model / req.model) even when the underlying provider ignores per-request overrides (e.g. RigAdapter explicitly warns that it uses the configured model). This can cause the API response to report a model that was not actually used. Consider plumbing the actual model used back from the provider (e.g. add model_used to CompletionResponse/ToolCompletionResponse, or add a provider capability/method to resolve the effective model) and use that for the OpenAI model field.

Copilot uses AI. Check for mistakes.
Comment thread src/channels/web/openai_compat.rs Outdated
Comment on lines +579 to +593
@@ -584,7 +590,7 @@ async fn handle_streaming(

let llm_result = if has_tools {
let tools = convert_tools(req.tools.as_deref().unwrap_or(&[]));
let mut tool_req = ToolCompletionRequest::new(messages, tools);
let mut tool_req = ToolCompletionRequest::new(messages, tools).with_model(req.model);

Copilot AI Feb 18, 2026

Copy link

Choose a reason for hiding this comment

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

Same as the non-streaming path: model_name is taken from req.model for SSE chunks, which can misreport the model if the provider does not actually honor per-request overrides (e.g. RigAdapter ignores overrides). The streamed model field should reflect the effective model used by the provider.

Copilot uses AI. Check for mistakes.
Comment thread tests/openai_compat_integration.rs Outdated
Comment on lines 6 to 60
@@ -24,7 +24,21 @@ const AUTH_TOKEN: &str = "test-openai-token";
// Mock LLM provider
// ---------------------------------------------------------------------------

struct MockLlmProvider;
#[derive(Default)]
struct MockLlmState {
completion_models: Mutex<Vec<Option<String>>>,
tool_completion_models: Mutex<Vec<Option<String>>>,
}

struct MockLlmProvider {
state: Arc<MockLlmState>,
}

impl MockLlmProvider {
fn new(state: Arc<MockLlmState>) -> Self {
Self { state }
}
}

#[async_trait]
impl LlmProvider for MockLlmProvider {
@@ -37,6 +51,12 @@ impl LlmProvider for MockLlmProvider {
}

async fn complete(&self, req: CompletionRequest) -> Result<CompletionResponse, LlmError> {
self.state
.completion_models
.lock()
.expect("completion_models lock poisoned")
.push(req.model.clone());

// Echo the last user message back

Copilot AI Feb 18, 2026

Copy link

Choose a reason for hiding this comment

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

This test uses std::sync::Mutex inside async LlmProvider methods. While it’s probably fine here because the lock isn’t held across .await, std::sync::Mutex can still block the Tokio runtime under contention. Using tokio::sync::Mutex (or parking_lot::Mutex) would avoid potential runtime blocking in async code.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings February 18, 2026 01:03

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@raahimSal

Copy link
Copy Markdown
Contributor Author

Addressed the review comments in follow-up commits.

  • Removed redundant with_llm_provider wiring in main.
  • Added model-name hardening (trim-empty plus control-character rejection) in OpenAI-compatible input validation.
  • Added warnings in RigAdapter when per-request model override is requested but unsupported.
  • Added provider-level effective_model_name(...) and switched OpenAI-compatible non-streaming and streaming responses to report the effective model used instead of always echoing requested model.
  • Added regression tests for effective model reporting (streaming and non-streaming) and switched integration test mock state locks to tokio::sync::Mutex.

Validation run:

  • cargo +1.92.0 test --test openai_compat_integration
  • cargo +1.92.0 test rig_adapter

Latest head: f883b8d.

Copilot AI review requested due to automatic review settings February 18, 2026 08:51

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot AI review requested due to automatic review settings February 18, 2026 21:08

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/channels/web/openai_compat.rs Outdated
Comment on lines +386 to +388
if model.trim().is_empty() {
return Err("model must not be empty".to_string());
}

Copilot AI Feb 18, 2026

Copy link

Choose a reason for hiding this comment

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

The validation checks if model.trim().is_empty() but doesn't actually use the trimmed version. This means a model name like " gpt-4 " (with leading/trailing spaces) would pass validation and be sent to the provider with the spaces intact. Consider either rejecting strings that don't equal their trimmed version, or trimming the model name before validation and use.

Suggested change
if model.trim().is_empty() {
return Err("model must not be empty".to_string());
}
let trimmed = model.trim();
if trimmed.is_empty() {
return Err("model must not be empty".to_string());
}
if trimmed != model {
return Err("model must not have leading or trailing whitespace".to_string());
}

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings February 19, 2026 04:08

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

src/llm/response_cache.rs:260

  • Now that CompletionRequest supports per-request model, the response cache key must incorporate the effective model for the request; otherwise prompts sent to different models can collide and return cached responses from the wrong model. Update the cache key computation to use request.model (or better: inner.effective_model_name(request.model.as_deref())) and add/adjust tests here to assert different model overrides create different cache entries.
    fn simple_request() -> CompletionRequest {
        CompletionRequest {
            messages: vec![ChatMessage::user("hello")],
            model: None,
            max_tokens: None,
            temperature: None,
            stop_sequences: None,
            metadata: Default::default(),
        }
    }

    fn different_request() -> CompletionRequest {
        CompletionRequest {
            messages: vec![ChatMessage::user("goodbye")],
            model: None,
            max_tokens: None,
            temperature: None,
            stop_sequences: None,
            metadata: Default::default(),

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/llm/provider.rs
Comment on lines +304 to +312
/// Resolve which model should be reported for a given request.
///
/// Providers that ignore per-request model overrides should override this
/// and return `active_model_name()`.
fn effective_model_name(&self, requested_model: Option<&str>) -> String {
requested_model
.map(std::borrow::ToOwned::to_owned)
.unwrap_or_else(|| self.active_model_name())
}

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

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

effective_model_name() will often be called on wrapper providers (e.g. CircuitBreakerProvider/CachedProvider) rather than the underlying concrete provider. Those wrappers currently don’t override this method, so the default implementation will report requested_model even when the inner provider ignores overrides (e.g. RigAdapter). Add effective_model_name() implementations to wrapper providers that delegate to inner.effective_model_name(requested_model) so OpenAI responses report the model actually used.

Copilot uses AI. Check for mistakes.
Comment thread src/llm/failover.rs
Comment on lines +340 to +342
fn effective_model_name(&self, requested_model: Option<&str>) -> String {
self.providers[self.last_used.load(Ordering::Relaxed)].effective_model_name(requested_model)
}

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

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

FailoverProvider::effective_model_name() uses last_used (a shared atomic) to pick which inner provider to query. Under concurrent requests, last_used can be updated by another request between the completion call and model reporting, causing the OpenAI response to report the wrong model/provider. Consider returning the chosen provider index/model name from try_providers() along with the response (or otherwise threading the chosen provider through the request path) so model reporting is request-scoped.

Copilot uses AI. Check for mistakes.
@serrrfirat

Copy link
Copy Markdown
Collaborator

🔴 High Severity — Response cache key ignores per-request model override

src/llm/response_cache.rs, line 150 (unchanged in this PR, but broken by it):

let key = cache_key(self.inner.model_name(), &request);

The cache_key() function receives the configured model name (self.inner.model_name()), not the per-request request.model. This means two requests with different model overrides but identical messages/parameters produce the same cache key, causing cross-model cache collisions.

Scenario:

  1. Client A sends {"model": "claude-3-haiku", "messages": [{"role": "user", "content": "hello"}]}
  2. Response is cached with key = sha256("configured-model|[hello]|...")
  3. Client B sends {"model": "claude-3-opus", "messages": [{"role": "user", "content": "hello"}]}
  4. Cache hit — Client B receives the haiku response, but the response claims "model": "claude-3-opus"

Impact: Wrong model responses served when response cache is enabled. Verified CachedProvider IS used in production (main.rs wraps the provider when response_cache_enabled is true).

Suggested fix:

// In CachedProvider::complete()
let model = request.model.as_deref().unwrap_or_else(|| self.inner.model_name());
let key = cache_key(model, &request);

Also add a test in response_cache.rs::tests that asserts different request.model values produce different cache keys.

@serrrfirat

Copy link
Copy Markdown
Collaborator

🟡 Medium Severity — Wrapper providers don't delegate effective_model_name()

src/llm/response_cache.rs (CachedProvider) and src/llm/circuit_breaker.rs (CircuitBreakerProvider) both implement LlmProvider but neither overrides effective_model_name().

The default trait implementation returns the requested model if present:

fn effective_model_name(&self, requested_model: Option<&str>) -> String {
    requested_model.map(ToOwned::to_owned).unwrap_or_else(|| self.active_model_name())
}

But RigAdapter overrides this to always return its configured model (ignoring the override):

fn effective_model_name(&self, _requested_model: Option<&str>) -> String {
    self.active_model_name()
}

Problem: When the provider chain is CachedProvider → CircuitBreakerProvider → RigAdapter, calling effective_model_name(Some("gpt-4")) on the outermost wrapper uses the default implementation and returns "gpt-4". But the actual LLM call was served by RigAdapter's configured model (which ignores overrides). The response model field lies to the client.

Suggested fix — add delegation in both wrappers:

// In CachedProvider and CircuitBreakerProvider LlmProvider impls:
fn effective_model_name(&self, requested_model: Option<&str>) -> String {
    self.inner.effective_model_name(requested_model)
}

This ensures the override behavior of the innermost provider propagates through the wrapping chain.

e,
"invalid_request_error",
));
}

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 Severity — No model allowlist for cost control

The old code rejected requests where the model didn't match the configured model (404 model_not_found). This new validation only checks syntax (empty, length, control chars) but accepts any model name and forwards it to the upstream NEAR AI API.

An authenticated client could request expensive models (e.g., claude-opus-4-20250514) through the operator's NEAR AI credentials, bypassing the operator's intended model/cost budget.

Suggestion: Consider an optional allowed_models: Option<Vec<String>> config. If set, validate req.model against it before forwarding. If unset, allow any model (preserving current behavior). This gives operators opt-in cost control without breaking the feature for those who want open model routing.

This is a design consideration rather than a bug — the current behavior may be intentional for NEAR AI's usage model. Worth documenting the security implication either way.

@raahimSal

Copy link
Copy Markdown
Contributor Author

Thanks everyone for the thorough review and detailed feedback.

I pushed a follow-up commit (206d574) that addresses the actionable items raised:

  • Fixed model-name validation at the OpenAI-compatible boundary to reject leading/trailing whitespace (plus new unit + integration coverage).
  • Fixed response-cache keying for per-request model overrides by using the effective model name (prevents cross-model cache collisions), with a regression test.
  • Added effective_model_name(...) delegation in wrapper providers (CachedProvider, CircuitBreakerProvider) so model reporting reflects actual provider behavior.
  • Fixed failover model reporting race by making provider selection request-scoped for effective_model_name(...), with a concurrency regression test.

Validation run on this branch:

  • cargo fmt --all -- --check
  • cargo clippy -- -D warnings
  • cargo test --all-features -- --nocapture

All green locally.

For the optional model allowlist / cost-control suggestion: I agree it is useful, but it broadens scope into policy/config. I kept this PR focused on correctness and compatibility fixes; happy to open a follow-up issue if maintainers want that in a separate change.

@ilblackdragon
ilblackdragon merged commit ccf6005 into nearai:main Feb 19, 2026
2 checks passed
This was referenced Feb 19, 2026
jaswinder6991 pushed a commit to jaswinder6991/ironclaw that referenced this pull request Feb 26, 2026
…rai#103)

* feat: support per-request model override for /v1/chat/completions

- add optional model override to completion request types\n- forward request model through gateway, worker, and orchestrator proxy paths\n- use request model in NEAR AI providers with fallback to active model\n- replace model-mismatch integration test with override propagation checks\n- update FEATURE_PARITY.md note for OpenAI-compatible API behavior\n\nRefs nearai#49

* Wire gateway OpenAI-compatible routes to active LLM provider

* Validate OpenAI model name length before streaming

* Address PR103 review feedback on model override and validation

* Report effective model in OpenAI-compatible responses

* Use async mutexes in OpenAI compatibility integration tests

* fix tests for per-request model field in response cache

* fix formatting and clippy lint after main merge

* Fix model override reporting and cache correctness

---------

Co-authored-by: Illia Polosukhin <ilblackdragon@gmail.com>
bkutasi pushed a commit to bkutasi/ironclaw that referenced this pull request Mar 28, 2026
…rai#103)

* feat: support per-request model override for /v1/chat/completions

- add optional model override to completion request types\n- forward request model through gateway, worker, and orchestrator proxy paths\n- use request model in NEAR AI providers with fallback to active model\n- replace model-mismatch integration test with override propagation checks\n- update FEATURE_PARITY.md note for OpenAI-compatible API behavior\n\nRefs nearai#49

* Wire gateway OpenAI-compatible routes to active LLM provider

* Validate OpenAI model name length before streaming

* Address PR103 review feedback on model override and validation

* Report effective model in OpenAI-compatible responses

* Use async mutexes in OpenAI compatibility integration tests

* fix tests for per-request model field in response cache

* fix formatting and clippy lint after main merge

* Fix model override reporting and cache correctness

---------

Co-authored-by: Illia Polosukhin <ilblackdragon@gmail.com>
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.

Support per-request model override in /v1/chat/completions

6 participants